JavaScript input-mask library. Source in lib/ (plain JS, ES modules). TypeScript declarations are generated from JS via tsc --emitDeclarationOnly — never hand-edit dist/types/.
| Task | Command |
|---|---|
| Build the bundles | npm run build |
| Rebuild on change | npm run build:watch |
| Generate the type declarations | npm run types |
| Lint only | npm run lint |
| Full validate (build + types + node + lint + jsdom) | npm test |
| Browser (Karma) | npm run test:chrome |
| Node + jsdom (Karma) | npm run test:jsdom |
| Node, DOM-less bundles | npm run test:node |
| Build the framework packages | npm run build:packages |
| Test the framework packages | npm run test:packages |
| BrowserStack matrix | npm run test:browserstack |
Command order matters: npm test runs build → types → node → lint → jsdom. Running the steps out of order fails (the tests need the built dist/ bundles, the lint and the type generation need the sources).
Gruntfile.js is kept for the release chores (version bump, NuGet packaging, publishing) and as a wrapper around the same chain. npx grunt validate additionally copies Changelog.md to inputmask-pages/src/assets/Changelog.md, so that file is a generated copy: edit the root changelog and run the copy, never the mirror.
- Entry point:
lib/index.js— public entry; re-exports the headless class and attaches the DOM driver (lib/dom/attach.js). Deep importlib/core/inputmask.jsfor the DOM-free class only - Headless class:
lib/core/inputmask.js— constructor, aliases/definitions, validation/format statics and the headless session API; part oflib/core/, so it never importslib/dom/ - Headless core:
lib/core/— mask lexer, validation, positioning, key processing, input handling, plus the safewindowshim (window.js); no direct DOM dependencies - DOM driver:
lib/dom/— browser event handlers (eventhandlers.js), event binding (eventruler.js), element patching (mask.js), environment sniffing (environment.js), andattach.jswhich adds the element-bound instance methods (mask,remove) and statics (remove,setValue) to the class - Bundle entry points:
bundle.js(vanilla),bundle.jquery.js(jQuery),bundle.colormask.js - Dependency abstraction:
lib/dependencyLibs/—inputmask.dependencyLib.jsis the vanilla DOM implementation andinputmask.dependencyLib.jquery.jsthe jQuery one;bundle.jquery.jsimports the jQuery variant and assigns it toInputmask.dependencyLib - Extensions:
lib/extensions/— each exports aregister*()function, called inbundle.js - Tests:
qunit/— QUnit test files, bundled by rollup intoqunit/qunit*.js(one per bundle lane);qunit/harness.jsregisters the shared suite - Output formats: ESM (
dist/esm/*.mjs), UMD (dist/inputmask.js), minified UMD (dist/inputmask.min.js) - Framework packages:
packages/*— React, Vue, Svelte and jQuery wrappers built with rollup (shared config inpackages/rollup.config.shared.mjs) and declarations emitted bytscintodist/types; Angular usesng-packagr. Build the core (npm run build && npm run types) beforenpm run build:packages.@inputmask/jqueryalso emits standalone UMD bundles (dist/jquery.inputmask.umd.js,dist/jquery.inputmask.min.js), and its CJS build bundles the core and the extensions because the core exports the extensions through animportcondition only, so a CJS build that keeps them external cannot berequired
build.jsrunsrewrite-imports.jsand then every config ofrollup.config.cjs.rewrite-imports.jsgenerates the extension entry files inlib/.rollup/(gitignored) fromlib/extensions/*.js: the core imports are rewritten to the sibling ESM bundle, so the extension bundles stay small and do not duplicate the core.- Side effects are declared in
rollup.config.cjs(buildSideEffects), not in thesideEffectsfield ofpackage.json. That field describes the published package for consumers, the config describes the build. Everyresolve()runs withignoreSideEffectsForRoot, so node-resolve does not apply thesideEffectsfield of the root package in parallel: a resolver suppliedmoduleSideEffectswins overtreeshake.moduleSideEffectsand silently drops side-effect-only imports. - A module only has to be listed when it is imported for its side effects alone.
moduleSideEffects: falsedoes not remove statements from a module that is included, it only allows dropping the module itself. - The UMD bundles register the
<input-mask>custom element (lib/components/inputmaskElement.js); the ESM main bundle does not.lib/index.jsdoes not import the element, sodist/esm/inputmask.mjsstays free of it and the element is opt-in throughdist/esm/inputmaskElement.mjs. npm run build:watchwatches the sources and rebuilds on change. It re-runsrewrite-imports.jsbefore every rebuild, because the extension bundles are built from the generatedlib/.rollup/copies: an edit tolib/extensions/*.jsonly reaches rollup once the copies are regenerated (the rewrite skips the copies whose content did not change). The previous build'scacheis passed back to rollup, so only the changed modules are re-transformed.dist/,lib/.rollup/andqunit/qunit*.jsare excluded from the watch, otherwise the build would trigger itself. A change torollup.config.cjsneeds a restart, since the config is required once.
QUnit is the only test framework. The browser suite runs once per built bundle:
| Lane | Command | Notes |
|---|---|---|
| Browser, UMD native | npm run test:umd |
dist/inputmask.js, no jQuery on the page |
| Browser, UMD jQuery | npm run test:jquery |
dist/jquery.inputmask.js, loads global jQuery first |
| Browser, ESM | npm run test:esm |
dist/esm/inputmask.mjs + extension bundles, no jQuery |
| All browser bundles | npm run test:bundles |
Runs the three lanes sequentially |
| Node + jsdom (Karma) | npm run test:jsdom |
Same suite via the jsdom launcher, UMD native bundle |
| All jsdom bundles | npm run test:jsdom:bundles |
jsdom equivalent of test:bundles |
| Node, DOM-less bundles | npm run test:node |
Runs qunit/tests_node.js against dist/ (build first) |
| BrowserStack matrix | npm run test:browserstack |
Real desktop + mobile devices (cross-env CI=true karma start karma.conf.js --single-run); needs BROWSERSTACK_USERNAME/BROWSERSTACK_ACCESS_KEY in the environment |
karma.conf.jsselects the test bundle withINPUTMASK_BUNDLE=umd|jquery|esm(defaultumd). The jQuery lane is the only one that addsnode_modules/jquery/dist/jquery.jsanddist/jquery.inputmask.js(as a global script) to the page; the native and ESM lanes run without jQuery.- Expected results: UMD native 937 passed + 1 skipped of 938, UMD jQuery 938 passed + 1 skipped of 939, ESM 937 passed + 1 skipped of 938. The native/ESM skip is the jQuery-plugin insertMode test (the
tests_jquery_inputmask.jsmodule is not registered there); the jQuery skip is the native dependencyLibextendguard test. qunit/harness.jsregisters the shared suite against whateverInputmaskthe entry passes in. The entries arequnit/index.js(UMD native),qunit/index.jquery.js(UMD jQuery) andqunit/index.esm.js(ESM core +dist/esm/extensions/*.mjs+dist/esm/dom/FormData.mjs, because the ESM entry does not register extensions itself).- The ESM build intentionally does not emit the jQuery plugin or binding (
rollup.config.cjshas those outputs disabled): ESM consumers use the native dependencyLib, so there is no ESM jQuery lane. npm testis unchanged: build, types, node lane, lint, and the UMD-native jsdom lane.npm run test:bundlesis the opt-in full browser matrix.QUnit.config.testTimeoutis set to 10s (client.qunit.testTimeoutinkarma.conf.js) so a stalled async test fails instead of tripping Karma's 30sbrowserNoActivityTimeoutand disconnecting the whole browser.- Real mobile BrowserStack (
bs_chrome_pixel*,bs_*_iPhone*) runs withdocument.hasFocus() === false, so the browser never dispatchesfocus/blureven thoughelement.focus()setsdocument.activeElement.qunit/focusShim.js(installed by the harness) replays the withheld event so the focus/blur-driven paths (showMaskOnFocus,clearMaskOnLostFocus,clearIncomplete,onincomplete) run as on a focused page; all real desktop + mobile devices pass the default UMD-native lane (937 executed + 1 jQuery-only skip). It is a no-op when the browser fires the real event. - The browser/jsdom suite is built by rollup into
qunit/qunit.js,qunit/qunit.jquery.jsandqunit/qunit.esm.js, so the build has to run first. qunit/tests_node.jsloadsdist/through Node's CommonJSrequireand a dynamicimportand must not be imported by the harness: the assertions rely on the Node semantics.- Only the jQuery-plugin tests (
qunit/tests_jquery_inputmask.jsand the jQuery insertMode test) need jQuery; the other test modules useInputmask.dependencyLib, so they exercise the backend of whichever bundle is under test. - To run a focused test: edit
qunit/harness.jsto register only the test module you need, rebuild the bundles, then run Karma.
numericInputis auto-set totruein the numeric alias when the user doesn't provide it (genMaskinlib/extensions/numeric.js). It flipsisRTLtotrue(inputmask.js:get isRTL() { return this.opts.numericInput || this.opts.isRTL; }).- When
isRTL/numericInputis active, buffers and caret positions are reversed relative to what the user sees: the mask is reversed in the mask-lexer,getBuffer()returns mask-order (reversed from display),handleRemoveswaps Backspace↔Delete and swapspos.begin/pos.end, andrefreshFromBufferreverses viabffr = inputmask.isRTL ? buffer.slice().reverse() : buffer. - Always read buffers with the display orientation in mind (e.g. internal
00,65displays as56,00). inputEventOnly: truedoes not bind keydown; the input event firesinputFallBackEvent, which mapsdeleteContentBackwardto a synthetic BackspacekeyEvent.- In the numeric
onBeforeWritekeydown branch, a normal backspace triggersrefreshFromBuffervia thecheckAlignmentpath (digit realignment), not via thebuffer: []negation-symbol branch — the negation branch can stay cold.
- Prettier: double quotes, no trailing commas, 2-space indent
- ESLint: flat config (
eslint.config.mjs), ESM native flat config based on@eslint/js+eslint-plugin-import-x+eslint-plugin-n+eslint-plugin-prettier(rules ported inline from the legacyeslint-config-standard) - Semicolons required (with omitLastInOneLineBlock)
import-x/orderenforced: newlines between import groups, alphabetical- Source files live in
lib/, never hand-editdist/: it is generated by rollup and committed, so regenerate it (npm run build) and commit the result