Repository: Tinkoff/maskito
Branch: main
Commit: f79688db8ac1
Files: 757
Total size: 1.6 MB
Directory structure:
gitextract_lbehwmqa/
├── .cspell.json
├── .editorconfig
├── .firebaserc
├── .github/
│ ├── CODEOWNERS
│ ├── CODE_OF_CONDUCT.md
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── 1-bug-report.yml
│ │ ├── 2-feature-request.yml
│ │ └── 3-documentation.yml
│ ├── renovate.json
│ └── workflows/
│ ├── assign-author.yml
│ ├── auto-merge.yml
│ ├── build.yml
│ ├── deploy-preview.yml
│ ├── deploy.yml
│ ├── e2e.yml
│ ├── lint.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .npmrc
├── .release-it.js
├── .ws-context
├── CHANGELOG.md
├── LICENSE
├── README.md
├── codecov.yml
├── eslint.config.ts
├── firebase.json
├── jest.config.ts
├── jest.preset.js
├── nx.json
├── package.json
├── projects/
│ ├── angular/
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── ng-package.json
│ │ ├── package.json
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ ├── lib/
│ │ │ │ ├── maskito.directive.ts
│ │ │ │ ├── maskito.pipe.ts
│ │ │ │ ├── pattern.directive.ts
│ │ │ │ └── tests/
│ │ │ │ ├── maskito.directive.spec.ts
│ │ │ │ └── maskito.spec.ts
│ │ │ └── test-setup.ts
│ │ └── tsconfig.lib.prod.json
│ ├── core/
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ └── src/
│ │ ├── index.ts
│ │ └── lib/
│ │ ├── classes/
│ │ │ ├── index.ts
│ │ │ ├── mask-history.ts
│ │ │ └── mask-model/
│ │ │ ├── mask-model.ts
│ │ │ ├── tests/
│ │ │ │ ├── dynamic-mask.spec.ts
│ │ │ │ └── mask-model-fixed-characters.spec.ts
│ │ │ └── utils/
│ │ │ ├── apply-overwrite-mode.ts
│ │ │ ├── calibrate-value-by-mask.ts
│ │ │ ├── get-leading-fixed-characters.ts
│ │ │ ├── guess-valid-value-by-pattern.ts
│ │ │ ├── guess-valid-value-by-reg-exp.ts
│ │ │ ├── is-fixed-character.ts
│ │ │ ├── remove-fixed-mask-characters.ts
│ │ │ └── validate-value-with-mask.ts
│ │ ├── constants/
│ │ │ ├── default-element-predicate.ts
│ │ │ ├── default-options.ts
│ │ │ └── index.ts
│ │ ├── mask.ts
│ │ ├── plugins/
│ │ │ ├── broken-prevent-default.plugin.ts
│ │ │ ├── change-event-plugin.ts
│ │ │ ├── double-space.plugin.ts
│ │ │ ├── index.ts
│ │ │ ├── initial-calibration-plugin.ts
│ │ │ └── strict-composition-plugin.ts
│ │ ├── types/
│ │ │ ├── element-predicate.ts
│ │ │ ├── element-state.ts
│ │ │ ├── index.ts
│ │ │ ├── mask-options.ts
│ │ │ ├── mask-processors.ts
│ │ │ ├── mask.ts
│ │ │ ├── maskito-element.ts
│ │ │ ├── plugin.ts
│ │ │ ├── selection-range.ts
│ │ │ └── typed-input-event.ts
│ │ └── utils/
│ │ ├── content-editable.ts
│ │ ├── dom/
│ │ │ ├── event-listener.ts
│ │ │ ├── get-content-editable-selection.ts
│ │ │ ├── history-events.ts
│ │ │ ├── hotkey.ts
│ │ │ ├── set-content-editable-selection.ts
│ │ │ └── update-element.ts
│ │ ├── element-states-equality.ts
│ │ ├── get-line-selection.ts
│ │ ├── get-not-empty-selection.ts
│ │ ├── get-word-selection.ts
│ │ ├── index.ts
│ │ ├── pipe.ts
│ │ ├── test/
│ │ │ ├── get-not-empty-selection.spec.ts
│ │ │ ├── get-word-selection.spec.ts
│ │ │ ├── pipe.spec.ts
│ │ │ └── transform.spec.ts
│ │ └── transform.ts
│ ├── demo/
│ │ ├── .gitignore
│ │ ├── esbuild-plugins/
│ │ │ ├── maskito-as-taiga-ui-dep.plugin.js
│ │ │ └── vue-esm.plugin.js
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── app.component.html
│ │ │ │ ├── app.component.spec.ts
│ │ │ │ ├── app.component.ts
│ │ │ │ ├── app.config.ts
│ │ │ │ ├── app.routes.ts
│ │ │ │ ├── app.style.less
│ │ │ │ ├── constants/
│ │ │ │ │ ├── demo-path.ts
│ │ │ │ │ ├── doc-example-primary-tab.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── modules/
│ │ │ │ │ ├── example-primary-tabs-icons/
│ │ │ │ │ │ ├── angular-logo.component.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── javascript-logo.component.ts
│ │ │ │ │ │ ├── react-logo.component.ts
│ │ │ │ │ │ └── vue-logo.component.ts
│ │ │ │ │ └── logo/
│ │ │ │ │ ├── logo.component.ts
│ │ │ │ │ ├── logo.style.less
│ │ │ │ │ └── logo.template.html
│ │ │ │ ├── server-error-handler.ts
│ │ │ │ └── utils/
│ │ │ │ ├── add-default-tabs-processor/
│ │ │ │ │ ├── add-default-tabs-processor.ts
│ │ │ │ │ └── default-tabs/
│ │ │ │ │ ├── angular-default-tab.ts
│ │ │ │ │ ├── js-default-tab.ts
│ │ │ │ │ ├── react-default-tab.ts
│ │ │ │ │ └── vue-default-tab.ts
│ │ │ │ └── index.ts
│ │ │ ├── assets/
│ │ │ │ └── manifest.webmanifest
│ │ │ ├── environments/
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── index.html
│ │ │ ├── main.server.ts
│ │ │ ├── main.ts
│ │ │ ├── pages/
│ │ │ │ ├── documentation/
│ │ │ │ │ ├── browser-support/
│ │ │ │ │ │ ├── browser-support.component.ts
│ │ │ │ │ │ └── browser-support.template.html
│ │ │ │ │ ├── core-concepts-overview/
│ │ │ │ │ │ ├── core-concepts-overview.component.ts
│ │ │ │ │ │ ├── core-concepts-overview.styles.less
│ │ │ │ │ │ ├── core-concepts-overview.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ └── maskito-public-api-demo.md
│ │ │ │ │ ├── element-state/
│ │ │ │ │ │ ├── element-state.component.ts
│ │ │ │ │ │ ├── element-state.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ └── element-state-demo.md
│ │ │ │ │ ├── mask-expression/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── basic-time-example.md
│ │ │ │ │ │ │ ├── dynamic-mask-expression-demo.md
│ │ │ │ │ │ │ └── reg-exp-mask-expression-demo.md
│ │ │ │ │ │ ├── mask-expression.component.ts
│ │ │ │ │ │ └── mask-expression.template.html
│ │ │ │ │ ├── maskito-libraries/
│ │ │ │ │ │ ├── maskito-libraries.component.ts
│ │ │ │ │ │ └── maskito-libraries.template.html
│ │ │ │ │ ├── next-steps/
│ │ │ │ │ │ ├── next-steps.component.ts
│ │ │ │ │ │ └── next-steps.template.html
│ │ │ │ │ ├── overwrite-mode/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── dynamic/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── replace/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── shift/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── overwrite-mode.component.ts
│ │ │ │ │ │ └── overwrite-mode.template.html
│ │ │ │ │ ├── plugins/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-initial-calibration/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ ├── index.md
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 2-strict-composition/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 3-change-event/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── oversimplified-number-mask.md
│ │ │ │ │ │ │ └── pads-zero-plugin.ts
│ │ │ │ │ │ ├── plugins.component.ts
│ │ │ │ │ │ ├── plugins.style.less
│ │ │ │ │ │ └── plugins.template.html
│ │ │ │ │ ├── processors/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── postprocessor-in-action.md
│ │ │ │ │ │ │ ├── preprocessor-first-arg-demo.md
│ │ │ │ │ │ │ ├── preprocessor-in-action-demo.md
│ │ │ │ │ │ │ └── processor-second-arg-demo.md
│ │ │ │ │ │ ├── processors.component.ts
│ │ │ │ │ │ └── processors.template.html
│ │ │ │ │ ├── real-world-form/
│ │ │ │ │ │ ├── index.html
│ │ │ │ │ │ ├── index.less
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── supported-input-types/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── password/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── search/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── tel/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── text/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── url/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── supported-input-types.component.ts
│ │ │ │ │ │ └── supported-input-types.template.html
│ │ │ │ │ ├── transformer/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ └── utility-in-action-demo.md
│ │ │ │ │ │ ├── transformer.component.ts
│ │ │ │ │ │ └── transformer.template.html
│ │ │ │ │ └── what-is-maskito/
│ │ │ │ │ ├── what-is-maskito.component.ts
│ │ │ │ │ ├── what-is-maskito.style.less
│ │ │ │ │ └── what-is-maskito.template.html
│ │ │ │ ├── frameworks/
│ │ │ │ │ ├── angular/
│ │ │ │ │ │ ├── angular-doc.component.ts
│ │ │ │ │ │ ├── angular-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ ├── 1-nested/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── 2-nested/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── 3-programmatically/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── 4-pipe/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── 5-custom-unmask-handler/
│ │ │ │ │ │ │ ├── index.html
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── unmask.directive.ts
│ │ │ │ │ │ ├── 6-pattern/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── basic-directive-approach.md
│ │ │ │ │ │ ├── custom-input-example.md
│ │ │ │ │ │ └── import-maskito.md
│ │ │ │ │ ├── react/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-use-maskito-basic-usage/
│ │ │ │ │ │ │ │ ├── example.component.tsx
│ │ │ │ │ │ │ │ └── useMaskitoBasicUsage.tsx
│ │ │ │ │ │ │ ├── 2-element-predicate/
│ │ │ │ │ │ │ │ ├── awesomeInput.tsx
│ │ │ │ │ │ │ │ ├── example.component.tsx
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── 3-merge-ref/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── 3-react-hook-form/
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── with-maskito-register.ts
│ │ │ │ │ │ │ ├── best-bad-practice.md
│ │ │ │ │ │ │ ├── controlled-input.md
│ │ │ │ │ │ │ └── merge-ref.md
│ │ │ │ │ │ ├── react-doc.component.ts
│ │ │ │ │ │ ├── react-doc.style.less
│ │ │ │ │ │ └── react-doc.template.html
│ │ │ │ │ └── vue/
│ │ │ │ │ ├── examples/
│ │ │ │ │ │ ├── best-bad-practice.md
│ │ │ │ │ │ ├── query-nested-input.md
│ │ │ │ │ │ ├── use-maskito-basic-usage.md
│ │ │ │ │ │ └── vue-1/
│ │ │ │ │ │ └── component.ts
│ │ │ │ │ ├── vue-doc.component.ts
│ │ │ │ │ └── vue-doc.template.html
│ │ │ │ ├── kit/
│ │ │ │ │ ├── date/
│ │ │ │ │ │ ├── date-mask-doc.component.ts
│ │ │ │ │ │ ├── date-mask-doc.style.less
│ │ │ │ │ │ ├── date-mask-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ ├── 1-localization/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-min-max/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── maskito-parse-stringify-date-demo.md
│ │ │ │ │ ├── date-range/
│ │ │ │ │ │ ├── date-range-mask-doc.component.ts
│ │ │ │ │ │ ├── date-range-mask-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ ├── 1-date-localization/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-min-max/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 3-min-max-length/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── 4-range-separator/
│ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ └── mask.ts
│ │ │ │ │ ├── date-time/
│ │ │ │ │ │ ├── date-time-mask-doc.component.ts
│ │ │ │ │ │ ├── date-time-mask-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ ├── 1-date-time-localization/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-date-time-separator/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 3-min-max/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 4-time-step/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 5-am-pm/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── maskito-parse-stringify-date-time-demo.md
│ │ │ │ │ ├── number/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-high-precision/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 2-separators/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 3-postfix/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 4-decimal-zero-padding/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 5-custom-minus-sign/
│ │ │ │ │ │ │ │ ├── components.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 6-minus-before-prefix/
│ │ │ │ │ │ │ │ ├── components.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 7-dynamic-decimal-zero-padding/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 8-thousand-separator-pattern/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 9-thousand-separator-pattern-intl/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── helpers/
│ │ │ │ │ │ │ ├── parse-number-as-bigint-type.md
│ │ │ │ │ │ │ ├── parse-number-as-number-type.md
│ │ │ │ │ │ │ ├── parse-number-invalid-usage.md
│ │ │ │ │ │ │ └── stringify-number.md
│ │ │ │ │ │ ├── number-mask-doc.component.ts
│ │ │ │ │ │ └── number-mask-doc.template.html
│ │ │ │ │ ├── plugins/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-selection-handler/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 2-caret-guard/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 3-event-handlers/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 4-reject/
│ │ │ │ │ │ │ ├── animation.css
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ ├── index.md
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── kit-plugins-doc.component.ts
│ │ │ │ │ │ └── kit-plugins-doc.template.html
│ │ │ │ │ └── time/
│ │ │ │ │ ├── examples/
│ │ │ │ │ │ ├── 1-modes/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-am-pm/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 3-step/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 4-affixes/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 5-time-segments-min-max/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── maskito-parse-stringify-time-demo.md
│ │ │ │ │ ├── time-mask-doc.component.ts
│ │ │ │ │ ├── time-mask-doc.style.less
│ │ │ │ │ └── time-mask-doc.template.html
│ │ │ │ ├── pages.ts
│ │ │ │ ├── phone/
│ │ │ │ │ ├── examples/
│ │ │ │ │ │ ├── 1-basic/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-validation/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 3-non-strict/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 4-lazy-metadata/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── simple.md
│ │ │ │ │ │ ├── 5-focus-blur-events/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── 6-national-format/
│ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ └── mask.ts
│ │ │ │ │ ├── phone-doc.component.ts
│ │ │ │ │ ├── phone-doc.style.less
│ │ │ │ │ └── phone-doc.template.html
│ │ │ │ ├── recipes/
│ │ │ │ │ ├── card/
│ │ │ │ │ │ ├── card-doc.component.ts
│ │ │ │ │ │ ├── card-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ └── 1-basic/
│ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ ├── style.less
│ │ │ │ │ │ └── template.html
│ │ │ │ │ ├── content-editable/
│ │ │ │ │ │ ├── content-editable-doc.component.ts
│ │ │ │ │ │ ├── content-editable-doc.template.html
│ │ │ │ │ │ └── examples/
│ │ │ │ │ │ ├── 1-time/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── 2-multi-line/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── maskito-with-content-editable.md
│ │ │ │ │ │ └── vanilla-js-tab.md
│ │ │ │ │ ├── network-address/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-ipv6/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 2-ipv4/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 3-mac/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── network-address-doc.component.ts
│ │ │ │ │ │ └── network-address-doc.template.html
│ │ │ │ │ ├── phone/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-us-phone/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 2-kz-phone/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ ├── mask.ts
│ │ │ │ │ │ │ └── template.html
│ │ │ │ │ │ ├── phone-doc.component.ts
│ │ │ │ │ │ └── phone-doc.template.html
│ │ │ │ │ ├── placeholder/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-cvc-code/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ ├── 2-phone/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 3-date/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── placeholder-doc.component.ts
│ │ │ │ │ │ └── placeholder-doc.template.html
│ │ │ │ │ ├── postfix/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-pattern-mask/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 2-postprocessor/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── postfix-doc.component.ts
│ │ │ │ │ │ └── postfix-doc.template.html
│ │ │ │ │ ├── prefix/
│ │ │ │ │ │ ├── examples/
│ │ │ │ │ │ │ ├── 1-pattern-mask/
│ │ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ │ └── 2-postprocessor/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ ├── prefix-doc.component.ts
│ │ │ │ │ │ └── prefix-doc.template.html
│ │ │ │ │ └── textarea/
│ │ │ │ │ ├── examples/
│ │ │ │ │ │ ├── 1-latin/
│ │ │ │ │ │ │ ├── component.ts
│ │ │ │ │ │ │ └── mask.ts
│ │ │ │ │ │ └── maskito-with-textarea.md
│ │ │ │ │ ├── textarea-doc.component.ts
│ │ │ │ │ └── textarea-doc.template.html
│ │ │ │ └── stackblitz/
│ │ │ │ ├── components/
│ │ │ │ │ ├── stackblitz-edit-button/
│ │ │ │ │ │ ├── stackblitz-edit-button.component.ts
│ │ │ │ │ │ └── stackblitz-edit-button.style.less
│ │ │ │ │ └── stackblitz-starter/
│ │ │ │ │ ├── stackblitz-starter.component.ts
│ │ │ │ │ └── stackblitz-starter.style.less
│ │ │ │ ├── files/
│ │ │ │ │ ├── example.ts.md
│ │ │ │ │ ├── starter.ts.md
│ │ │ │ │ └── styles.css
│ │ │ │ ├── index.ts
│ │ │ │ └── stackblitz.service.ts
│ │ │ ├── server.ts
│ │ │ ├── styles.less
│ │ │ ├── test-setup.ts
│ │ │ └── typings.d.ts
│ │ ├── tsconfig.app.json
│ │ └── tsconfig.typecheck.json
│ ├── demo-integrations/
│ │ ├── cypress-react.config.ts
│ │ ├── cypress.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── fixtures/
│ │ │ │ └── example.json
│ │ │ ├── plugins/
│ │ │ │ └── index.js
│ │ │ ├── support/
│ │ │ │ ├── assertions/
│ │ │ │ │ ├── have-ng-control-value.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── commands/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── paste.ts
│ │ │ │ │ └── smart-tick.ts
│ │ │ │ ├── component-index.html
│ │ │ │ ├── component-react.ts
│ │ │ │ ├── component.ts
│ │ │ │ ├── constants/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── real-events-support.ts
│ │ │ │ └── e2e.ts
│ │ │ └── tests/
│ │ │ ├── addons/
│ │ │ │ └── phone/
│ │ │ │ ├── phone-basic.cy.ts
│ │ │ │ ├── phone-national-trunk-prefix.cy.ts
│ │ │ │ ├── phone-non-strict.cy.ts
│ │ │ │ ├── phone-separator.cy.ts
│ │ │ │ └── phone-strict.cy.ts
│ │ │ ├── angular/
│ │ │ │ ├── form-control-changes.cy.ts
│ │ │ │ └── unmask-handler.cy.ts
│ │ │ ├── component-testing/
│ │ │ │ ├── angular/
│ │ │ │ │ ├── disable-mask-on-null.cy.ts
│ │ │ │ │ └── pattern.cy.ts
│ │ │ │ ├── angular-predicate/
│ │ │ │ │ ├── angular-predicate.cy.ts
│ │ │ │ │ └── multi-test.component.ts
│ │ │ │ ├── change-event-plugin/
│ │ │ │ │ └── change-event-plugin.cy.ts
│ │ │ │ ├── initial-calibration-plugin/
│ │ │ │ │ └── dispatch-event.cy.ts
│ │ │ │ ├── multi-character-date-segment-separator/
│ │ │ │ │ └── multi-character-date-segment-separator.cy.ts
│ │ │ │ ├── native-max-length/
│ │ │ │ │ └── native-maxlength-attribute.cy.ts
│ │ │ │ ├── native-select-method/
│ │ │ │ │ └── native-select-method.cy.ts
│ │ │ │ ├── number/
│ │ │ │ │ ├── alone-decimal-separator.cy.ts
│ │ │ │ │ ├── min-max-bigint.cy.ts
│ │ │ │ │ ├── mirrored-prefix-postfix.cy.ts
│ │ │ │ │ ├── mirrored-value-postfix.cy.ts
│ │ │ │ │ ├── multi-character-prefix.cy.ts
│ │ │ │ │ ├── overwrite-selection-range.cy.ts
│ │ │ │ │ ├── postfix-multi-character.cy.ts
│ │ │ │ │ ├── postfix-with-point.cy.ts
│ │ │ │ │ ├── runtime-postfix-changes/
│ │ │ │ │ │ ├── runtime-postfix-changes.cy.ts
│ │ │ │ │ │ └── sandbox.component.ts
│ │ │ │ │ └── with-initial-value.cy.ts
│ │ │ │ ├── overwrite-mode/
│ │ │ │ │ └── overwrite-mode-replace.cy.ts
│ │ │ │ ├── paste/
│ │ │ │ │ └── cy-paste-utility.cy.ts
│ │ │ │ ├── phone/
│ │ │ │ │ ├── phone-national-format.cy.ts
│ │ │ │ │ └── phone-with-initial-value.cy.ts
│ │ │ │ ├── placeholder/
│ │ │ │ │ ├── placeholder-dispatch-input-events.cy.ts
│ │ │ │ │ ├── placeholder-has-same-characters-as-textfield.cy.ts
│ │ │ │ │ └── placeholder-partial-removal-on-blur.cy.ts
│ │ │ │ ├── react/
│ │ │ │ │ ├── async-predicate-options-race/
│ │ │ │ │ │ ├── reactApp.tsx
│ │ │ │ │ │ └── reactAsyncPredicateOptionsRace.cy.tsx
│ │ │ │ │ ├── async-predicates-race/
│ │ │ │ │ │ ├── reactApp.tsx
│ │ │ │ │ │ └── reactAsyncPredicatesRace.cy.tsx
│ │ │ │ │ ├── awesomeInput.tsx
│ │ │ │ │ └── change-event/
│ │ │ │ │ └── changeEvent.cy.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── kit/
│ │ │ │ ├── date/
│ │ │ │ │ ├── date-basic.cy.ts
│ │ │ │ │ ├── date-fullwidth-to-halfwidth.cy.ts
│ │ │ │ │ ├── date-min-max.cy.ts
│ │ │ │ │ ├── date-mode.cy.ts
│ │ │ │ │ ├── date-segments-zero-padding.cy.ts
│ │ │ │ │ └── date-separator.cy.ts
│ │ │ │ ├── date-range/
│ │ │ │ │ ├── date-range-basic.cy.ts
│ │ │ │ │ ├── date-range-custom-range-separator.cy.ts
│ │ │ │ │ ├── date-range-fullwidth-to-halfwidth.cy.ts
│ │ │ │ │ ├── date-range-min-max-length.cy.ts
│ │ │ │ │ ├── date-range-min-max.cy.ts
│ │ │ │ │ ├── date-range-mode.cy.ts
│ │ │ │ │ ├── date-range-segments-zero-padding.cy.ts
│ │ │ │ │ └── date-range-separator.cy.ts
│ │ │ │ ├── date-time/
│ │ │ │ │ ├── date-time-basic.cy.ts
│ │ │ │ │ ├── date-time-date-time-separator.cy.ts
│ │ │ │ │ ├── date-time-fullwidth-to-halfwidth.cy.ts
│ │ │ │ │ ├── date-time-meridiem.cy.ts
│ │ │ │ │ ├── date-time-min-max.cy.ts
│ │ │ │ │ ├── date-time-mode.cy.ts
│ │ │ │ │ ├── date-time-separator.cy.ts
│ │ │ │ │ └── date-time-time-step.cy.ts
│ │ │ │ ├── number/
│ │ │ │ │ ├── number-basic.cy.ts
│ │ │ │ │ ├── number-bigint.cy.ts
│ │ │ │ │ ├── number-decimal-separator.cy.ts
│ │ │ │ │ ├── number-decimal-zero-padding.cy.ts
│ │ │ │ │ ├── number-examples.cy.ts
│ │ │ │ │ ├── number-fullwidth-to-halfwidth.cy.ts
│ │ │ │ │ ├── number-max-validation.cy.ts
│ │ │ │ │ ├── number-min-validation.cy.ts
│ │ │ │ │ ├── number-minus-before-prefix.cy.ts
│ │ │ │ │ ├── number-minus-sign.cy.ts
│ │ │ │ │ ├── number-precision.cy.ts
│ │ │ │ │ ├── number-prefix-postfix.cy.ts
│ │ │ │ │ ├── number-thousand-separator.cy.ts
│ │ │ │ │ ├── number-zero-integer-part.cy.ts
│ │ │ │ │ └── utils.ts
│ │ │ │ └── time/
│ │ │ │ ├── time-affixes.cy.ts
│ │ │ │ ├── time-basic.cy.ts
│ │ │ │ ├── time-fullwidth-to-halfwidth.cy.ts
│ │ │ │ ├── time-meridiem.cy.ts
│ │ │ │ ├── time-mode.cy.ts
│ │ │ │ ├── time-segment-max-values.cy.ts
│ │ │ │ └── time-step.cy.ts
│ │ │ ├── react/
│ │ │ │ └── element-predicate.cy.ts
│ │ │ ├── recipes/
│ │ │ │ ├── card/
│ │ │ │ │ └── card.cy.ts
│ │ │ │ ├── content-editable/
│ │ │ │ │ ├── multi-line.cy.ts
│ │ │ │ │ └── single-line-time-mask.cy.ts
│ │ │ │ ├── network-address/
│ │ │ │ │ ├── ipv4.cy.ts
│ │ │ │ │ ├── ipv6.cy.ts
│ │ │ │ │ └── mac.cy.ts
│ │ │ │ ├── phone/
│ │ │ │ │ └── phone.cy.ts
│ │ │ │ ├── placeholder/
│ │ │ │ │ ├── date.cy.ts
│ │ │ │ │ ├── us-phone.cy.ts
│ │ │ │ │ └── сvc-code.cy.ts
│ │ │ │ ├── plugins/
│ │ │ │ │ └── reject.cy.ts
│ │ │ │ ├── postfix/
│ │ │ │ │ ├── percentage.cy.ts
│ │ │ │ │ └── postprocessor.cy.ts
│ │ │ │ ├── prefix/
│ │ │ │ │ ├── dynamic-pattern-mask-expression.cy.ts
│ │ │ │ │ └── postprocessor.cy.ts
│ │ │ │ └── textarea/
│ │ │ │ └── textarea-latin-letters-digits.cy.ts
│ │ │ ├── ssr/
│ │ │ │ └── ssr.cy.ts
│ │ │ ├── utils.ts
│ │ │ └── vue/
│ │ │ └── vue.cy.ts
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ ├── kit/
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ └── src/
│ │ ├── index.ts
│ │ └── lib/
│ │ ├── constants/
│ │ │ ├── date-segment-max-values.ts
│ │ │ ├── default-decimal-pseudo-separators.ts
│ │ │ ├── default-min-max-dates.ts
│ │ │ ├── default-pseudo-minuses.ts
│ │ │ ├── default-time-segment-bounds.ts
│ │ │ ├── index.ts
│ │ │ ├── meridiem.ts
│ │ │ ├── time-fixed-characters.ts
│ │ │ ├── time-segment-value-lengths.ts
│ │ │ └── unicode-characters.ts
│ │ ├── masks/
│ │ │ ├── date/
│ │ │ │ ├── date-mask.ts
│ │ │ │ ├── date-params.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── tests/
│ │ │ │ │ └── date-mask.spec.ts
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── parse-date.ts
│ │ │ │ ├── stringify-date.ts
│ │ │ │ ├── tests/
│ │ │ │ │ ├── parse-date.spec.ts
│ │ │ │ │ └── stringify-date.spec.ts
│ │ │ │ └── to-date-segments.ts
│ │ │ ├── date-range/
│ │ │ │ ├── constants.ts
│ │ │ │ ├── date-range-mask.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── processors/
│ │ │ │ │ ├── min-max-range-length-postprocessor.ts
│ │ │ │ │ └── swap-dates-postprocessor.ts
│ │ │ │ └── tests/
│ │ │ │ ├── date-segments-zero-padding.spec.ts
│ │ │ │ └── pseudo-range-separators.spec.ts
│ │ │ ├── date-time/
│ │ │ │ ├── constants/
│ │ │ │ │ ├── date-time-separator.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── date-time-mask.ts
│ │ │ │ ├── date-time-params.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── postprocessors/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── min-max-date-time-postprocessor.ts
│ │ │ │ ├── preprocessors/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── valid-date-time-preprocessor.ts
│ │ │ │ ├── tests/
│ │ │ │ │ ├── date-segments-zero-padding.spec.ts
│ │ │ │ │ ├── date-time-separator.spec.ts
│ │ │ │ │ └── pseudo-date-end-separator.spec.ts
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-date-time-string-complete.ts
│ │ │ │ ├── parse-date-time.ts
│ │ │ │ ├── split-date-time-string.ts
│ │ │ │ ├── stringify-date-time.ts
│ │ │ │ └── tests/
│ │ │ │ ├── parse-date-time.spec.ts
│ │ │ │ ├── split-date-time-string.spec.ts
│ │ │ │ └── stringify-date-time.spec.ts
│ │ │ ├── number/
│ │ │ │ ├── index.ts
│ │ │ │ ├── number-mask.ts
│ │ │ │ ├── number-params.ts
│ │ │ │ ├── plugins/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── leading-zeroes-validation.plugin.ts
│ │ │ │ │ ├── min-max.plugin.ts
│ │ │ │ │ └── not-empty-integer.plugin.ts
│ │ │ │ ├── processors/
│ │ │ │ │ ├── affixes-filter-preprocessor.ts
│ │ │ │ │ ├── decimal-zero-padding-postprocessor.ts
│ │ │ │ │ ├── empty-postprocessor.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── initialization-only-preprocessor.ts
│ │ │ │ │ ├── leading-minus-deletion-preprocessor.ts
│ │ │ │ │ ├── leading-zeroes-validation-postprocessor.ts
│ │ │ │ │ ├── min-max-postprocessor.ts
│ │ │ │ │ ├── non-removable-chars-deletion-preprocessor.ts
│ │ │ │ │ ├── not-empty-integer-part-preprocessor.ts
│ │ │ │ │ ├── number-prefix-postprocessor.ts
│ │ │ │ │ ├── pseudo-character-preprocessor.ts
│ │ │ │ │ ├── repeated-decimal-separator-preprocessor.ts
│ │ │ │ │ ├── tests/
│ │ │ │ │ │ ├── leading-zeroes-validation-postprocessor.spec.ts
│ │ │ │ │ │ └── not-empty-integer-part-preprocessor.spec.ts
│ │ │ │ │ ├── thousand-separator-postprocessor.ts
│ │ │ │ │ └── zero-precision-preprocessor.ts
│ │ │ │ ├── tests/
│ │ │ │ │ └── number-mask.spec.ts
│ │ │ │ └── utils/
│ │ │ │ ├── extract-affixes.ts
│ │ │ │ ├── generate-mask-expression.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── number-parts.ts
│ │ │ │ ├── parse-number.ts
│ │ │ │ ├── stringify-number-without-exp.ts
│ │ │ │ ├── stringify-number.ts
│ │ │ │ ├── tests/
│ │ │ │ │ ├── parse-number.spec.ts
│ │ │ │ │ ├── stringify-number-without-exp.spec.ts
│ │ │ │ │ ├── stringify-number.spec.ts
│ │ │ │ │ ├── to-number-parts.spec.ts
│ │ │ │ │ └── validate-decimal-pseudo-separators.spec.ts
│ │ │ │ ├── validate-decimal-pseudo-separators.ts
│ │ │ │ └── with-number-defaults.ts
│ │ │ └── time/
│ │ │ ├── index.ts
│ │ │ ├── time-mask.ts
│ │ │ ├── time-params.ts
│ │ │ └── utils/
│ │ │ ├── index.ts
│ │ │ ├── parse-time.ts
│ │ │ ├── stringify-time.ts
│ │ │ └── tests/
│ │ │ ├── parse-time.spec.ts
│ │ │ └── stringify-time.spec.ts
│ │ ├── plugins/
│ │ │ ├── add-on-focus.ts
│ │ │ ├── caret-guard.ts
│ │ │ ├── event-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── reject-event.ts
│ │ │ ├── remove-on-blur.ts
│ │ │ ├── selection-change.ts
│ │ │ └── time/
│ │ │ ├── meridiem-stepping.ts
│ │ │ └── time-segments-stepping.ts
│ │ ├── processors/
│ │ │ ├── colon-convert-preprocessor.ts
│ │ │ ├── date-segments-zero-padding-postprocessor.ts
│ │ │ ├── first-date-end-separator-preprocessor.ts
│ │ │ ├── fullwidth-to-halfwidth-preprocessor.ts
│ │ │ ├── index.ts
│ │ │ ├── invalid-time-segment-insertion-preprocessor.ts
│ │ │ ├── meridiem-processors.ts
│ │ │ ├── min-max-date-postprocessor.ts
│ │ │ ├── normalize-date-preprocessor.ts
│ │ │ ├── postfix-postprocessor.ts
│ │ │ ├── prefix-postprocessor.ts
│ │ │ ├── tests/
│ │ │ │ ├── first-date-end-separator-preprocessor.spec.ts
│ │ │ │ ├── normalize-date-preprocessor.spec.ts
│ │ │ │ ├── postfix-postprocessor.spec.ts
│ │ │ │ ├── prefix-postprocessor.spec.ts
│ │ │ │ ├── valid-date-preprocessor.spec.ts
│ │ │ │ └── with-placeholder.spec.ts
│ │ │ ├── valid-date-preprocessor.ts
│ │ │ ├── with-placeholder.ts
│ │ │ └── zero-placeholders-preprocessor.ts
│ │ ├── types/
│ │ │ ├── date-mode.ts
│ │ │ ├── date-segments.ts
│ │ │ ├── index.ts
│ │ │ ├── time-mode.ts
│ │ │ └── time-segments.ts
│ │ └── utils/
│ │ ├── clamp.ts
│ │ ├── count-digits.ts
│ │ ├── date/
│ │ │ ├── append-date.ts
│ │ │ ├── date-segment-value-length.ts
│ │ │ ├── date-to-segments.ts
│ │ │ ├── get-date-segments-order.ts
│ │ │ ├── get-first-complete-date.ts
│ │ │ ├── is-date-string-complete.ts
│ │ │ ├── parse-date-range-string.ts
│ │ │ ├── parse-date-string.ts
│ │ │ ├── raise-segment-value-to-min.ts
│ │ │ ├── segments-to-date.ts
│ │ │ ├── tests/
│ │ │ │ ├── append-date.spec.ts
│ │ │ │ ├── get-date-segment-value-length.spec.ts
│ │ │ │ └── parse-date-range-string.spec.ts
│ │ │ ├── to-date-string.ts
│ │ │ └── validate-date-string.ts
│ │ ├── dummy.ts
│ │ ├── escape-reg-exp.ts
│ │ ├── find-common-beginning-substr.ts
│ │ ├── index.ts
│ │ ├── is-empty.ts
│ │ ├── pad-with-zeroes-until-valid.ts
│ │ ├── tests/
│ │ │ ├── clamp.spec.ts
│ │ │ ├── escape-reg-exp.spec.ts
│ │ │ ├── find-common-beginning-substr.spec.ts
│ │ │ ├── get-first-complete-date.spec.ts
│ │ │ ├── is-empty.spec.ts
│ │ │ ├── to-half-width-colon.spec.ts
│ │ │ └── to-half-width-number.spec.ts
│ │ ├── time/
│ │ │ ├── create-time-mask-expression.ts
│ │ │ ├── enrich-time-segments-with-zeroes.ts
│ │ │ ├── index.ts
│ │ │ ├── pad-end-time-segments.ts
│ │ │ ├── pad-start-time-segments.ts
│ │ │ ├── pad-time-segments.ts
│ │ │ ├── parse-time-string.ts
│ │ │ ├── tests/
│ │ │ │ ├── enrich-time-segments-with-zeroes.spec.ts
│ │ │ │ ├── parse-time-string.spec.ts
│ │ │ │ └── to-time-string.spec.ts
│ │ │ └── to-time-string.ts
│ │ ├── to-half-width-colon.ts
│ │ └── to-half-width-number.ts
│ ├── phone/
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ └── src/
│ │ ├── index.ts
│ │ └── lib/
│ │ └── masks/
│ │ ├── index.ts
│ │ └── phone/
│ │ ├── constants/
│ │ │ ├── index.ts
│ │ │ └── template-filler.ts
│ │ ├── index.ts
│ │ ├── phone-mask-non-strict.ts
│ │ ├── phone-mask-strict.ts
│ │ ├── phone-mask.ts
│ │ ├── processors/
│ │ │ ├── browser-autofill-preprocessor.ts
│ │ │ ├── cut-init-country-code-preprocessor.ts
│ │ │ ├── index.ts
│ │ │ ├── paste-non-strict-phone-preprocessor.ts
│ │ │ ├── paste-strict-phone-preprocessor.ts
│ │ │ ├── phone-length-postprocessor.ts
│ │ │ └── sanitize-phone-preprocessor.ts
│ │ ├── tests/
│ │ │ ├── get-phone-template.spec.ts
│ │ │ └── phone-mask.spec.ts
│ │ └── utils/
│ │ ├── cut-phone-by-valid-length.ts
│ │ ├── generate-phone-mask.ts
│ │ ├── get-country-from-number.ts
│ │ ├── get-phone-template.ts
│ │ ├── index.ts
│ │ └── select-template.ts
│ ├── react/
│ │ ├── .babelrc
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── package.json
│ │ ├── project.json
│ │ └── src/
│ │ ├── index.ts
│ │ └── lib/
│ │ ├── adaptControlledElement.tsx
│ │ ├── tests/
│ │ │ ├── controlledInput.spec.tsx
│ │ │ ├── elementPredicate.spec.tsx
│ │ │ └── useMaskito.spec.tsx
│ │ ├── useIsomorphicLayoutEffect.tsx
│ │ └── useMaskito.tsx
│ └── vue/
│ ├── README.md
│ ├── jest.config.ts
│ ├── package.json
│ ├── project.json
│ └── src/
│ ├── index.ts
│ └── lib/
│ ├── maskito.spec.ts
│ └── maskito.ts
├── tsconfig.build.json
├── tsconfig.json
└── tsconfig.spec.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .cspell.json
================================================
{
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"import": ["@taiga-ui/cspell-config"],
"files": ["*/*.*"],
"ignorePaths": [
".git",
".gitignore",
".npmrc",
".cspell.json",
"**/dist/**",
"**/assets/**",
"**/node_modules/**",
"**/demo/server.ts",
"**/demo-integrations/src/tests/**",
"*.{log,svg,snap,png,ogv,yml}",
"**/package.json",
"**/tsconfig*.json"
],
"ignoreWords": ["Acpekt", "WHATWG", "prebundle"]
}
================================================
FILE: .editorconfig
================================================
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
================================================
FILE: .firebaserc
================================================
{
"projects": {
"default": "maskito"
}
}
================================================
FILE: .github/CODEOWNERS
================================================
* @taiga-family/core-team
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education,
socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed representative at an online or offline
event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project at
opensource@acpekt.ru. All complaints will be reviewed and investigated and will result in a response that is deemed
necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to
the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
> Thank you for considering contributing to our project. Your help if very welcome!
When contributing, it's better to first discuss the change you wish to make via issue, email, or any other method with
the owners of this repository before making a change.
All members of our community are expected to follow our [Code of Conduct](.github/CODE_OF_CONDUCT.md). Please make sure
you are welcoming and friendly in all of our spaces.
## Getting started
In order to make your contribution please make a fork of the repository. After you've pulled the code, follow these
steps to kick-start the development:
1. Run `npm ci` to install dependencies
2. Run `npm start` to launch demo project where you could test your changes
3. Use following commands to ensure code quality
```bash
npm run lint
npm run build
npm run test
npm run cy:run
```
## Pull Request Process
1. We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) in our commit messages, i.e.
`feat(core): improve typing`
2. Update [demo](projects/demo) application to reflect changes related to public API and everything relevant
3. Make sure you cover all code changes with unit tests and/or [Cypress](https://www.cypress.io) tests
4. When you are ready, create Pull Request of your fork into original repository
================================================
FILE: .github/ISSUE_TEMPLATE/1-bug-report.yml
================================================
name: '🐞 - Bug Report'
title: '🐞 - '
description: Report a bug in the Maskito
labels: ['bug']
type: Bug
body:
- type: dropdown
id: affected-packages
attributes:
label: Which package(s) are the source of the bug?
options:
- '@maskito/core'
- '@maskito/kit'
- '@maskito/phone'
- '@maskito/angular'
- '@maskito/react'
- '@maskito/vue'
- Don't known / other
multiple: true
validations:
required: true
- type: input
id: playground-link
attributes:
label: Playground Link
description: |
Link to an isolated reproduction in our [StackBlitz playground](https://maskito.dev/stackblitz).
If either of the following holds true:
- You can't reproduce the issue in the playground
- Your issue requires some complex setup - such as multiple files or a specific folder structure.
So you can use any link that might help for reproduction bug: github repo, demo url, etc.
***Help us to help you!***
placeholder: https://stackblitz.com/edit/...
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
placeholder: |
Please provide the exception or error you saw.
How do you trigger this bug?
Please walk us through it step by step.
Please provide a screenshot if possible.
validations:
required: true
- type: input
id: maskito-version
attributes:
label: Maskito version
placeholder: x.y.z
validations:
required: true
- type: checkboxes
id: browser-specific
attributes:
label: Which browsers have you used?
description: You may select more than one.
options:
- label: Chrome
- label: Firefox
- label: Safari
- label: Edge
- type: checkboxes
id: operating-systems
attributes:
label: Which operating systems have you used?
description: You may select more than one.
options:
- label: macOS
- label: Windows
- label: Linux
- label: iOS
- label: Android
================================================
FILE: .github/ISSUE_TEMPLATE/2-feature-request.yml
================================================
name: '🚀 - Feature Request'
title: '🚀 - '
description: Suggest a feature for Maskito
labels: ['feature']
type: Feature
body:
- type: dropdown
id: affected-packages
attributes:
label: Which package(s) are relevant/related to the feature request?
options:
- '@maskito/core'
- '@maskito/kit'
- '@maskito/phone'
- '@maskito/angular'
- '@maskito/react'
- '@maskito/vue'
- Don't known / other
multiple: true
- type: textarea
id: description
attributes:
label: Description
placeholder: |
Proposed solution.
Alternatives considered.
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/3-documentation.yml
================================================
name: '📚 - Documentation'
title: '📚 - '
description: Report an issue in Maskito's documentation
labels: ['documentation']
type: Documentation
body:
- type: input
id: affected-url
attributes:
label: What is the affected URL?
- type: textarea
id: description
attributes:
label: Description
placeholder: |
How do you trigger this bug?
Please walk us through it step by step.
Please provide a screenshot if possible.
validations:
required: true
- type: checkboxes
id: browser-specific
attributes:
label: Which browsers have you used?
description: You may select more than one.
options:
- label: Chrome
- label: Firefox
- label: Safari
- label: Edge
- type: checkboxes
id: operating-systems
attributes:
label: Which operating systems have you used?
description: You may select more than one.
options:
- label: macOS
- label: Windows
- label: Linux
- label: iOS
- label: Android
================================================
FILE: .github/renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>taiga-family/renovate-config"],
"packageRules": [
{
"enabled": false,
"matchPackageNames": [
"/^@nx.*/",
"/^nx$/",
"cypress"
]
},
{
"enabled": false,
"matchPackageNames": ["jest-preset-angular"]
}
]
}
================================================
FILE: .github/workflows/assign-author.yml
================================================
name: 🤖 PR author as an assignee
on:
pull_request:
types: [opened]
jobs:
assign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: toshimaru/auto-author-assign@v3.0.1
continue-on-error: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/auto-merge.yml
================================================
name: 🤖 PR auto merge
on:
pull_request:
env:
PR_JOBS_NAME: '[ "Packages", "Demo", "Firebase", "Lint result", "tests", "E2E result" ]'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.value }}
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- id: matrix
run: echo "value=$PR_JOBS_NAME" >> $GITHUB_OUTPUT
wait:
needs: [setup]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
value: ${{ fromJSON(needs.setup.outputs.matrix) }}
steps:
- uses: taiga-family/ci/actions/run/wait-job@v1.201.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: ${{ matrix.value }}
approve:
needs: [wait]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/auto/approve/double@v1.201.0
if: env.IS_TAIGA_FAMILY_BOT_PR_AUTHOR == 'true'
with:
token1: ${{ secrets.GITHUB_TOKEN }}
token2: ${{ secrets.TAIGA_FAMILY_APPROVE_BOT_PAT }}
- uses: taiga-family/ci/actions/run/merge@v1.201.0
if: env.IS_TAIGA_FAMILY_BOT_PR_AUTHOR == 'true'
with:
token: ${{ secrets.TAIGA_FAMILY_BOT_PAT }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on:
pull_request:
push:
branches: [main]
jobs:
build-packages:
name: Packages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npx nx run-many --target build --all --exclude=demo
build-demo:
name: Demo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npx nx build-gh-pages
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/deploy-preview.yml
================================================
name: Deploy / preview
on: pull_request
jobs:
build_and_preview:
name: Firebase
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npx nx run-many --target build --all --exclude=demo
- run: npx nx run demo:build:typecheck
- name: Debug output
run: tree dist/demo/browser -P '*.html'
- name: Deploy preview
uses: FirebaseExtended/action-hosting-deploy@v0.10.0
if: env.IS_OWNER_MODE == 'true'
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_MASKITO }}
projectId: maskito
expires: 1d
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy
on:
workflow_dispatch:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'chore(release)') }}
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npx nx build-gh-pages
- uses: JamesIves/github-pages-deploy-action@v4.8.0
with:
branch: gh-pages
folder: dist/demo/browser
silent: false
clean: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/e2e.yml
================================================
name: E2E
on: [pull_request]
env:
CACHE_DIST_KEY: dist-${{ github.ref }}-${{ github.sha }}
CYPRESS_CACHE_FOLDER: ./node_modules/cache-cypress
UNIVERSAL_SERVER: http://localhost:4000
STATIC_SERVER: http://localhost:8080
jobs:
build-demo:
name: Build demo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
fetch-depth: 10
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npx tsc --project projects/demo-integrations/tsconfig.json
- name: Mark demo-app directory for persist in cache
uses: actions/cache@v5
with:
path: dist/demo
key: ${{ env.CACHE_DIST_KEY }}
- name: Build demo
# --optimization false to keep `window.ng` inside Cypress tests
run: npm run build -- --optimization false
e2e-kit:
needs: [build-demo]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project: [date, date-range, date-time, number, time]
name: Kit / ${{ matrix.project }}
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- uses: taiga-family/ci/actions/setup/cypress@v1.201.0
- name: Download demo build from cache
uses: actions/cache@v5
with:
path: dist/demo
key: ${{ env.CACHE_DIST_KEY }}
- name: Serving SSR server
run: |
npm run serve:ssr -- --ci & sleep 5
curl -X GET -I -f "${{ env.UNIVERSAL_SERVER }}"
- name: Run Cypress tests
run:
npx nx e2e demo-integrations --spec="**/kit/${{ matrix.project }}/**/*.cy.ts" --baseUrl=${{
env.UNIVERSAL_SERVER}}
e2e-recipes:
needs: [build-demo]
runs-on: ubuntu-latest
name: Recipes
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- uses: taiga-family/ci/actions/setup/cypress@v1.201.0
- name: Download demo build from cache
uses: actions/cache@v5
with:
path: dist/demo
key: ${{ env.CACHE_DIST_KEY }}
- name: Serving SSR server
run: |
npm run serve:ssr -- --ci & sleep 5
curl -X GET -I -f "${{ env.UNIVERSAL_SERVER }}"
- name: Run Cypress tests
run: npx nx e2e demo-integrations --spec="**/recipes/**/*.cy.ts" --baseUrl=${{ env.UNIVERSAL_SERVER}}
e2e-others:
needs: [build-demo]
runs-on: ubuntu-latest
name: Others
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- uses: taiga-family/ci/actions/setup/cypress@v1.201.0
- name: Download demo build from cache
uses: actions/cache@v5
with:
path: dist/demo
key: ${{ env.CACHE_DIST_KEY }}
- name: Serving SSR server
run: |
npm run serve:ssr -- --ci & sleep 5
curl -X GET -I -f "${{ env.UNIVERSAL_SERVER }}"
- name: Run Cypress tests
# Replace with npm run cy:run -- --spec "**/!(kit|recipes)/*.cy.ts" --config baseUrl="${{ env.UNIVERSAL_SERVER }}"
# After this issue fix: https://github.com/cypress-io/cypress/issues/22407
run:
npx nx e2e demo-integrations --spec="**/(angular|react|ssr|addons|others)/**/*.cy.ts" --baseUrl=${{
env.UNIVERSAL_SERVER}}
component-testing:
runs-on: ubuntu-latest
name: Component Testing
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- uses: taiga-family/ci/actions/setup/cypress@v1.201.0
- name: Run Cypress tests
run:
npx nx component-test demo-integrations --browser=chrome && npx nx ct-react demo-integrations --browser=chrome
result:
needs: [build-demo, e2e-kit, e2e-recipes, e2e-others, component-testing]
runs-on: ubuntu-latest
name: E2E result
steps:
- run: echo "Success"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npm run typecheck
cspell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npm run cspell -- --no-progress
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
persist-credentials: false
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npm run prettier ${{ env.SUPPORT_AUTO_PUSH == 'true' && '-- --write' || '-- --check' }}
- uses: taiga-family/ci/actions/auto/push@v1.201.0
with:
token: ${{ secrets.TAIGA_FAMILY_BOT_PAT }}
stylelint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
persist-credentials: false
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npm run stylelint ${{ env.SUPPORT_AUTO_PUSH == 'true' && '-- --fix' || '' }}
- uses: taiga-family/ci/actions/auto/push@v1.201.0
with:
token: ${{ secrets.TAIGA_FAMILY_BOT_PAT }}
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
with:
persist-credentials: false
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- run: npm run lint ${{ env.SUPPORT_AUTO_PUSH == 'true' && '-- --fix' || '' }}
- uses: taiga-family/ci/actions/auto/push@v1.201.0
with:
token: ${{ secrets.TAIGA_FAMILY_BOT_PAT }}
result:
needs: [typecheck, cspell, prettier, stylelint, eslint]
runs-on: ubuntu-latest
name: Lint result
steps:
- run: echo "Success"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/release.yml
================================================
name: ⚠️ Release
on:
workflow_dispatch:
inputs:
mode:
type: choice
description: Bump version as requested
required: true
options:
- patch
- minor
- alpha
- major
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- uses: taiga-family/ci/actions/run/release-it@v1.201.0
id: release-it
with:
ref: ${{ github.ref }}
mode: ${{ github.event.inputs.mode }}
npmToken: ${{ secrets.NPM_TOKEN }}
githubToken: ${{ secrets.TAIGA_FAMILY_BOT_PAT }}
- uses: taiga-family/ci/actions/run/read-package-json@v1.201.0
id: info
- name: Announce to Telegram
if: steps.release-it.outputs.released == 'true'
uses: taiga-family/ci/actions/messenger/telegram/announce@v1.201.0
with:
chatId: ${{ secrets.TAIGA_TELEGRAM_CHAT_ID }}
topicId: ${{ secrets.TAIGA_TELEGRAM_CHAT_THREAD_ID }}
token: ${{ secrets.TAIGA_TELEGRAM_BOT_TOKEN }}
version: v${{ steps.info.outputs.version }}
textLink: '@maskito/core'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .github/workflows/test.yml
================================================
name: Tests
on:
pull_request:
push:
branches:
- main
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: taiga-family/ci/actions/setup/variables@v1.201.0
- uses: taiga-family/ci/actions/setup/node@v1.201.0
- name: Run tests
run: npx nx run-many --target test --all --coverage
- name: Archive coverage artifacts
uses: actions/upload-artifact@v7.0.1
with:
name: coverage-${{ github.workflow }}-${{ github.run_id }}
path: coverage
codecov:
name: Collect coverage
needs: [tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/download-artifact@v8.0.1
with:
name: coverage-${{ github.workflow }}-${{ github.run_id }}
path: coverage
- name: Display structure of coverage files
run: tree -L 2 ./coverage -P 'lcov.info'
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/core/
flags: summary,core
name: core
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/kit/
flags: summary,kit
name: kit
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/phone/
flags: summary,phone
name: phone
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/angular/
flags: summary,angular
name: angular
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/react/
flags: summary,react
name: react
- uses: codecov/codecov-action@v6.0.0
with:
directory: ./coverage/vue/
flags: summary,vue
name: vue
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
================================================
FILE: .gitignore
================================================
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
**/node_modules/
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
*.iml
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
# cypress
**/cypress/**/screenshots/**
projects/demo-integrations/cypress
.nx
.ssl
RELEASE_BODY.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
.rollup.cache
================================================
FILE: .husky/commit-msg
================================================
npx --no -- commitlint --edit $1
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged
npm run typecheck
================================================
FILE: .npmrc
================================================
engine-strict=true
loglevel=error
================================================
FILE: .release-it.js
================================================
module.exports = require('@taiga-ui/release-it-config');
================================================
FILE: .ws-context
================================================
{
"framework": "angular",
"projects/vue/**": {
"framework": "vue"
},
"projects/react/**": {
"framework": "react"
}
}
================================================
FILE: CHANGELOG.md
================================================
### [5.2.2](https://github.com/taiga-family/maskito/compare/v5.2.1...v5.2.2) (2026-03-31)
### 🐞 Bug Fixes
- **kit**: `Number` with `negativePattern=minusFirst` + `prefix` has unexpected caret shift on minus insertion (#2616)
[(1d7bc77)](https://github.com/taiga-family/maskito/commit/1d7bc7761f1e4c7dad3b01b6c651295dd6d31bc4)
- **kit**: `Number` should ignore digits in affixes for `min`/`max` validation (#2615)
[(227a860)](https://github.com/taiga-family/maskito/commit/227a860453b4d0cee42a96e8ef06d155a44cce06)
### [5.2.1](https://github.com/taiga-family/maskito/compare/v5.2.0...v5.2.1) (2026-03-27)
### 🐞 Bug Fixes
- **react**: omitted `d.ts` files (#2607)
[(a10e118)](https://github.com/taiga-family/maskito/commit/a10e1187c81cfd4af8935edd7d459c3e6b5c6cc4)
- **kit**: `maskitoParseNumber` incorrectly parses postfix containing digits (#2600)
[(4805166)](https://github.com/taiga-family/maskito/commit/48051666d3235fe72c5bc90d83405175905ec7fa)
### [5.2.0](https://github.com/taiga-family/maskito/compare/v5.1.2...v5.2.0) (2026-03-25)
### 🚀 Features
- **kit**: `Number` supports `thousandSeparatorPattern` (#2584)
[(f4e1340)](https://github.com/taiga-family/maskito/commit/f4e1340ddedd8abf04c181c8db31edf420564845)
### 🐞 Bug Fixes
- **kit**: validate digit count in time segment for `maskitoParseDateTime` (#2581)
[(9c3e9bc)](https://github.com/taiga-family/maskito/commit/9c3e9bccc046be9e9a91ea05255bf55985144df1)
### [5.1.2](https://github.com/taiga-family/maskito/compare/v5.1.1...v5.1.2) (2026-03-13)
### 🐞 Bug Fixes
- **kit**: `maskitoParseDate` & `maskitoParseDateTime` supports parsing of dates in formats `mm/dd`, `dd/mm` (#2577)
[(a0f073b)](https://github.com/taiga-family/maskito/commit/a0f073b26b3be0d6bf776d970adc65c745cb3d41)
- **kit**: `maskitoParseDate` should return `null` for invalid `Date` string (#2561)
[(cf2b5f2)](https://github.com/taiga-family/maskito/commit/cf2b5f206a6679d2171bfd25fdc969ffee6dad7f)
### [5.1.1](https://github.com/taiga-family/maskito/compare/v5.1.0...v5.1.1) (2026-02-16)
### 🐞 Bug Fixes
- **kit**: `maskitoStringifyDate` incorrectly formats year with leading zeroes (#2538)
[(9817f08)](https://github.com/taiga-family/maskito/commit/9817f084dd34628afd1bebe8070189edf8f3fd1f)
- **kit**: `Number` deletes the previous non-selected character on the first deletion (#2537)
[(40ef2e0)](https://github.com/taiga-family/maskito/commit/40ef2e03b8ecaf564a1d4f055b8d629ba8932370)
- **phone**: `Phone` preserves previously entered digits on new digits paste (#2481)
[(f836f4f)](https://github.com/taiga-family/maskito/commit/f836f4f5ec94d865a2bcb44960161cfe25fe3a64)
## [5.1.0](https://github.com/taiga-family/maskito/compare/v5.0.1...v5.1.0) (2026-01-22)
### 🚀 Features
- **phone:** `Phone` supports national format ([#2461](https://github.com/taiga-family/maskito/issues/2461))
([c90bca2](https://github.com/taiga-family/maskito/commit/c90bca2c2f0f3bc3746a2559fc84bd688075c1be))
### 🐞Bug Fixes
- **kit:** `Number` fails to dynamically change postfix ([#2501](https://github.com/taiga-family/maskito/issues/2501))
([cd73d6a](https://github.com/taiga-family/maskito/commit/cd73d6a729068e4b301509b12def6e149a7b5d66))
- **kit:** `Number` throws `Failed to parse String to BigInt` error
([#2509](https://github.com/taiga-family/maskito/issues/2509))
([7b80f79](https://github.com/taiga-family/maskito/commit/7b80f79140740462d3d924ba49993e1376d2b6e3))
- **react:** `useMaskito` should destroy Maskito instance if element is detached from the DOM
([#2507](https://github.com/taiga-family/maskito/issues/2507))
([1cdb203](https://github.com/taiga-family/maskito/commit/1cdb20359aaf4fa479420638d76bc7b238a6cb4c))
### [5.0.1](https://github.com/taiga-family/maskito/compare/v5.0.0...v5.0.1) (2025-12-26)
### 🐞 Bug Fixes
- **phone**: `Phone` removes last digit if pasted without '+' (#2480)
[(5d709ca)](https://github.com/taiga-family/maskito/commit/5d709ca9c2e4a8abbe99a38c47551a9e6b2d8d36)
- **kit**: `maskitoStringifyNumber` supports extremal exponent values (#2463)
[(1340257)](https://github.com/taiga-family/maskito/commit/1340257486a86499f94c96dbe67949ada32ebae4)
- **phone**: `Phone` with initial value has problems with the first time delete action (#2455)
[(fa596fa)](https://github.com/taiga-family/maskito/commit/fa596fa101333b519bc3d760d6f4969151178e1a)
## [5.0.0](https://github.com/taiga-family/maskito/compare/v4.0.1...v5.0.0) (2025-12-03)
### ⚠ BREAKING CHANGES
- Bump Safari browser support (#2439)
| | < 5.0.0 | ≥ 5.0.0 |
| -------------- | ------- | ------- |
| Safari Desktop | 13.1+ | 14.1+ |
| Safari Mobile | 13.4+ | 14.5+ |
- **kit**: `Number` supports `BigInt` (#2431)
[(2d2f86d)](https://github.com/taiga-family/maskito/commit/2d2f86dafb1524528305908af27b4df37d9e1330)
New default values for `maskitoNumberOptionsGenerator` / `maskitoStringifyNumber`:
| MaskitoNumberParams | < 5.0.0 | ≥ 5.0.0 |
| ------------------- | ------------------------- | ----------- |
| `min` | `Number.MIN_SAFE_INTEGER` | `-Infinity` |
| `max` | `Number.MAX_SAFE_INTEGER` | `Infinity` |
### [4.0.1](https://github.com/taiga-family/maskito/compare/v4.0.0...v4.0.1) (2025-11-14)
### 🐞 Bug Fixes
- **kit**: `Time` with `[step]` ignores `timeSegmentMinValues` on arrow stepping (#2420)
[(398a5c1)](https://github.com/taiga-family/maskito/commit/398a5c163d03502d413ca15c8756f41891b75197)
- **kit**: `DateRange` with `[minLength]` & `[maxLength]` incorrectly appends month in backward direction (#2369)
[(3c80959)](https://github.com/taiga-family/maskito/commit/3c80959773a4c44b29e70a420bc1fc6e074030b9)
## [4.0.0](https://github.com/taiga-family/maskito/compare/v3.11.1...v4.0.0) (2025-10-13)
### ⚠ BREAKING CHANGES
- **kit**: delete deprecated `precision` & `decimalZeroPadding` parameters from `Number` mask (#2354)
**Previous behavior:**
```ts
import {maskitoNumberOptionsGenerator} from '@maskito/kit';
const options = maskitoNumberOptionsGenerator({
precision: 2, // ---> Use `maximumFractionDigits` instead
decimalZeroPadding: true, // ---> Use `minimumFractionDigits` instead
});
```
⬇️
**New behavior**:
```ts
import {maskitoNumberOptionsGenerator} from '@maskito/kit';
const options = maskitoNumberOptionsGenerator({
maximumFractionDigits: 2,
minimumFractionDigits: 2,
});
```
- **kit**: `maskitoParseNumber` accepts only `MaskitoNumberParams` as the 2nd argument (#2355)
**Previous behavior:**
```ts
import {maskitoParseNumber} from '@maskito/kit';
maskitoParseNumber(
'0,42',
',', // decimalSeparator
);
```
⬇️
**New behavior**:
```ts
import {maskitoParseNumber} from '@maskito/kit';
maskitoParseNumber(
'0,42',
{decimalSeparator: ','}, // MaskitoNumberParams
);
```
- **kit**: remove invalid `MM.SS.MSS` type from `MaskitoTimeMode` (use `MM:SS.MSS` instead) (#2365)
- **angular**: bump minimum required Angular version (16+ => 19+) (#2347) (#2348) (#2349)
- **angular**: `MaskitoDirective` uses model inputs (#2363)
### [3.11.1](https://github.com/taiga-family/maskito/compare/v3.11.0...v3.11.1) (2025-09-30)
### 🐞 Bug Fixes
- **kit**: resolve circular dependencies inside `Number` mask (#2344)
[(efb3039)](https://github.com/taiga-family/maskito/commit/efb303980905c33bfe58e0163b74c72da5f83fcd)
- **kit**: `Number` fails to clear initial value (by selecting all + Backspace/Delete) (#2343)
[(63f6e72)](https://github.com/taiga-family/maskito/commit/63f6e725af215dc492ddca02d30123de0dd026de)
- **kit**: `Number` has broken support for postfix with leading point (#2337)
[(e9a3598)](https://github.com/taiga-family/maskito/commit/e9a3598c9ce7f5c39f932d4e6b3c0ffabcde3741)
### [3.11.0](https://github.com/taiga-family/maskito/compare/v3.10.3...v3.11.0) (2025-09-23)
### 🚀 Features
- **kit**: `Number` supports minus before prefix (#2281)
[(480c1fd)](https://github.com/taiga-family/maskito/commit/480c1fde7693b62df768364c0df00fc7328cb4e6)
- **kit**: `Number` uses `toNumberParts` / `fromNumberParts` approach (#2270)
[(891780a)](https://github.com/taiga-family/maskito/commit/891780a8f179345a49dbe8b8036e639ae0a98cbd)
### 🐞 Bug Fixes
- **kit**: `PostfixPostprocessor` duplicates postfix on paste of value with incompleted postfix (#2267)
[(2707771)](https://github.com/taiga-family/maskito/commit/27077719ffc8628758664638e802e1ad3c9f8e27)
- **kit**: `maskitoStringifyTime` and `maskitoParseTime` should support `AM` / `PM` formats (#2260)
[(a0aea6f)](https://github.com/taiga-family/maskito/commit/a0aea6f741fea3139f4e7d7c8f84ce46c1738c26)
- **angular**: use `@Input` setters instead of `ngOnChanges` to handle programmatic changes (#2257)
[(cb8c129)](https://github.com/taiga-family/maskito/commit/cb8c129f1afd196a38f87dd4b36328ddea3b60a5)
### [3.10.3](https://github.com/taiga-family/maskito/compare/v3.10.2...v3.10.3) (2025-08-06)
### 🐞 Bug Fixes
- **kit**: `DateRange` + `minLength` / `maxLength` has incorrect limits (#2210)
[(e8917e0)](https://github.com/taiga-family/maskito/commit/e8917e0a124b26ffc9806f74c5c70016084f5280)
- **kit**: `maskitoStringifyNumber` fails to stringify number with exponential notation (#2224)
[(9fe0b08)](https://github.com/taiga-family/maskito/commit/9fe0b080b2703a72674a25b8ce352486cc274663)
### [3.10.2](https://github.com/taiga-family/maskito/compare/v3.10.1...v3.10.2) (2025-07-28)
### 🐞 Bug Fixes
- **kit**: `Number` with `input[maxlength]` is incompatible with `document.execCommand('delete')` (#2217)
[(2604d2c)](https://github.com/taiga-family/maskito/commit/2604d2ce8dc60e16f464a3fc4328f907bef58d55)
### [3.10.1](https://github.com/taiga-family/maskito/compare/v3.10.0...v3.10.1) (2025-07-18)
### 🐞 Bug Fixes
- **core**: dynamic mask switching to mask without fixed character fails on new character insertion (#2207)
[(50e68d4)](https://github.com/taiga-family/maskito/commit/50e68d4bb8f6e5330bda76c67806514a7fa53294)
### [3.10.0](https://github.com/taiga-family/maskito/compare/v3.9.1...v3.10.0) (2025-07-04)
### 🚀 Features
- **kit**: `Time` supports `prefix` & `postfix` parameters (#2185)
[(2cc7462)](https://github.com/taiga-family/maskito/commit/2cc7462583a2fe372d0cad312fb5f0d90ca0fe8e)
### 🐞 Bug Fixes
- **core**: invalid behavior of dynamic mask expression with trailing fixed characters (#2184)
[(cecf9d6)](https://github.com/taiga-family/maskito/commit/cecf9d69468e56de8ff4f39af7ebc07d5a686fe8)
- **core**: do not insert fixed character on attempt to enter invalid character at its position (#2181)
[(7a51702)](https://github.com/taiga-family/maskito/commit/7a51702361237a41cd9bbdcdbb8e46d0bfa2e4bc)
- **kit**: date-related mask with month-first mode has incorrect zero-padding logic (#2166)
[(26294e8)](https://github.com/taiga-family/maskito/commit/26294e8250591c727f99ccec563e8492df7c1068)
### [3.9.1](https://github.com/taiga-family/maskito/compare/v3.9.0...v3.9.1) (2025-06-23)
### 🐞 Bug Fixes
- **kit**: `Number` with custom `minusSign` has broken `min`/`max` behavior (#2164)
[(52ed25d)](https://github.com/taiga-family/maskito/commit/52ed25debaa2838a0b360983a508a3d627c78277)
- **core**: Android with Microsoft SwiftKey Keyboard ignores `preventDefault()` for `beforeinput` event on backspace
(#2163) [(722d9af)](https://github.com/taiga-family/maskito/commit/722d9afc265df8392613c770759df3bb8955e08e)
### [3.9.0](https://github.com/taiga-family/maskito/compare/v3.8.0...v3.9.0) (2025-06-05)
### 🚀 Features
- **react**: add support for React-specific `onChange` event handler (#2153)
[(e941847)](https://github.com/taiga-family/maskito/commit/e941847990662835343c4e25d3f2b2e64ab54345)
### 🐞 Bug Fixes
- **core**: do not unnecessarily trigger element's `value` setter on every keystroke (#2152)
[(fd3449b)](https://github.com/taiga-family/maskito/commit/fd3449b69f88dbcab5b06b03ff19273b511bcd64)
### [3.8.0](https://github.com/taiga-family/maskito/compare/v3.7.2...v3.8.0) (2025-05-13)
### 🚀 Features
- **angular**: new `MaskitoPattern` directive (#2081)
[(c3f7142)](https://github.com/taiga-family/maskito/commit/c3f7142245b603af9136541de9d181189e01a7a3)
### 🐞 Bug Fixes
- **kit**: update the first digit zero-padding logic for date-related mask (#2117)
[(b5b2598)](https://github.com/taiga-family/maskito/commit/b5b2598f455f3ad3438c3bd89b81009aca82f17c)
- **core**: incorrect handle of paste event for `<input />` with `maxlength` attribute (#2090)
[(e20e50b)](https://github.com/taiga-family/maskito/commit/e20e50bb92aca9d70bc483f9fc66904264a64c35)
- **kit**: `Number` should support non-erasable minus (as `prefix`) for `max <= 0` (#2087)
[(3910914)](https://github.com/taiga-family/maskito/commit/39109144075d58734d1545be888cbd03c5b6286e)
### [3.7.2](https://github.com/taiga-family/maskito/compare/v3.7.1...v3.7.2) (2025-04-22)
### 🐞 Bug Fixes
- **kit**: missing export of `maskitoParseDateTime` & `maskitoStringifyDateTime` utilities (#2074)
[(6aa34aa)](https://github.com/taiga-family/maskito/commit/6aa34aa610cf140248bc7a691beb5aaba1f0e0cd)
### [3.7.1](https://github.com/taiga-family/maskito/compare/v3.7.0...v3.7.1) (2025-04-16)
### 🐞 Bug Fixes
- **core**: updated selection range (even if textfield value is untouched) should not be ignored (#2069)
[(9276117)](https://github.com/taiga-family/maskito/commit/927611775e8d23eb89663150cd84e7981b12d2e7)
### [3.7.0](https://github.com/taiga-family/maskito/compare/v3.6.0...v3.7.0) (2025-04-15)
### 🚀 Features
- **kit**: new `maskitoParseDateTime` and `maskitoStringifyDateTime` helpers (#2055)
[(5028084)](https://github.com/taiga-family/maskito/commit/5028084f9be876cf8b1dc2607956cd4906285c43)
### 🐞 Bug Fixes
- **core**: add possibility to overwrites `selection` property in processors (#2053)
[(de354f4)](https://github.com/taiga-family/maskito/commit/de354f4fbeed7a632e23c0e1d00809effbb0229b)
### [3.6.0](https://github.com/taiga-family/maskito/compare/v3.5.0...v3.6.0) (2025-04-08)
### 🚀 Features
- **kit**: `Number` supports new properties `minimumFractionDigits` & `maximumFractionDigits` (#2022)
[(8719b9e)](https://github.com/taiga-family/maskito/commit/8719b9e30d6463deff4aed213cba774189ddd305)
### 🐞 Bug Fixes
- **core**: double space bar removes characters (#2040)
[(ccbebd8)](https://github.com/taiga-family/maskito/commit/ccbebd878ae7ba92da0a8d25d5b9d0b5c3ed3bcf)
### [3.5.0](https://github.com/taiga-family/maskito/compare/v3.4.0...v3.5.0) (2025-03-21)
### 🚀 Features
- **kit**: `Time` supports `MM:SS` mode (#2008)
[(b93ad1e)](https://github.com/taiga-family/maskito/commit/b93ad1ecc71f608dd68de01b43487153b8e89d95)
### 🐞 Bug Fixes
- **kit**: `maskitoParseDate` should return `null` for incompleted date string (#2009)
[(9eec35b)](https://github.com/taiga-family/maskito/commit/9eec35b878411a79fec84986cbea94fbdc9f24d8)
### [3.4.0](https://github.com/taiga-family/maskito/compare/v3.3.0...v3.4.0) (2025-03-10)
### 🚀 Features
- **kit**: new `maskitoStringifyNumber` helper (#1987)
[(cbfd4bc)](https://github.com/taiga-family/maskito/commit/cbfd4bc4bb6ca56bf12667bb3626c55ae1b04c48)
### 🐞 Bug Fixes
- **phone**: `Phone` should accept incomplete phone number of selected country (even with `strict=true`) (#1982)
[(965d735)](https://github.com/taiga-family/maskito/commit/965d7358ad39888d3844c121dd6934ee66cdc541)
### [3.3.0](https://github.com/taiga-family/maskito/compare/v3.2.1...v3.3.0) (2025-02-28)
### 🚀 Features
- **kit**: new `maskitoParseDate` and `maskitoStringifyDate` helpers (#1973)
[(208a4ab)](https://github.com/taiga-family/maskito/commit/208a4abc8018b368d3154ebc26a81504b6abec3d)
- **kit**: `Date` supports `dd/mm` and `mm/dd` modes (#1939)
[(bc290af)](https://github.com/taiga-family/maskito/commit/bc290affdcdc1cd6e088a32a60dc5e74fd00a1d8)
### 🐞 Bug Fixes
- **kit**: `SelectionChangeHandler` does not work for Safari after programmatic update of textfield value (#1930)
[(34c11d0)](https://github.com/taiga-family/maskito/commit/34c11d0ee88b861ab21d54113aff21f3091a053f)
### [3.2.1](https://github.com/taiga-family/maskito/compare/v3.2.0...v3.2.1) (2024-12-26)
### 🚀 Features
- **kit**: remove circular import (#1861)
[(15ff0b8)](https://github.com/taiga-family/maskito/commit/15ff0b8558bc954ac6eda07bdb13d087fc2f3491)
### 🐞 Bug Fixes
- **kit**: `Number` should ignore `[decimalSeparator]` value if `[precision]=0` (#1908)
[(19effe2)](https://github.com/taiga-family/maskito/commit/19effe2c7218646335b2f08c53a1ed3c3f0d89a1)
- **kit**: `Number` + postfix (with leading space) adds unnecessary spaces on paste value with trailing spaces (#1865)
[(c37b1d6)](https://github.com/taiga-family/maskito/commit/c37b1d636fefee1cba17b4aa07ccdd30edc5ff66)
- **kit**: `DateRange` should accept single character date segment paste even if date and range separators are equal
(#1796) [(be6a4c3)](https://github.com/taiga-family/maskito/commit/be6a4c3c57132cf320ec462372fd8536dca4781a)
### [3.2.0](https://github.com/taiga-family/maskito/compare/v3.1.2...v3.2.0) (2024-10-29)
### 🚀 Features
- **kit**: new `maskitoSelectionChangeHandler` plugin (#1794)
[(c6e9a4d)](https://github.com/taiga-family/maskito/commit/c6e9a4d9b1a2e75bc44aaecbda840b84f786d065)
### [3.1.2](https://github.com/taiga-family/maskito/compare/v3.1.1...v3.1.2) (2024-10-22)
### 🐞 Bug Fixes
- **kit**: `Time` & `DateTime` has conflicts between `step` & `AM/PM` features (#1791)
[(805f70b)](https://github.com/taiga-family/maskito/commit/805f70b74e04fb3b8613f89d84e771c734438dab)
- **kit**: `Number` incorrectly shift caret for 1st time insertion into textfield with initial value (#1792)
[(0049d91)](https://github.com/taiga-family/maskito/commit/0049d91a0a498977bb5f4cba9fbf9f02cb74dae9)
### [3.1.1](https://github.com/taiga-family/maskito/compare/v3.1.0...v3.1.1) (2024-10-17)
### 🐞 Bug Fixes
- **kit**: `Number` fails to prevent user insertion of extra spaces on invalid positions (#1789)
[(a40445c)](https://github.com/taiga-family/maskito/commit/a40445cf4d852328a9310a55cf38801e17525476)
- **kit**: `DateTime` fails to process value without any separators (paste from clipboard) (#1779)
[(1733422)](https://github.com/taiga-family/maskito/commit/1733422b803fda3de9b40a9fa675ef6bb8b5195e)
### [3.1.0](https://github.com/taiga-family/maskito/compare/v3.0.3...v3.1.0) (2024-10-09)
### 🚀 Features
- **kit**: `Time` & `DateTime` support `AM` / `PM` formats (#1708)
[(98ce35e)](https://github.com/taiga-family/maskito/commit/98ce35e8fd3318a750959d840f36caaf427fe8f0)
- **kit**: simplify some code logic for `Time` mask (#1688)
[(8c608b8)](https://github.com/taiga-family/maskito/commit/8c608b8cb5eaeca1166b78c6691d38303eb67c6c)
### 🐞 Bug Fixes
- **core**: `overwriteMode: replace` has incorrect behavior on attempt to insert invalid characters (#1772)
[(5aeb074)](https://github.com/taiga-family/maskito/commit/5aeb0741fa82ad6e43e862059a17b2e78ee9831b)
### [3.0.3](https://github.com/taiga-family/maskito/compare/v3.0.2...v3.0.3) (2024-09-25)
### 🐞 Bug Fixes
- **angular**: race condition when `[maskitoOptions]` are changed before long element predicate is resolved (#1696)
[(9f9bad3)](https://github.com/taiga-family/maskito/commit/9f9bad3036774fa51350c3c8402cf57f15e789d6)
- **kit**: `Time` has invalid segment separator for `MM:SS.MSS` mode (#1687)
[(93972be)](https://github.com/taiga-family/maskito/commit/93972be370e1abf4278497b11f61d3c923ae5caa)
- **core**: incorrect behavior of `overwriteMode = replace` if selection contains several characters (#1685)
[(67c3c10)](https://github.com/taiga-family/maskito/commit/67c3c10704f62efff4c47f1ad802859d54257752)
- **react**: race condition when `options` are changed before long element predicate is resolved (#1651)
[(f2932ce)](https://github.com/taiga-family/maskito/commit/f2932ce10ec80a1080befaee9e5c235bc41a1b16)
### [3.0.2](https://github.com/taiga-family/maskito/compare/v3.0.1...v3.0.2) (2024-09-20)
### 🐞 Bug Fixes
- **core:** `Time` with `[step]` has unexpected cursor jump to the next segment on `ArrowUp`/`ArrowDown`
([#1478](https://github.com/taiga-family/maskito/issues/1478))
([59a5927](https://github.com/taiga-family/maskito/commit/59a5927822e2c20691dc0948c438d67d497b6381))
- **core:** fix scroll for masked narrow textfields ([#1645](https://github.com/taiga-family/maskito/issues/1645))
([c6d2828](https://github.com/taiga-family/maskito/commit/c6d282873f10892ecb3536b878d919fc57f5c921))
### [3.0.1](https://github.com/taiga-family/maskito/compare/v3.0.0...v3.0.1) (2024-08-19)
### 🐞 Bug Fixes
- **kit:** `maskitoStringifyTime` was adding `0` on the wrong side
([#1401](https://github.com/taiga-family/maskito/issues/1401))
([b28ee12](https://github.com/taiga-family/maskito/commit/b28ee12f923b86eb3a8c32d17cd401e9222cfc30))
- **kit:** `Placeholder` should support partial programmatic removal of placeholder's characters
([#1441](https://github.com/taiga-family/maskito/issues/1441))
([146a557](https://github.com/taiga-family/maskito/commit/146a55723ec4a1ac8b9cfba254056b84173326c9))
- **kit:** `Time` incorrectly validates value if `timeSegmentMaxValues` includes single digit
([#1402](https://github.com/taiga-family/maskito/issues/1402))
([26670f4](https://github.com/taiga-family/maskito/commit/26670f4dbdfb84495ea0faa127868185d7bb0765))
## [3.0.0](https://github.com/taiga-family/maskito/compare/v2.5.0...v3.0.0) (2024-07-18)
### ⚠ BREAKING CHANGES
- **phone:** remove built-in `RemoveOnBlur` / `AddOnFocus` plugins from `@maskito/phone`
([#1352](https://github.com/taiga-family/maskito/issues/1352))
Learn more: https://maskito.dev/addons/phone#focus-blur
- **angular:** bump minimum required Angular version (15+ => 16+)
([#1328](https://github.com/taiga-family/maskito/issues/1328))
- **angular:** delete deprecated `MaskitoModule` & `MaskitoCVA`
([#1391](https://github.com/taiga-family/maskito/issues/1391))
### 🚀 Features
- **core:** new built-in `maskitoChangeEventPlugin` ([#1338](https://github.com/taiga-family/maskito/issues/1338))
Learn more: https://maskito.dev/core-concepts/plugins#change-event
## [2.5.0](https://github.com/taiga-family/maskito/compare/v2.4.0...v2.5.0) (2024-06-24)
### 🚀 Features
- **kit:** new `maskitoParseTime` and `maskitoStringifyTime` utils
([#1302](https://github.com/taiga-family/maskito/issues/1302))
([d0f9b13](https://github.com/taiga-family/maskito/commit/d0f9b1331f3bb18403691ac7c513c31f5123cf78))
### 🐞 Bug Fixes
- **core:** correct handling of browser autofill/suggestion in Firefox
([#1326](https://github.com/taiga-family/maskito/issues/1326))
([a049207](https://github.com/taiga-family/maskito/commit/a049207b355da72092948a8c556020062fb7c819))
- **kit:** `Date`, `DateRange`, `DateTime` supports multi-character date segments separator
([#1306](https://github.com/taiga-family/maskito/issues/1306))
([cdf2fae](https://github.com/taiga-family/maskito/commit/cdf2faee4c16cd3963557a511d4ec053e2d41fc0))
- **kit:** move caret after attempt to erase fixed character in a mask with `Placeholder`
([#1307](https://github.com/taiga-family/maskito/issues/1307))
([87ae431](https://github.com/taiga-family/maskito/commit/87ae431ded798e3c31d6247f965a00c27ddad3f1))
## [2.4.0](https://github.com/taiga-family/maskito/compare/v2.3.2...v2.4.0) (2024-06-03)
### 🚀 Features
- **kit:** `Time` & `DateTime` support increment / decrement of time segment via `ArrowUp` / `ArrowDown`
([#1223](https://github.com/taiga-family/maskito/issues/1223))
([af961b8](https://github.com/taiga-family/maskito/commit/af961b84f8765e7d2147c80210e3a8ac6ed30597))
- **kit:** `Time` supports `SS.MSS` & `MM.SS.MSS` modes ([#1224](https://github.com/taiga-family/maskito/issues/1224))
([7bed4bc](https://github.com/taiga-family/maskito/commit/7bed4bcaac14908e7e445b277f5b4b6e5b0fd281))
### 🐞 Bug Fixes
- **core:** add `.select()`-method support for `MaskitoElement`
([#1268](https://github.com/taiga-family/maskito/issues/1268))
([51f5934](https://github.com/taiga-family/maskito/commit/51f5934f382b7862a6653412b687c46fd318b0bb))
- **kit:** `Number` should support float `min`/`max`-parameters in range -1 < x < 1
([#1280](https://github.com/taiga-family/maskito/issues/1280))
([b44013e](https://github.com/taiga-family/maskito/commit/b44013e0a45ffcfa69564f13d634a79d45b4d926))
### [2.3.2](https://github.com/taiga-family/maskito/compare/v2.3.1...v2.3.2) (2024-05-16)
### 🐞 Bug Fixes
- **kit:** `Number` pads integer part with zero if user selects all and then types decimal separator
([#1220](https://github.com/taiga-family/maskito/issues/1220))
([8371e45](https://github.com/taiga-family/maskito/commit/8371e45767150ebc4db03a2b74c68afd6fe1e593))
- **phone:** revert mistakenly fixated `libphonenumber-js` peer-dependency to just `>=1.0.0`
([#1234](https://github.com/taiga-family/maskito/issues/1234))
([27ee4a1](https://github.com/taiga-family/maskito/commit/27ee4a1264c0a70a5a06427368b8d18ed0e25bd4))
- **react:** revert mistakenly fixated `react` & `react-demo` peer-dependencies to just `>=16.8`
([#1231](https://github.com/taiga-family/maskito/issues/1231))
([ae89d6f](https://github.com/taiga-family/maskito/commit/ae89d6ff549dfb21d7db56b26e3c1f3a7044a817))
- **vue:** revert mistakenly fixated `vue` peer-dependency to just `>=3.0.0`
([#1232](https://github.com/taiga-family/maskito/issues/1232))
([22d84e2](https://github.com/taiga-family/maskito/commit/22d84e2f731ae8798f457466be7c9538d2f40fd9))
### [2.3.1](https://github.com/taiga-family/maskito/compare/v2.3.0...v2.3.1) (2024-04-23)
### 🐞 Bug Fixes
- **kit:** `Number` should drop decimal separator if all digits are erased
([#1211](https://github.com/taiga-family/maskito/issues/1211))
([5836c96](https://github.com/taiga-family/maskito/commit/5836c965d6ce5ad497aaa59118204adc3e8625d8))
- **kit:** `Number` with `decimalZeroPadding=true` should erase everything on `.00`
([#1207](https://github.com/taiga-family/maskito/issues/1207))
([d72f225](https://github.com/taiga-family/maskito/commit/d72f2257cec1a023aa81bb7de62e9543404630bd))
- **kit:** `Placeholder` can have now the same character as textfield's value
([#1209](https://github.com/taiga-family/maskito/issues/1209))
([ed06936](https://github.com/taiga-family/maskito/commit/ed06936c41297cbd2e8ed308558914e9ad6c2eda))
## [2.3.0](https://github.com/taiga-family/maskito/compare/v2.2.0...v2.3.0) (2024-04-16)
### 🚀 Features
- **core:** add `contenteditable` support ([#1039](https://github.com/taiga-family/maskito/issues/1039))
([0d5bb31](https://github.com/taiga-family/maskito/commit/0d5bb319225fb61f3ac7643c21208122b4a2a2ae))
- **kit:** `DateTime` supports configurable parameter `dateTimeSeparator`
([#1143](https://github.com/taiga-family/maskito/issues/1143))
([ec86284](https://github.com/taiga-family/maskito/commit/ec8628467814cff7dfae22668370236f402d8146))
### 🐞 Bug Fixes
- **kit:** `Date` formatting errors for `mm/yyyy`, `yyyy/mm`, `mm/yy` modes
([#1177](https://github.com/taiga-family/maskito/issues/1177))
([948a350](https://github.com/taiga-family/maskito/commit/948a35098da2233bc78793eb7e83b7c5136becbd))
## [2.2.0](https://github.com/taiga-family/maskito/compare/v2.1.0...v2.2.0) (2024-03-07)
### 🚀 Features
- **kit:** `Number` supports new configurable parameter `minusSign`
([#1118](https://github.com/taiga-family/maskito/issues/1118))
([a7bec35](https://github.com/taiga-family/maskito/commit/a7bec35f19d7dfa4023ad83fa36a935b2d636fc7))
### 🐞 Bug Fixes
- totally disable `Maskito` if nullable options are passed inside `@maskito/{angular,react,vue}`
([#1117](https://github.com/taiga-family/maskito/issues/1117))
([8cbadcf](https://github.com/taiga-family/maskito/commit/8cbadcfdf9af283dc687b131361f7bb19a7f9b02))
## [2.1.0](https://github.com/taiga-family/maskito/compare/v2.0.2...v2.1.0) (2024-03-04)
### 🚀 Features
- **kit:** `Date` & `DateRange` & `DateTime` has improved zero-padding support for browser autofill & IME composition
([#1027](https://github.com/taiga-family/maskito/issues/1027))
([77ac01c](https://github.com/taiga-family/maskito/commit/77ac01ca0b5e61d36dc3240a35c3dc93ce5fe93c))
- **kit:** add full-width numbers support for `Time`, `Date`, `DateTime`, `DateRange`
([#1043](https://github.com/taiga-family/maskito/issues/1043))
([434c9c5](https://github.com/taiga-family/maskito/commit/434c9c5f349ab3c19e11722e95313c5763203b08))
### 🐞 Bug Fixes
- **kit:** `maskitoParseNumber` should interpret japanese prolonged sound mark as pseudo minus
([#1115](https://github.com/taiga-family/maskito/issues/1115))
([b152698](https://github.com/taiga-family/maskito/commit/b152698fda8ac671286eb5f4a29de62562934fa2))
### [2.0.2](https://github.com/taiga-family/maskito/compare/v2.0.1...v2.0.2) (2024-02-01)
### 🐞 Bug Fixes
- **kit:** `Number` with initial value has problems with the first time input
([#986](https://github.com/taiga-family/maskito/issues/986))
([e40d3ff](https://github.com/taiga-family/maskito/commit/e40d3ff93c668c8afa60cd347faa7ebec76d0e6a))
- **react:** `@maskito/react` includes again missing `cjs` module format
([#991](https://github.com/taiga-family/maskito/issues/991))
([18e3e0c](https://github.com/taiga-family/maskito/commit/18e3e0cf8911fa764a73e2e937081186f1dcde79))
### [2.0.1](https://github.com/taiga-family/maskito/compare/v2.0.0...v2.0.1) (2024-01-31)
### 🐞 Bug Fixes
- **core:** `maskitoUpdateElement` should not dispatch `InputEvent` if value is not changed
([#977](https://github.com/taiga-family/maskito/issues/977))
([2410b64](https://github.com/taiga-family/maskito/commit/2410b6478c88f4d530b4469d7d50b1e4663d1572))
- **core:** don't execute `setSelectionRange` if element is not focused
([#937](https://github.com/taiga-family/maskito/issues/937))
([92f288b](https://github.com/taiga-family/maskito/commit/92f288b677dbe77f7978308dd7b1612d6bfd68fb))
- **kit:** `Number` rejects the first time input of full width digits
([#955](https://github.com/taiga-family/maskito/issues/955))
([c416884](https://github.com/taiga-family/maskito/commit/c41688488630e83d69eba795580916145e5fe17c))
- **react:** `@maskito/react` library should not include `core-js` imports
([#962](https://github.com/taiga-family/maskito/issues/962))
([3b7e401](https://github.com/taiga-family/maskito/commit/3b7e4014029fae206020723c18762f08e92b8c41))
## [2.0.0](https://github.com/taiga-family/maskito/compare/v1.9.0...v2.0.0) (2024-01-22)
### ⚠ BREAKING CHANGES
- **core:** merge `MaskitoElementPredicate` & `MaskitoElementPredicateAsync` into single type
([#757](https://github.com/taiga-family/maskito/issues/757))
- **core:** remove value's calibration on initialization + new `maskitoInitialCalibrationPlugin`
([#778](https://github.com/taiga-family/maskito/issues/778))
- **core:** bump Firefox browser support (55+ => 87+) ([#876](https://github.com/taiga-family/maskito/issues/876)) and
drop legacy fallbacks for `Firefox` ([#756](https://github.com/taiga-family/maskito/issues/756))
- **kit:** delete deprecated `separator` for `DateRange` (use `dateSeparator` instead)
([#790](https://github.com/taiga-family/maskito/issues/790))
- **angular:** bump minimum required Angular version (12+ => 15+)
([#710](https://github.com/taiga-family/maskito/issues/710))
([#720](https://github.com/taiga-family/maskito/issues/720))
([#725](https://github.com/taiga-family/maskito/issues/725))
- **angular:** deprecate `MaskitoModule` (use standalone `MaskitoDirective`, `MaskitoCVA`, `MaskitoPipe`)
([#754](https://github.com/taiga-family/maskito/issues/754))
### 🚀 More features
- **core:** new built-in `maskitoStrictCompositionPlugin` ([#881](https://github.com/taiga-family/maskito/issues/881))
- **kit:** `Number` allows to enter full width numbers ([#864](https://github.com/taiga-family/maskito/issues/864))
### 🐞 Bug Fixes
- **core:** drop some excess dispatches of `Input`-event ([#882](https://github.com/taiga-family/maskito/issues/882))
- **kit:** add `{bubbles:true}` for `input` events inside all built-in plugins to support `ReactSyntheticEvent`
([#806](https://github.com/taiga-family/maskito/issues/806))
- **kit:** `Number` has problems when prefix/postfix includes `decimalSeparator` symbol
([#874](https://github.com/taiga-family/maskito/issues/874))
([#816](https://github.com/taiga-family/maskito/issues/816))
([#921](https://github.com/taiga-family/maskito/issues/921))
- **kit:** `Placeholder` is not compatible with `maskitoEventHandler` + `focus`/`blur` events
([#928](https://github.com/taiga-family/maskito/pull/928))
## [1.9.0](https://github.com/taiga-family/maskito/compare/v1.8.2...v1.9.0) (2023-11-23)
### 🚀 Features
- **phone:** add ability to configure the separator ([#685](https://github.com/taiga-family/maskito/issues/685))
([ab6bb11](https://github.com/taiga-family/maskito/commit/ab6bb11b1b40e069d31598b676c04456329aaf64))
### [1.8.2](https://github.com/taiga-family/maskito/compare/v1.8.1...v1.8.2) (2023-11-16)
### 🐞 Bug Fixes
- **kit:** `PrefixPostprocessor` has problems with multi-character prefix
([#669](https://github.com/taiga-family/maskito/issues/669))
([be459e5](https://github.com/taiga-family/maskito/commit/be459e51f3cbf028fa36b1b6a57e47d7fe8482a3))
### [1.8.1](https://github.com/taiga-family/maskito/compare/v1.8.0...v1.8.1) (2023-10-19)
### 🐞 Bug Fixes
- **kit:** `Date` accept single character date segment during paste
([#610](https://github.com/taiga-family/maskito/issues/610))
([e493198](https://github.com/taiga-family/maskito/commit/e4931987c2fad37894ea07f658f08e35152040df))
## [1.8.0](https://github.com/taiga-family/maskito/compare/v1.7.0...v1.8.0) (2023-10-18)
### 🚀 Features
- **angular:** allow nullable options ([#605](https://github.com/taiga-family/maskito/issues/605))
([21eaa7c](https://github.com/taiga-family/maskito/commit/21eaa7c0c0e7d5173c6f070f5222ba6492e196a6))
### 🐞 Bug Fixes
- **kit:** `Number` has broken zero padding when `decimalSeparator` equals to non-default value
([#586](https://github.com/taiga-family/maskito/issues/586))
([7241761](https://github.com/taiga-family/maskito/commit/72417614dd4974c22854dfacc2ee35044c080074))
## [1.7.0](https://github.com/taiga-family/maskito/compare/v1.6.0...v1.7.0) (2023-09-15)
### 🚀 Features
New `@maskito/phone` library ([#425](https://github.com/taiga-family/maskito/pull/425))
([#482](https://github.com/taiga-family/maskito/issues/482))
Learn more: https://maskito.dev/addons/phone
## [1.6.0](https://github.com/taiga-family/maskito/compare/v1.5.1...v1.6.0) (2023-09-15)
### 🚀 Features
- **react:** `elementPredicate` can accept asynchronous predicate
([#502](https://github.com/taiga-family/maskito/issues/502))
([4bbf758](https://github.com/taiga-family/maskito/commit/4bbf758107ed4b2fdbde5a241f22c0f363c22104))
### 🐞 Bug Fixes
- **kit:** `Number` should accept all types of spaces as interchangeable characters for `thousandSeparator`
([#505](https://github.com/taiga-family/maskito/issues/505))
([73975bb](https://github.com/taiga-family/maskito/commit/73975bbc676487330359056c367f73e32ea6eaf4))
### [1.5.1](https://github.com/taiga-family/maskito/compare/v1.5.0...v1.5.1) (2023-09-08)
### 🐞 Bug Fixes
- **vue:** `elementPredicate` should accept `MaskitoElementPredicateAsync` type
([#487](https://github.com/taiga-family/maskito/issues/487))
([fe7e9dc](https://github.com/taiga-family/maskito/commit/fe7e9dcb468bf3ab30978c947d8fa21cc0e51a75))
## [1.5.0](https://github.com/taiga-family/maskito/compare/v1.4.0...v1.5.0) (2023-09-04)
### 🚀 Features
- **core:** add IME composition support ([#467](https://github.com/taiga-family/maskito/issues/467))
([e7d664b](https://github.com/taiga-family/maskito/commit/e7d664b66a008a742c0a532e341b0e0bb0a0f759))
- **demo:** documentation is now available at https://maskito.dev
([#392](https://github.com/taiga-family/maskito/issues/392))
([355f87f](https://github.com/taiga-family/maskito/commit/355f87fd536758bc2db59f760ed114d28264122a))
### 🐞 Bug Fixes
- **kit:** `maskitoCaretGuard` doesn't work after focus on ` `
([#462](https://github.com/taiga-family/maskito/issues/462))
([9f456da](https://github.com/taiga-family/maskito/commit/9f456dad7f7f9d02db4eb5993ecb5fb5aabfe613))
- **kit:** `Number` should drop items from `decimalPseudoSeparators` if any is equal to `thousandSeparator`
([#390](https://github.com/taiga-family/maskito/issues/390))
([2107adc](https://github.com/taiga-family/maskito/commit/2107adc445ed26ce1507c5e0c534b668d7ae5b12))
## [1.4.0](https://github.com/taiga-family/maskito/compare/v1.3.0...v1.4.0) (2023-07-27)
### 🚀 Features
- **kit:** `Date` & `DateRange` support new modes `yyyy`, `mm/yyyy`, `yyyy/mm`
([#384](https://github.com/taiga-family/maskito/issues/384))
([7886d50](https://github.com/taiga-family/maskito/commit/7886d50012a76fec872816b6d5e2b7e67c931dd7))
- **kit:** `Time` supports new mode `HH` ([#385](https://github.com/taiga-family/maskito/issues/385))
([3c7a3f6](https://github.com/taiga-family/maskito/commit/3c7a3f65a0013152473ba57af8da28012cb58f32))
## [1.3.0](https://github.com/taiga-family/maskito/compare/v1.2.2...v1.3.0) (2023-07-24)
### 🚀 Features
- **kit:** `DateRange` add configurable parameter `rangeSeparator`
([#376](https://github.com/taiga-family/maskito/issues/376))
([d904842](https://github.com/taiga-family/maskito/commit/d90484214da76f4c73ad925eef5fe391a154c499))
### 🐞 Bug Fixes
- **kit:** `Number` has problems with run-time updates of postfix
([#380](https://github.com/taiga-family/maskito/issues/380))
([8210896](https://github.com/taiga-family/maskito/commit/8210896d2095a44e79a27a38e4c8745e2beccdb7))
### [1.2.2](https://github.com/taiga-family/maskito/compare/v1.2.1...v1.2.2) (2023-07-19)
### 🐞 Bug Fixes
- **kit:** `maskitoCaretGuard` should wait for `mouseup` before execution
([#372](https://github.com/taiga-family/maskito/issues/372))
([8554fea](https://github.com/taiga-family/maskito/commit/8554fead2a2474104f0674fb597cf86467274943))
- **kit:** `Number` should remove repeated leading zeroes for integer part only on `blur`-event
([#373](https://github.com/taiga-family/maskito/issues/373))
([7cf4938](https://github.com/taiga-family/maskito/commit/7cf4938853ccbd049b89482f8eb22ab4e71fe01f))
### [1.2.1](https://github.com/taiga-family/maskito/compare/v1.2.0...v1.2.1) (2023-07-11)
### 🐞 Bug Fixes
- **kit:** `Number` with `postfix` should be compatible with `decimalZeroPadding`
([#364](https://github.com/taiga-family/maskito/issues/364))
([501cf9c](https://github.com/taiga-family/maskito/commit/501cf9c747229d1776fb62cc04fbc8879990c617))
- **kit:** `Prefix`/`Postfix` is incompatible if they end/start with the same character
([#366](https://github.com/taiga-family/maskito/issues/366))
([06afbcb](https://github.com/taiga-family/maskito/commit/06afbcb4a2c5c15e2ef9dc81db4309adf01aa8ef))
## [1.2.0](https://github.com/taiga-family/maskito/compare/v1.1.1...v1.2.0) (2023-07-03)
### 🚀 Features
- **kit:** `maskitoCaretGuard`'s function has the 2nd argument with current selection range
([#358](https://github.com/taiga-family/maskito/issues/358))
([eedc4d6](https://github.com/taiga-family/maskito/commit/eedc4d610efaf36b98a4049f5c5334561b5b21c5))
### 🐞 Bug Fixes
- **kit:** `maskitoCaretGuard` incorrectly validates the left side of constraints
([#356](https://github.com/taiga-family/maskito/issues/356))
([17ee90f](https://github.com/taiga-family/maskito/commit/17ee90fe055f6a8370d6ea75ff2b236fd498441e))
- **kit:** `Number` should skip min/max validation if value does not contain any digits
([#359](https://github.com/taiga-family/maskito/issues/359))
([ed8221e](https://github.com/taiga-family/maskito/commit/ed8221e14eca62334af41b4c8e571eb86ed68247))
### [1.1.1](https://github.com/taiga-family/maskito/compare/v1.1.0...v1.1.1) (2023-06-29)
### 🐞 Bug Fixes
- **core:** don't ignore native attribute `maxlength` ([#350](https://github.com/taiga-family/maskito/issues/350))
([8504f49](https://github.com/taiga-family/maskito/commit/8504f497152931da06dd745763be2505587f97b4))
- **kit:** `Number` should ignore new typed decimal separator if it already exists in text field
([#351](https://github.com/taiga-family/maskito/issues/351))
([4ccfdc8](https://github.com/taiga-family/maskito/commit/4ccfdc86ff08bcebfd18c04403aa9c9c83cbbd02))
## [1.1.0](https://github.com/taiga-family/maskito/compare/v1.0.0...v1.1.0) (2023-06-23)
### 🚀 Features
- **kit:** `maskitoEventHandler` accepts `AddEventListenerOptions` as the 3d optional argument
([#346](https://github.com/taiga-family/maskito/issues/346))
([1d5866e](https://github.com/taiga-family/maskito/commit/1d5866efa5e0e4736dd735ae006e027e9bd01e31))
- **kit:** use capturing phase for `focus`/`blur` events in plugins
([#347](https://github.com/taiga-family/maskito/issues/347))
([ef539e1](https://github.com/taiga-family/maskito/commit/ef539e160f601023e513036d704f7daff9689286))
### 🐞 Bug Fixes
- **kit:** `Date` allows to replace the whole selection range with zero
([#345](https://github.com/taiga-family/maskito/issues/345))
([98fd21d](https://github.com/taiga-family/maskito/commit/98fd21d50899db365b864faf597fad9a21a3db06))
## [1.0.0](https://github.com/taiga-family/maskito/compare/v0.16.0...v1.0.0) (2023-06-21)
### ⚠ BREAKING CHANGES
- **core:** delete deprecated `preprocessor` & `postprocessor` from `MaskitoOptions`
([#337](https://github.com/taiga-family/maskito/issues/337))
([0b6aad2](https://github.com/taiga-family/maskito/commit/0b6aad2622ed152d12c91f8ca64b767709ecdbc2))
- **kit:** delete deprecated `isNegativeAllowed` parameter from `Number` mask
([#338](https://github.com/taiga-family/maskito/issues/338))
([9fd3005](https://github.com/taiga-family/maskito/commit/9fd30055b3157072076f7a8567045fac05b6af9e))
## [0.16.0](https://github.com/taiga-family/maskito/compare/v0.15.0...v0.16.0) (2023-06-20)
### 🚀 Features
- **vue:** support async predicate ([#336](https://github.com/taiga-family/maskito/issues/336))
([d1452b5](https://github.com/taiga-family/maskito/commit/d1452b5f1b2f8a252dfd05a5c1eb04ba971a1970))
### 🐞 Bug Fixes
- **kit:** `Number` fails to parse small number on blur (exponential notation problem)
([#339](https://github.com/taiga-family/maskito/issues/339))
([7f83a7f](https://github.com/taiga-family/maskito/commit/7f83a7f170906c1911eb4444da2d636c0338ed4a))
## [0.15.0](https://github.com/taiga-family/maskito/compare/v0.14.0...v0.15.0) (2023-06-14)
### 🚀 Features
- **core:** add new parameters `preprocessors` & `postprocessors` and deprecate `preprocessor` & `postprocessor`
([#333](https://github.com/taiga-family/maskito/issues/333))
([0137775](https://github.com/taiga-family/maskito/commit/01377751a9875143257930934b1e2a9143b6da03))
### 🐞 Bug Fixes
- **kit:** `maskitoParseNumber` should return `NaN` for all strings with no digits
([#331](https://github.com/taiga-family/maskito/issues/331))
([d1ebcec](https://github.com/taiga-family/maskito/commit/d1ebceceedf418b21a68082f7350002d09159ebf))
- **kit:** `Number` incorrectly implements `min`/`max` behaviour
([#334](https://github.com/taiga-family/maskito/issues/334))
([9876d88](https://github.com/taiga-family/maskito/commit/9876d885f98f86d18db04d723460b468bca3837d))
## [0.14.0](https://github.com/taiga-family/maskito/compare/v0.13.0...v0.14.0) (2023-06-09)
### 🚀 Features
- **angular:** `[maskitoElement]` can accept asynchronous predicate
([#316](https://github.com/taiga-family/maskito/issues/316))
([3d8949e](https://github.com/taiga-family/maskito/commit/3d8949e878e644079b7f5404cb9ebf6c5eadab86))
- **kit:** `Number` pads empty integer part with zero on blur (if decimal part exists)
([#328](https://github.com/taiga-family/maskito/issues/328))
([bd01967](https://github.com/taiga-family/maskito/commit/bd01967fba38be26a3c8f0d2f23c0ced12d3b1c2))
## [0.13.0](https://github.com/taiga-family/maskito/compare/v0.12.1...v0.13.0) (2023-06-02)
### 🚀 Features
- **core:** better layout-independent way to detect `Undo` and `Redo`
([#320](https://github.com/taiga-family/maskito/issues/320))
([4c5a7f6](https://github.com/taiga-family/maskito/commit/4c5a7f64b9a8ac209584c75e17ec022674b87c1b))
- **vue:** add dedicated Vue package ([#321](https://github.com/taiga-family/maskito/issues/321))
([f6ffb24](https://github.com/taiga-family/maskito/commit/f6ffb24eca5f1a1a57a93103b9e74cdf410e4132))
### [0.12.1](https://github.com/taiga-family/maskito/compare/v0.12.0...v0.12.1) (2023-05-25)
### 🐞 Bug Fixes
- **kit:** `DateTime` validate min / max if date is complete
([#314](https://github.com/taiga-family/maskito/issues/314))
([5783e76](https://github.com/taiga-family/maskito/commit/5783e766a657abcf0fc7f8a8d12ac1bf412dc18a))
- **kit:** `Time` & `DateTime` should accept time segment separator typed by user
([#317](https://github.com/taiga-family/maskito/issues/317))
([3bcac7f](https://github.com/taiga-family/maskito/commit/3bcac7f6566043991a9211f04db744a5ec6f019f))
## [0.12.0](https://github.com/taiga-family/maskito/compare/v0.11.1...v0.12.0) (2023-05-19)
### 🚀 Features
- **core:** add `plugins` to `MaskitoOptions` ([#305](https://github.com/taiga-family/maskito/issues/305))
([b512ae2](https://github.com/taiga-family/maskito/commit/b512ae2c64b2a2c6560e2e5c68d8c72952474c71))
- **core:** expose `MaskitoMask`, `MaskitoPreprocessor`, `MaskitoPostprocessor` and `MaskitoPlugin`
([#307](https://github.com/taiga-family/maskito/issues/307))
([9315a9f](https://github.com/taiga-family/maskito/commit/9315a9f4620b3be86cf3b7af993861664f281a19))
- **kit:** new `maskitoWithPlaceholder` utility ([#299](https://github.com/taiga-family/maskito/issues/299))
([21eb69c](https://github.com/taiga-family/maskito/commit/21eb69cfeb73bbe645d5a5879659ab8b6aadbf0c))
### [0.11.1](https://github.com/taiga-family/maskito/compare/v0.11.0...v0.11.1) (2023-05-11)
### 🐞 Bug Fixes
- **core:** `insertFromDrop` action behaves now in the same way as `insertFromPaste`
([#291](https://github.com/taiga-family/maskito/issues/291))
([58e0fcc](https://github.com/taiga-family/maskito/commit/58e0fccb7ddd3c741ffa3c8b99efbcf4571aab37))
- **kit:** `Time` doesn't validate time segments on `drop` event
([#289](https://github.com/taiga-family/maskito/issues/289))
([0c6d1b9](https://github.com/taiga-family/maskito/commit/0c6d1b9917d0c86a98c0d215c38a0e2076ff5680))
## [0.11.0](https://github.com/taiga-family/maskito/compare/v0.10.0...v0.11.0) (2023-05-02)
### 🚀 Features
- **react:** new library `@maskito/react` ([#273](https://github.com/taiga-family/maskito/issues/273))
([4c2f755](https://github.com/taiga-family/maskito/commit/4c2f755bac9513689964af7fdb7f4deec56bfb52))
## [0.10.0](https://github.com/taiga-family/maskito/compare/v0.9.0...v0.10.0) (2023-04-25)
### 🚀 Features
- **kit:** `Number` keeps untouched decimal part if `precision: Infinity`
([#253](https://github.com/taiga-family/maskito/issues/253))
([261779e](https://github.com/taiga-family/maskito/commit/261779ead327397a61b27e634bc827ee70b718f4))
- **kit:** `Number` supports new `prefix` & `postfix` parameters
([#264](https://github.com/taiga-family/maskito/issues/264))
([6e78581](https://github.com/taiga-family/maskito/commit/6e785818dabcde623d8c1c40a584166a0a66f5b6))
- **kit:** new `maskitoPostfixPostprocessorGenerator` ([#257](https://github.com/taiga-family/maskito/issues/257))
([fdc86db](https://github.com/taiga-family/maskito/commit/fdc86dbad368bfc17efd1047b7d68d9622968bb0))
### 🐞 Bug Fixes
- **kit:** `Number` fails to trim leading zeroes after deleting of leading digit
([#268](https://github.com/taiga-family/maskito/issues/268))
([4ae0010](https://github.com/taiga-family/maskito/commit/4ae0010ef2149694d22d7ae9eb8c9880120c8c75))
- **kit:** `Number` should trim redundant thousand separators
([#267](https://github.com/taiga-family/maskito/issues/267))
([100b793](https://github.com/taiga-family/maskito/commit/100b79317a420103ca98a3b43fe646a6f77d19d5))
- **kit:** fix first zero in short-format date-mask ([#251](https://github.com/taiga-family/maskito/issues/251))
([08bdfd2](https://github.com/taiga-family/maskito/commit/08bdfd26727777c3a6fc870e433003be2b64cc0e))
## [0.9.0](https://github.com/taiga-family/maskito/compare/v0.8.1...v0.9.0) (2023-03-31)
### 🚀 Features
- **kit:** new `maskitoPrefixPostprocessorGenerator` ([#235](https://github.com/taiga-family/maskito/issues/235))
([50f0d58](https://github.com/taiga-family/maskito/commit/50f0d58ccbfa22d15174d76479a9d642687db099))
### 🐞 Bug Fixes
- **angular:** Jest throws `Class constructor DefaultValueAccessor cannot be invoked without 'new'`
([#232](https://github.com/taiga-family/maskito/issues/232))
([5089612](https://github.com/taiga-family/maskito/commit/508961288898a5fdd21cc0e26b23ecc8845f9068))
### [0.8.1](https://github.com/taiga-family/maskito/compare/v0.8.0...v0.8.1) (2023-03-27)
### 🐞 Bug Fixes
- `@maskito/core` & `@maskito/kit` now include both `UMD` and `ESM` module formats
([#227](https://github.com/taiga-family/maskito/issues/227))
([fa1c514](https://github.com/taiga-family/maskito/commit/fa1c514a5753e3bca20e8b0994e4bf9f1c0ab6a4))
## [0.8.0](https://github.com/taiga-family/maskito/compare/v0.7.2...v0.8.0) (2023-03-23)
### 🚀 Features
- **kit:** `DateRange` swaps dates if the 2nd date is less than the 1st one
([#212](https://github.com/taiga-family/maskito/issues/212))
([3efbb42](https://github.com/taiga-family/maskito/commit/3efbb42f2dd5c4e43ff514da7a82abfc7c4b3a38))
### 🐞 Bug Fixes
- **core:** incorrect order of actions during update of native element
([#225](https://github.com/taiga-family/maskito/issues/225))
([394d5d9](https://github.com/taiga-family/maskito/commit/394d5d996bdb9d21229ea0301eb3f776bee05d30))
### [0.7.2](https://github.com/taiga-family/maskito/compare/v0.7.1...v0.7.2) (2023-03-23)
### 🐞 Bug Fixes
- **angular:** `@maskito/angular` should not depend on `@maskito/kit`
([#221](https://github.com/taiga-family/maskito/issues/221))
([0ae7b20](https://github.com/taiga-family/maskito/commit/0ae7b2089ec0436caa8dbb14d5c696ae93e9e7ed))
- **angular:** `npm i @maskito/angular` throws `unable to resolve dependency tree`
([#220](https://github.com/taiga-family/maskito/issues/220))
([8b4d6e6](https://github.com/taiga-family/maskito/commit/8b4d6e6186db47f97d328186b7afd9af75a3889b))
### [0.7.1](https://github.com/taiga-family/maskito/compare/v0.7.0...v0.7.1) (2023-03-22)
### 🐞 Bug Fixes
- **angular:** use `@nrwl/angular:package` executor instead of `@nrwl/angular:ng-packagr-lite`
([#216](https://github.com/taiga-family/maskito/issues/216))
([164d015](https://github.com/taiga-family/maskito/commit/164d015c2f18a279e195b45329e84c0d023c9483))
- **kit:** `Number` broken `Delete`-button navigation if `decimalZeroPadding=true`
([#211](https://github.com/taiga-family/maskito/issues/211))
([1b750d1](https://github.com/taiga-family/maskito/commit/1b750d135ebd53bfeda2ca734425de08a808b1af))
## [0.7.0](https://github.com/taiga-family/maskito/compare/v0.6.0...v0.7.0) (2023-03-20)
### 🚀 Features
- **core:** add `deleteSoftLineBackward` & `deleteSoftLineForward` support
([#207](https://github.com/taiga-family/maskito/issues/207))
([cbd5479](https://github.com/taiga-family/maskito/commit/cbd5479c04c07113804eee6ea6c9838ee8681597))
- **kit:** use 1 as min segment value in `Date`-related masks
([#197](https://github.com/taiga-family/maskito/issues/197))
([c85ca23](https://github.com/taiga-family/maskito/commit/c85ca2355cb0b6fcef73f3e7497f7c31fa82c87c))
### 🐞 Bug Fixes
- **core:** `Maskito` losses valid characters on invalid insertion (`overwriteMode: replace`)
([#208](https://github.com/taiga-family/maskito/issues/208))
([ef183b4](https://github.com/taiga-family/maskito/commit/ef183b454e4a7db5b2cb48cbe26129bf303f676a))
- **kit:** `Number` should drop leading zeroes for negative numbers
([#204](https://github.com/taiga-family/maskito/issues/204))
([6e9adf7](https://github.com/taiga-family/maskito/commit/6e9adf758aa585944ee08f2e2aff81a5664adefd))
## [0.6.0](https://github.com/taiga-family/maskito/compare/v0.5.0...v0.6.0) (2023-03-15)
### 🚀 Features
- **angular:** add CVA and pipe ([#187](https://github.com/taiga-family/maskito/issues/187))
([a099257](https://github.com/taiga-family/maskito/commit/a099257a16b569444cdae9276ce66e9a806f531e))
- **core:** add `deleteWordBackward` & `deleteWordForward` support
([#193](https://github.com/taiga-family/maskito/issues/193))
([24b761c](https://github.com/taiga-family/maskito/commit/24b761c84d0947df5e4c78a2114f1de8f6ca20f4))
### 🐞 Bug Fixes
- **core:** show trailing fixed characters + duplicated fixed character on `Drop`
([#185](https://github.com/taiga-family/maskito/issues/185))
([c7f6a1b](https://github.com/taiga-family/maskito/commit/c7f6a1bb8098b5641ed8c6921c2ebc86c6135b58))
- **kit:** `maskitoParseNumber` incorrectly parses negative numbers
([#190](https://github.com/taiga-family/maskito/issues/190))
([d713bd1](https://github.com/taiga-family/maskito/commit/d713bd143e5090870a406ea14498cb99843bb9d0))
- **kit:** `Number` should drop decimal part on paste from clipboard if `precision=0`
([#195](https://github.com/taiga-family/maskito/issues/195))
([ba85c38](https://github.com/taiga-family/maskito/commit/ba85c38ec0c81a22ff758f4ba386d045ac49ffd5))
## [0.5.0](https://github.com/taiga-family/maskito/compare/v0.4.0...v0.5.0) (2023-03-09)
### 🚀 Features
- **core:** new utility `maskitoTransform(value, maskitoOptions)`
([#177](https://github.com/taiga-family/maskito/issues/177))
([20316f1](https://github.com/taiga-family/maskito/commit/20316f15e153bfeeb45eda6406b8792e00f3238f))
- **kit:** new utility `maskitoParseNumber` ([#178](https://github.com/taiga-family/maskito/issues/178))
([fc58141](https://github.com/taiga-family/maskito/commit/fc58141625ecbdc7d804aa382a69b38bf7146fc4))
### 🐞 Bug Fixes
- **kit:** `Number` mask throws an error on empty string in `thousandSeparator`
([#176](https://github.com/taiga-family/maskito/issues/176))
([cd52fad](https://github.com/taiga-family/maskito/commit/cd52fad80bc278f171dafa1709c54cba3f8fbc81))
## [0.4.0](https://github.com/taiga-family/maskito/compare/v0.3.0...v0.4.0) (2023-03-02)
### 🚀 Features
- **angular:** `maskitoElement` add new input ([#164](https://github.com/taiga-family/maskito/issues/164))
([407c131](https://github.com/taiga-family/maskito/commit/407c131d2d8f8514173ad7a5e248759e2d4f8abc))
### 🐞 Bug Fixes
- **kit:** `Number` pads empty integer part when paste from clipboard
([#168](https://github.com/taiga-family/maskito/issues/168))
([d043a82](https://github.com/taiga-family/maskito/commit/d043a82561cbed94b19d59d174fa6da7f08d49d4))
## [0.3.0](https://github.com/taiga-family/maskito/compare/v0.2.0...v0.3.0) (2023-03-01)
### 🚀 Features
- **angular:** add other maskito packages to `ng-update.packageGroup` of `package.json`
([#161](https://github.com/taiga-family/maskito/issues/161))
([bdecdaa](https://github.com/taiga-family/maskito/commit/bdecdaa9cac2681e35191cabd2d5d853eb97a09d))
### 🐞 Bug Fixes
- **kit:** `Date`-mask fix wrong year that appears using the min property
([#157](https://github.com/taiga-family/maskito/issues/157))
([544e891](https://github.com/taiga-family/maskito/commit/544e8912d2752e0fc8f77757e935070b94823f65))
## [0.2.0](https://github.com/taiga-family/maskito/compare/v0.1.1...v0.2.0) (2023-02-28)
### 🚀 Features
- **kit:** new `DateTime` mask ([#146](https://github.com/taiga-family/maskito/issues/146))
([6d6b2c1](https://github.com/taiga-family/maskito/commit/6d6b2c17b5c0f62bc804451524cd4b2ce3e50660))
### 🐞 Bug Fixes
- **kit:** `Number` is now replacing hyphen, en-dash and em-dash with minus sign
([#153](https://github.com/taiga-family/maskito/issues/153))
([1f21f11](https://github.com/taiga-family/maskito/commit/1f21f1159baadcef65e49bacaec77eba3b6f36d8))
### [0.1.1](https://github.com/taiga-family/maskito/compare/v0.1.0...v0.1.1) (2023-02-15)
### 🐞 Bug Fixes
- **core:** `Module parse failed: 'import' and 'export' may appear only with 'sourceType: module'`
([#131](https://github.com/taiga-family/maskito/issues/131))
([41e05c0](https://github.com/taiga-family/maskito/commit/41e05c09e41ed611e0c2b9aa07a953dfbe049da7))
## 0.1.0 (2023-02-14)
This release introduces the first publishing of the following packages:
- `@maskito/core` It is the main zero-dependency and framework-agnostic package. It can be used alone in Vanilla
JavaScript project. It listens `beforeinput` and `input` events to validate and calibrate textfield's value.
Read more: https://maskito.dev/core-concepts/overview
- `@maskito/kit` The optional framework-agnostic package. It contains ready-to-use masks with configurable
parameters. This release introduces the following masks:
- [Number](https://maskito.dev/kit/number)
- [Time](https://maskito.dev/kit/time)
- [Date](https://maskito.dev/kit/date)
- [DateRange](https://maskito.dev/kit/date-range)
- `@maskito/angular` The Angular-specific library. It provides two convenient ways of using Maskito:
- Basic directive approach (when developer has direct access to native input element).
- Dependency Injection approach (when native input element is hidden somewhere deep inside another component).
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 Acpekt
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Maskito
[](https://npmjs.com/package/@maskito/core)
[](https://github.com/taiga-family/maskito/actions/workflows/build.yml)
Documentation •
Submit an Issue •
Contact Us
**Maskito** is a collection of libraries, built with TypeScript. It helps you to create an input mask which ensures that
users type values according to predefined format.
## Why Maskito
- **Maskito** supports all user’s interactions with text fields: basic typing and deleting via keyboard, pasting,
dropping text inside with a pointer, browser autofill, predictive text from mobile native keyboard.
- **Maskito** is robust. The whole project is developed with strict TypeScript mode. Our code is covered by hundreds of
[Cypress](https://www.cypress.io) tests.
- Server Side Rendering and Shadow DOM support.
- You can use it with `HTMLInputElement` or `HTMLTextAreaElement` or even with `[contenteditable]` element.
- **Maskito** core is zero-dependency package. You can mask input in your vanilla JavaScript project. However, we have
separate packages for Angular, React and Vue as well.
- **Maskito** includes optional framework-agnostic package with configurable ready-to-use masks.
No text field with invalid value! Use Maskito. **Mask it!** Learn more about the library in our
[documentation](https://maskito.dev).
## Contributing
If you have suggestions for how **Maskito** could be improved, or want to report a bug, open an issue! We'd love all and
any contributions.
For more, check out the [Contributing Guide](CONTRIBUTING.md).
## Maintained
Maskito is a part of [Taiga UI](https://github.com/taiga-family/taiga-ui) libraries family which is backed and used by a
large enterprise. This means you can rely on timely support and continuous development.
| **Package** | **Downloads** |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| [@maskito/core](https://npmjs.com/package/@maskito/core) | [](https://npmjs.com/package/@maskito/core) |
| [@maskito/kit](https://npmjs.com/package/@maskito/kit) | [](https://npmjs.com/package/@maskito/kit) |
| [@maskito/react](https://npmjs.com/package/@maskito/react) | [](https://npmjs.com/package/@maskito/react) |
| [@maskito/angular](https://npmjs.com/package/@maskito/angular) | [](https://npmjs.com/package/@maskito/angular) |
| [@maskito/vue](https://npmjs.com/package/@maskito/vue) | [](https://npmjs.com/package/@maskito/vue) |
| [@maskito/phone](https://npmjs.com/package/@maskito/phone) | [](https://npmjs.com/package/@maskito/phone) |
## License
🆓 Feel free to use our library in your commercial and private applications
All **Maskito** packages are covered by [Apache 2.0](/LICENSE)
Read more about this license [here](https://choosealicense.com/licenses/apache-2.0/)
================================================
FILE: codecov.yml
================================================
codecov:
branch: main
notify:
require_ci_to_pass: no
coverage:
# This value is used to customize the visible color range in Codecov.
# The first number represents the red, and the second represents green.
# You can change the range of colors by adjusting this configuration.
range: 50..100 # by default 70..100
round: down
precision: 2
# Disable codecov/patch check
status:
project:
default:
enabled: false
patch:
default:
enabled: false
================================================
FILE: eslint.config.ts
================================================
import taiga from '@taiga-ui/eslint-plugin-experience-next';
export default [
...taiga.configs.recommended,
{
files: ['*.tsx'],
rules: {
'react/display-name': 'off',
'react/react-in-jsx-scope': 'off',
'no-irregular-whitespace': 'off',
},
},
{
files: ['*.spec.tsx'],
rules: {
'jest/prefer-ending-with-an-expect': [
'error',
{assertFunctionNames: ['expect', 'check']},
],
},
},
// TODO: fix later
{
files: ['**/*'],
rules: {
'@typescript-eslint/no-unused-private-class-members': 'off',
'@typescript-eslint/prefer-function-type': 'off',
'@typescript-eslint/no-restricted-types': 'off',
'@typescript-eslint/non-nullable-type-assertion-style': 'off',
'@angular-eslint/template/alt-text': 'off',
'@angular-eslint/prefer-signals': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'no-irregular-whitespace': 'off',
'de-morgan/no-negated-disjunction': 'off',
'@angular-eslint/template/no-interpolation-in-attributes': 'off',
'@typescript-eslint/no-invalid-this': 'off',
'@angular-eslint/consistent-component-styles': 'off',
'@typescript-eslint/no-useless-default-assignment': 'off',
'import/consistent-type-specifier-style': 'off',
'@typescript-eslint/no-unnecessary-type-conversion': 'off',
'@typescript-eslint/consistent-type-exports': 'off',
'@typescript-eslint/method-signature-style': 'off',
'@typescript-eslint/strict-void-return': 'off',
},
},
];
================================================
FILE: firebase.json
================================================
{
"hosting": {
"public": "dist/demo/browser",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
================================================
FILE: jest.config.ts
================================================
import {getJestProjectsAsync} from '@nx/jest';
import type {Config} from 'jest';
export default async (): Promise => ({projects: await getJestProjectsAsync()});
================================================
FILE: jest.preset.js
================================================
const nxPreset = require('@nx/jest/preset').default;
const {resolve} = require('node:path');
module.exports = {
...nxPreset,
transform: {
'^.+\\.(ts|tsx|js|jsx|mjs|html|svg)$': [
'jest-preset-angular',
{
diagnostics: true,
stringifyContentPathRegex: String.raw`\.html$`,
tsconfig: resolve(__dirname, 'tsconfig.spec.json'),
},
],
},
};
================================================
FILE: nx.json
================================================
{
"tui": {
"enabled": false
},
"workspaceLayout": {
"libsDir": "projects",
"appsDir": "projects"
},
"defaultProject": "demo",
"generators": {
"@nx/js:library": {
"buildable": true,
"publishable": true,
"strict": true,
"linter": "none",
"unitTestRunner": "jest",
"config": "project"
},
"@nx/angular:library": {
"linter": "none",
"unitTestRunner": "jest",
"buildable": true,
"publishable": true,
"compilationMode": "partial",
"strict": true,
"skipModule": true,
"standaloneConfig": true
},
"@nx/angular:application": {
"style": "less",
"linter": "none",
"unitTestRunner": "jest"
},
"@nx/angular:component": {
"style": "less"
},
"@nx/react": {
"application": {
"babel": true
},
"library": {
"linter": "eslint",
"publishable": true,
"bundler": "rollup",
"style": "none",
"strict": true,
"unitTestRunner": "jest"
}
}
},
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"sharedGlobals": [
"{workspaceRoot}/angular.json",
"{workspaceRoot}/nx.json",
"{workspaceRoot}/karma.*.js",
"{workspaceRoot}/tsconfig.*.json",
"{workspaceRoot}/tsconfig.json",
"{workspaceRoot}/babel.config.json"
],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/**/*.cy.[jt]s?(x)",
"!{projectRoot}/cypress.config.[jt]s"
]
},
"targetDefaults": {
"build": {
"inputs": ["production", "^production"],
"cache": true
},
"lint": {
"cache": true
},
"@nx/jest:jest": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true,
"options": {
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"component-test": {
"cache": true,
"inputs": ["default", "^production"]
},
"ct-react": {
"cache": true,
"inputs": ["default", "^production"]
}
},
"parallel": 3,
"useInferencePlugins": false,
"defaultBase": "origin/main"
}
================================================
FILE: package.json
================================================
{
"name": "maskito",
"version": "5.2.2",
"description": "Collection of libraries to create an input mask which ensures that user types value according to predefined format",
"homepage": "https://maskito.dev",
"bugs": "https://github.com/taiga-family/maskito/issues",
"repository": {
"type": "git",
"url": "https://github.com/taiga-family/maskito.git"
},
"license": "Apache-2.0",
"author": {
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov",
"url": "https://github.com/nsbarsukov"
},
"contributors": [
{
"email": "alexander@inkin.ru",
"name": "Alex Inkin"
},
{
"email": "vladimir.potekh@gmail.com",
"name": "Vladimir Potekhin"
},
{
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov"
},
{
"email": "nextzeddicus@gmail.com",
"name": "Georgiy Lunin"
}
],
"workspaces": [
"projects/*"
],
"scripts": {
"build": "nx run demo:build:production",
"cspell": "cspell --relative --dot --gitignore .",
"cy:open": "cypress open --project ./projects/demo-integrations/",
"cy:run": "nx e2e demo-integrations && nx component-test demo-integrations && nx ct-react demo-integrations",
"lint": "eslint .",
"prepare": "husky",
"prettier": "prettier !package-lock.json . --ignore-path .gitignore",
"release": "npx nx run-many --target publish --all",
"release:local": "npx release-it --no-git.push --'hooks.before:release=\"echo Skip publish\"'",
"serve:ssr": "node dist/demo/server/server.mjs",
"start": "nx run demo:serve",
"stylelint": "stylelint '**/*.{less,css}'",
"test": "nx run-many --target test --all",
"typecheck": "tsc --noEmit --skipLibCheck --incremental false --tsBuildInfoFile null --project tsconfig.spec.json && tsc -p projects/demo-integrations/tsconfig.json"
},
"commitlint": {
"extends": [
"@taiga-ui/commitlint-config"
]
},
"lint-staged": {
"*.less": [
"stylelint --fix"
],
"*.{js,ts,html,md,less,json,svg,yml}": [
"npm run lint -- --fix",
"prettier --write"
]
},
"browserslist": [
"extends @taiga-ui/browserslist-config"
],
"prettier": "@taiga-ui/prettier-config",
"stylelint": {
"extends": [
"@taiga-ui/stylelint-config"
],
"ignoreFiles": [
"**/dist/**",
"**/coverage/**",
"**/node_modules/**"
]
},
"overrides": {
"@taiga-ui/addon-commerce": {
"@maskito/angular": ">=4.0.0",
"@maskito/core": ">=4.0.0",
"@maskito/kit": ">=4.0.0",
"@maskito/phone": ">=4.0.0"
},
"@taiga-ui/kit": {
"@maskito/angular": ">=4.0.0",
"@maskito/core": ">=4.0.0",
"@maskito/kit": ">=4.0.0",
"@maskito/phone": ">=4.0.0"
}
},
"devDependencies": {
"@angular-devkit/build-angular": "19.2.22",
"@angular-devkit/core": "19.2.22",
"@angular-devkit/schematics": "19.2.22",
"@angular/build": "19.2.22",
"@angular/cli": "19.2.22",
"@angular/compiler-cli": "19.2.20",
"@angular/core": "19.2.20",
"@angular/platform-browser-dynamic": "19.2.20",
"@nx/angular": "21.6.3",
"@nx/eslint": "21.6.3",
"@nx/jest": "21.6.3",
"@nx/js": "21.6.3",
"@nx/module-federation": "21.6.3",
"@nx/react": "21.6.3",
"@nx/rollup": "21.6.3",
"@nx/workspace": "21.6.3",
"@taiga-ui/configs": "0.476.0",
"@tinkoff/eslint-config": "5.2.0",
"@tinkoff/eslint-config-react": "5.2.0",
"@types/highlight.js": "10.1.0",
"@types/node": "24.10.11",
"http-server": "14.1.1",
"husky": "9.1.7",
"ng-packagr": "19.2.2",
"nx": "21.6.3",
"postcss-preset-env": "10.6.1",
"ts-node": "10.9.2",
"tsutils": "3.21.0",
"typescript": "5.8.3"
},
"engines": {
"node": ">= 24",
"npm": ">= 11",
"yarn": "Please use npm instead of yarn to install dependencies"
},
"auto-changelog": {
"prepend": true,
"template": "templates/note.hbs"
},
"syncer": {
"includePaths": [
"./projects",
"./package-lock.json"
],
"matchPackageNames": [
"@maskito/*",
"maskito"
]
}
}
================================================
FILE: projects/angular/README.md
================================================
# @maskito/angular
[](https://npmjs.com/package/@maskito/angular)
[](https://bundlephobia.com/result?p=@maskito/angular)
Documentation •
Submit an Issue •
Contact Us
> The Angular-specific library. It provides a convenient way to use Maskito as a directive.
## How to install
```bash
npm i @maskito/{core,angular}
```
================================================
FILE: projects/angular/jest.config.ts
================================================
export default {
displayName: 'angular',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['/src/test-setup.ts'],
coverageDirectory: '../../coverage/angular',
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};
================================================
FILE: projects/angular/ng-package.json
================================================
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/angular",
"lib": {
"entryFile": "src/index.ts"
}
}
================================================
FILE: projects/angular/package.json
================================================
{
"name": "@maskito/angular",
"version": "5.2.2",
"description": "The Angular-specific Maskito's library",
"keywords": [
"input",
"mask",
"inputmask",
"input-mask",
"text-mask",
"format",
"input-format",
"input-formatting",
"angular"
],
"homepage": "https://maskito.dev",
"bugs": "https://github.com/taiga-family/maskito/issues",
"repository": {
"type": "git",
"url": "https://github.com/taiga-family/maskito.git"
},
"license": "Apache-2.0",
"author": {
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov",
"url": "https://github.com/nsbarsukov"
},
"contributors": [
{
"email": "alexander@inkin.ru",
"name": "Alex Inkin"
},
{
"email": "vladimir.potekh@gmail.com",
"name": "Vladimir Potekhin"
},
{
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov"
},
{
"email": "nextzeddicus@gmail.com",
"name": "Georgiy Lunin"
}
],
"dependencies": {
"tslib": "2.8.1"
},
"devDependencies": {
"@angular/core": "19.2.20",
"@angular/forms": "19.2.20"
},
"peerDependencies": {
"@angular/core": ">=19.0.0",
"@angular/forms": ">=19.0.0",
"@maskito/core": "^5.2.2"
},
"ng-update": {
"packageGroup": [
"@maskito/core",
"@maskito/kit"
]
}
}
================================================
FILE: projects/angular/project.json
================================================
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "angular",
"prefix": "maskito",
"projectType": "library",
"sourceRoot": "projects/angular/src",
"tags": [],
"targets": {
"build": {
"configurations": {
"development": {},
"production": {
"tsConfig": "{projectRoot}/tsconfig.lib.prod.json"
}
},
"defaultConfiguration": "production",
"dependsOn": [
{
"dependencies": true,
"params": "forward",
"target": "build"
}
],
"executor": "@nx/angular:package",
"options": {
"project": "{projectRoot}/ng-package.json",
"tsConfig": "tsconfig.build.json"
},
"outputs": ["{workspaceRoot}/dist/{projectName}"]
},
"publish": {
"dependsOn": [
{
"params": "ignore",
"target": "build"
}
],
"executor": "nx:run-commands",
"options": {
"command": "npm publish ./dist/{projectName} --ignore-scripts"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "{projectRoot}/jest.config.ts"
},
"outputs": ["{workspaceRoot}/coverage/{projectName}"]
}
}
}
================================================
FILE: projects/angular/src/index.ts
================================================
export * from './lib/maskito.directive';
export * from './lib/maskito.pipe';
export * from './lib/pattern.directive';
================================================
FILE: projects/angular/src/lib/maskito.directive.ts
================================================
import {
Directive,
effect,
ElementRef,
inject,
model,
NgZone,
type OnDestroy,
untracked,
} from '@angular/core';
import {DefaultValueAccessor} from '@angular/forms';
import {
Maskito,
MASKITO_DEFAULT_ELEMENT_PREDICATE,
type MaskitoOptions,
maskitoTransform,
} from '@maskito/core';
@Directive({selector: '[maskito]'})
export class MaskitoDirective implements OnDestroy {
private readonly elementRef: HTMLElement = inject(ElementRef).nativeElement;
private readonly ngZone = inject(NgZone);
private maskedElement: Maskito | null = null;
protected readonly initEffect = effect(async () => {
const options = this.options();
const elementPredicate = this.elementPredicate();
const {elementRef, ngZone} = this;
this.destroy();
if (!options) {
return;
}
const predicateResult = await elementPredicate(elementRef);
if (
untracked(this.elementPredicate) !== elementPredicate ||
untracked(this.options) !== options
) {
// Ignore the result of the predicate if the
// maskito element (or its options) has changed before the predicate was resolved.
return;
}
ngZone.runOutsideAngular(() => {
this.maskedElement = new Maskito(predicateResult, options);
});
});
public readonly options = model(null, {alias: 'maskito'});
public readonly elementPredicate = model(MASKITO_DEFAULT_ELEMENT_PREDICATE, {
alias: 'maskitoElement',
});
constructor() {
const accessor = inject(DefaultValueAccessor, {self: true, optional: true});
if (accessor) {
const original = accessor.writeValue.bind(accessor);
accessor.writeValue = (value: unknown) => {
const options = untracked(this.options);
original(
options ? maskitoTransform(String(value ?? ''), options) : value,
);
};
}
}
public ngOnDestroy(): void {
this.destroy();
}
private destroy(): void {
this.maskedElement?.destroy();
this.maskedElement = null;
}
}
================================================
FILE: projects/angular/src/lib/maskito.pipe.ts
================================================
import {Pipe, type PipeTransform} from '@angular/core';
import {
MASKITO_DEFAULT_OPTIONS,
type MaskitoOptions,
maskitoTransform,
} from '@maskito/core';
@Pipe({name: 'maskito'})
export class MaskitoPipe implements PipeTransform {
public transform(value: unknown, maskitoOptions: MaskitoOptions | null): string {
return maskitoTransform(
String(value ?? ''),
maskitoOptions ?? MASKITO_DEFAULT_OPTIONS,
);
}
}
================================================
FILE: projects/angular/src/lib/pattern.directive.ts
================================================
import {Directive, inject} from '@angular/core';
import {MaskitoDirective} from './maskito.directive';
@Directive({
selector: '[maskitoPattern]',
inputs: ['maskitoPattern'],
hostDirectives: [MaskitoDirective],
})
export class MaskitoPattern {
private readonly maskitoDirective = inject(MaskitoDirective, {self: true});
public set maskitoPattern(pattern: RegExp | string) {
this.maskitoDirective.options.set({
mask: typeof pattern === 'string' ? new RegExp(`^${pattern}$`) : pattern,
});
}
}
================================================
FILE: projects/angular/src/lib/tests/maskito.directive.spec.ts
================================================
import {ChangeDetectionStrategy, Component, signal} from '@angular/core';
import {type ComponentFixture, TestBed} from '@angular/core/testing';
import {afterEach, beforeEach, describe, expect, it, jest} from '@jest/globals';
import {MaskitoDirective} from '@maskito/angular';
import {
Maskito,
MASKITO_DEFAULT_ELEMENT_PREDICATE,
type MaskitoElementPredicate,
type MaskitoOptions,
} from '@maskito/core';
const DIGIT_ONLY: MaskitoOptions = {mask: /^\d*$/};
describe('MaskitoDirective — initEffect', () => {
@Component({
imports: [MaskitoDirective],
template: `
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestHostComponent {
public readonly options = signal(null);
public readonly predicate = signal(MASKITO_DEFAULT_ELEMENT_PREDICATE);
}
let fixture: ComponentFixture;
let destroySpy: ReturnType;
function getDirective(): MaskitoDirective {
return fixture.debugElement.children[0]!.injector.get(MaskitoDirective);
}
function getMaskedElement(): Maskito | null {
return (getDirective() as unknown as {maskedElement: Maskito | null})
.maskedElement;
}
beforeEach(() => {
TestBed.configureTestingModule({imports: [TestHostComponent]});
destroySpy = jest.spyOn(Maskito.prototype, 'destroy');
fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('when options is null', () => {
it('does not create a Maskito instance', async () => {
await fixture.whenStable();
expect(getMaskedElement()).toBeNull();
});
});
describe('when options are provided', () => {
beforeEach(async () => {
fixture.componentInstance.options.set(DIGIT_ONLY);
fixture.detectChanges();
await fixture.whenStable();
});
it('creates a Maskito instance', () => {
expect(getMaskedElement()).toBeInstanceOf(Maskito);
});
it('destroys the old instance and creates a new one when options change', async () => {
fixture.componentInstance.options.set({mask: /^[a-z]*$/});
fixture.detectChanges();
await fixture.whenStable();
expect(destroySpy).toHaveBeenCalledTimes(1);
expect(getMaskedElement()).toBeInstanceOf(Maskito);
});
it('destroys the instance when options become null', async () => {
fixture.componentInstance.options.set(null);
fixture.detectChanges();
await fixture.whenStable();
expect(destroySpy).toHaveBeenCalledTimes(1);
expect(getMaskedElement()).toBeNull();
});
it('destroys the instance when the directive is destroyed', () => {
const directive = getDirective() as unknown as {
maskedElement: Maskito | null;
};
fixture.destroy();
expect(destroySpy).toHaveBeenCalledTimes(1);
expect(directive.maskedElement).toBeNull();
});
});
describe('stale async predicate', () => {
function makeControlledPredicate(): {
predicate: MaskitoElementPredicate;
resolveCall: (index: number) => void;
} {
const resolvers: Array<() => void> = [];
const predicate: MaskitoElementPredicate = async (el) =>
new Promise((resolve) => {
resolvers.push(() => resolve(el as HTMLInputElement));
});
return {predicate, resolveCall: (i) => resolvers[i]!()};
}
it('ignores the result when elementPredicate changes before it resolves', async () => {
const {predicate: slowPredicate, resolveCall} = makeControlledPredicate();
fixture.componentInstance.options.set(DIGIT_ONLY);
fixture.componentInstance.predicate.set(slowPredicate);
fixture.detectChanges();
await fixture.whenStable();
expect(getMaskedElement()).toBeNull();
fixture.componentInstance.predicate.set(MASKITO_DEFAULT_ELEMENT_PREDICATE);
fixture.detectChanges();
await fixture.whenStable();
expect(getMaskedElement()).toBeInstanceOf(Maskito);
expect(destroySpy).not.toHaveBeenCalled();
resolveCall(0);
await fixture.whenStable();
expect(destroySpy).not.toHaveBeenCalled();
expect(getMaskedElement()).toBeInstanceOf(Maskito);
});
it('ignores the result when options change before the predicate resolves', async () => {
const {predicate: slowPredicate, resolveCall} = makeControlledPredicate();
fixture.componentInstance.options.set(DIGIT_ONLY);
fixture.componentInstance.predicate.set(slowPredicate);
fixture.detectChanges();
await fixture.whenStable();
expect(getMaskedElement()).toBeNull();
fixture.componentInstance.options.set({mask: /^[a-z]*$/});
fixture.detectChanges();
await fixture.whenStable();
resolveCall(0);
await fixture.whenStable();
expect(destroySpy).not.toHaveBeenCalled();
expect(getMaskedElement()).toBeNull();
});
});
});
================================================
FILE: projects/angular/src/lib/tests/maskito.spec.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {type ComponentFixture, TestBed} from '@angular/core/testing';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {beforeEach, describe, expect, it} from '@jest/globals';
import {MaskitoDirective, MaskitoPipe} from '@maskito/angular';
import type {MaskitoOptions} from '@maskito/core';
describe('Maskito Angular package', () => {
@Component({
imports: [MaskitoDirective, MaskitoPipe, ReactiveFormsModule],
template: `
{{ control.value | maskito: options }}
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
changeDetection: ChangeDetectionStrategy.Default,
})
class TestComponent {
public readonly control = new FormControl();
public options: MaskitoOptions | null = {
mask: /^\d+(,\d{0,2})?$/,
preprocessors: [
({elementState, data}) => {
const {value, selection} = elementState;
return {
elementState: {
selection,
value: value.replace('.', ','),
},
data: data.replace('.', ','),
};
},
],
};
}
let fixture: ComponentFixture;
beforeEach(() => {
TestBed.configureTestingModule({imports: [TestComponent]});
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('null is treated as empty string', () => {
expect(getText()).toBe('');
expect(getValue()).toBe('');
});
it('formats new control value', () => {
fixture.componentInstance.control.setValue(12345.6789);
fixture.detectChanges();
expect(getText()).toBe('12345,67');
expect(getValue()).toBe('12345,67');
});
it('disable mask formatting if options is null', () => {
fixture.componentInstance.options = null;
fixture.detectChanges();
fixture.componentInstance.control.setValue(123456.9999);
fixture.detectChanges();
expect(getText()).toBe('123456.9999');
expect(getValue()).toBe('123456.9999');
});
function getText(): string {
return fixture.debugElement.nativeElement
.querySelector('#pipe')
.textContent.trim();
}
function getValue(): string {
return fixture.debugElement.nativeElement.querySelector('#input').value;
}
});
================================================
FILE: projects/angular/src/test-setup.ts
================================================
import {setupZoneTestEnv} from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();
================================================
FILE: projects/angular/tsconfig.lib.prod.json
================================================
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"declarationMap": false
}
}
================================================
FILE: projects/core/README.md
================================================
# @maskito/core
[](https://npmjs.com/package/@maskito/core)
[](https://bundlephobia.com/result?p=@maskito/core)
Documentation •
Submit an Issue •
Contact Us
> It is the main zero-dependency and framework-agnostic Maskito's package. It can be used alone in
> vanilla JavaScript project.
## How to install
```bash
npm i @maskito/core
```
================================================
FILE: projects/core/jest.config.ts
================================================
export default {
displayName: 'core',
preset: '../../jest.preset.js',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/core',
};
================================================
FILE: projects/core/package.json
================================================
{
"name": "@maskito/core",
"version": "5.2.2",
"description": "The main zero-dependency and framework-agnostic Maskito's package to create an input mask",
"keywords": [
"input",
"mask",
"inputmask",
"input-mask",
"text-mask",
"format",
"input-format",
"input-formatting",
"javascript",
"typescript"
],
"homepage": "https://maskito.dev",
"bugs": "https://github.com/taiga-family/maskito/issues",
"repository": {
"type": "git",
"url": "https://github.com/taiga-family/maskito.git"
},
"license": "Apache-2.0",
"author": {
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov",
"url": "https://github.com/nsbarsukov"
},
"contributors": [
{
"email": "alexander@inkin.ru",
"name": "Alex Inkin"
},
{
"email": "vladimir.potekh@gmail.com",
"name": "Vladimir Potekhin"
},
{
"email": "nikita.s.barsukov@gmail.com",
"name": "Nikita Barsukov"
},
{
"email": "nextzeddicus@gmail.com",
"name": "Georgiy Lunin"
}
]
}
================================================
FILE: projects/core/project.json
================================================
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "core",
"projectType": "library",
"sourceRoot": "projects/core/src",
"tags": [],
"targets": {
"build": {
"executor": "@nx/rollup:rollup",
"options": {
"assets": [
{
"glob": "README.md",
"input": "{projectRoot}",
"output": "."
}
],
"compiler": "tsc",
"external": "all",
"format": ["esm", "cjs"],
"main": "{projectRoot}/src/index.ts",
"outputPath": "dist/{projectName}",
"project": "{projectRoot}/package.json",
"tsConfig": "tsconfig.build.json",
"useLegacyTypescriptPlugin": false
},
"outputs": ["{options.outputPath}"]
},
"publish": {
"dependsOn": [
{
"params": "ignore",
"target": "build"
}
],
"executor": "nx:run-commands",
"options": {
"command": "npm publish ./dist/{projectName} --ignore-scripts"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "{projectRoot}/jest.config.ts"
},
"outputs": ["{workspaceRoot}/coverage/{projectName}"]
}
}
}
================================================
FILE: projects/core/src/index.ts
================================================
export {
MASKITO_DEFAULT_ELEMENT_PREDICATE,
MASKITO_DEFAULT_OPTIONS,
} from './lib/constants';
export {Maskito} from './lib/mask';
export {
maskitoChangeEventPlugin,
maskitoInitialCalibrationPlugin,
maskitoStrictCompositionPlugin,
} from './lib/plugins';
export type {
MaskitoElement,
MaskitoElementPredicate,
MaskitoMask,
MaskitoMaskExpression,
MaskitoOptions,
MaskitoPlugin,
MaskitoPostprocessor,
MaskitoPreprocessor,
} from './lib/types';
export {
maskitoAdaptContentEditable,
maskitoPipe,
maskitoTransform,
maskitoUpdateElement,
} from './lib/utils';
================================================
FILE: projects/core/src/lib/classes/index.ts
================================================
export {MaskHistory} from './mask-history';
export {MaskModel} from './mask-model/mask-model';
================================================
FILE: projects/core/src/lib/classes/mask-history.ts
================================================
import type {ElementState, TypedInputEvent} from '../types';
export abstract class MaskHistory {
private now: ElementState | null = null;
private readonly past: ElementState[] = [];
private future: ElementState[] = [];
protected abstract updateElementState(
state: ElementState,
eventInit: Pick,
): void;
protected undo(): void {
const state = this.past.pop();
if (state && this.now) {
this.future.push(this.now);
this.updateElement(state, 'historyUndo');
}
}
protected redo(): void {
const state = this.future.pop();
if (state && this.now) {
this.past.push(this.now);
this.updateElement(state, 'historyRedo');
}
}
protected updateHistory(state: ElementState): void {
if (!this.now) {
this.now = state;
return;
}
const isValueChanged = this.now.value !== state.value;
const isSelectionChanged = this.now.selection.some(
(item, index) => item !== state.selection[index],
);
if (!isValueChanged && !isSelectionChanged) {
return;
}
if (isValueChanged) {
this.past.push(this.now);
this.future = [];
}
this.now = state;
}
private updateElement(
state: ElementState,
inputType: TypedInputEvent['inputType'],
): void {
this.now = state;
this.updateElementState(state, {inputType, data: null});
}
}
================================================
FILE: projects/core/src/lib/classes/mask-model/mask-model.ts
================================================
import type {
ElementState,
MaskitoMaskExpression,
MaskitoOptions,
SelectionRange,
} from '../../types';
import {areElementStatesEqual} from '../../utils/element-states-equality';
import {applyOverwriteMode} from './utils/apply-overwrite-mode';
import {calibrateValueByMask} from './utils/calibrate-value-by-mask';
import {removeFixedMaskCharacters} from './utils/remove-fixed-mask-characters';
export class MaskModel implements ElementState {
private readonly unmaskInitialState: ElementState = {value: '', selection: [0, 0]};
public value = '';
public selection: SelectionRange = [0, 0];
constructor(
initialElementState: ElementState,
private readonly maskOptions: Required,
) {
const expression = this.getMaskExpression(initialElementState);
const {value, selection} = calibrateValueByMask(initialElementState, expression);
this.unmaskInitialState = removeFixedMaskCharacters(
{value, selection},
expression,
);
this.value = value;
this.selection = selection;
}
public addCharacters(newCharacters: string): void {
const {value, selection, maskOptions} = this;
const initialElementState = {value, selection} as const;
const {
selection: [from, to],
} = applyOverwriteMode(
initialElementState,
newCharacters,
maskOptions.overwriteMode,
);
const maskExpression = this.getMaskExpression({
value: `${value.slice(0, from)}${newCharacters}${value.slice(to)}`,
selection: [from + newCharacters.length, from + newCharacters.length],
});
const [unmaskedFrom, unmaskedTo] = applyOverwriteMode(
this.unmaskInitialState,
newCharacters,
maskOptions.overwriteMode,
).selection;
const newUnmaskedLeadingValuePart = `${this.unmaskInitialState.value.slice(0, unmaskedFrom)}${newCharacters}`;
const newCaretIndex = newUnmaskedLeadingValuePart.length;
const maskedElementState = calibrateValueByMask(
{
value: `${newUnmaskedLeadingValuePart}${this.unmaskInitialState.value.slice(unmaskedTo)}`,
selection: [newCaretIndex, newCaretIndex],
},
maskExpression,
initialElementState,
);
const prevLeadingPart = value.slice(0, from);
const newLeadingPartState = calibrateValueByMask(
{
value: newUnmaskedLeadingValuePart,
selection: [newCaretIndex, newCaretIndex],
},
maskExpression,
initialElementState,
);
const isInvalidCharsInsertion =
newLeadingPartState.value === prevLeadingPart ||
(newLeadingPartState.value.length < prevLeadingPart.length &&
removeFixedMaskCharacters(newLeadingPartState, maskExpression).value ===
this.unmaskInitialState.value.slice(0, unmaskedFrom));
if (
isInvalidCharsInsertion ||
areElementStatesEqual(this, maskedElementState) // If typing new characters does not change value
) {
throw new Error('Invalid mask value');
}
this.value = maskedElementState.value;
this.selection = maskedElementState.selection;
}
public deleteCharacters(): void {
const [from, to] = this.selection;
if (from === to || !to) {
return;
}
const {value} = this;
const maskExpression = this.getMaskExpression({
value: `${value.slice(0, from)}${value.slice(to)}`,
selection: [from, from],
});
const initialElementState = {value, selection: [from, to]} as const;
const [unmaskedFrom, unmaskedTo] = this.unmaskInitialState.selection;
const newUnmaskedValue = `${this.unmaskInitialState.value.slice(0, unmaskedFrom)}${this.unmaskInitialState.value.slice(unmaskedTo)}`;
const maskedElementState = calibrateValueByMask(
{value: newUnmaskedValue, selection: [unmaskedFrom, unmaskedFrom]},
maskExpression,
initialElementState,
);
this.value = maskedElementState.value;
this.selection = maskedElementState.selection;
}
private getMaskExpression(elementState: ElementState): MaskitoMaskExpression {
const {mask} = this.maskOptions;
return typeof mask === 'function' ? mask(elementState) : mask;
}
}
================================================
FILE: projects/core/src/lib/classes/mask-model/tests/dynamic-mask.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import {MASKITO_DEFAULT_OPTIONS} from '@maskito/core';
import type {ElementState, MaskitoMask, MaskitoOptions} from '../../../types';
import {MaskModel} from '../mask-model';
const EMPTY_STATE: ElementState = {
selection: [0, 0],
value: '',
};
describe('MaskModel | Dynamic mask', () => {
describe('switching on the fly works', () => {
const SHORT: MaskitoMask = Array.from({length: 10}).fill(/\d/);
const MEDIUM: MaskitoMask = [
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
' ',
/\d/,
/\d/,
/\d/,
/\d/,
];
const LONG: MaskitoMask = Array.from({length: 20}).fill(/\d/);
const maskitoOptions: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask: ({value}) => {
const digitsCount = value.replaceAll(/\D/g, '').length;
if (digitsCount <= 10) {
return SHORT;
}
if (digitsCount <= 16) {
return MEDIUM;
}
return LONG;
},
};
it('enable short mask if number of digits is <=10', () => {
const maskModel = new MaskModel(EMPTY_STATE, maskitoOptions);
maskModel.addCharacters('01234abc56789');
expect(maskModel.value).toBe('0123456789');
expect(maskModel.selection).toEqual([
'0123456789'.length,
'0123456789'.length,
]);
});
it('enable medium mask if number of digits is 10 < x <= 16', () => {
const maskModel = new MaskModel(EMPTY_STATE, maskitoOptions);
maskModel.addCharacters('01234abc56789123456');
expect(maskModel.value).toBe('0123 4567 8912 3456');
expect(maskModel.selection).toEqual([
'0123 4567 8912 3456'.length,
'0123 4567 8912 3456'.length,
]);
});
it('enable long mask if number of digits is >16 (by paste)', () => {
const maskModel = new MaskModel(EMPTY_STATE, maskitoOptions);
maskModel.addCharacters('01234abc567891234567');
expect(maskModel.value).toBe('01234567891234567');
expect(maskModel.selection).toEqual([
'01234567891234567'.length,
'01234567891234567'.length,
]);
});
it('enable long mask if number of digits is >16 (by adding new character to the previous mask)', () => {
const initialValue = '0123 4567 8912 3456';
const maskModel = new MaskModel(
{
value: initialValue,
selection: [initialValue.length, initialValue.length],
},
maskitoOptions,
);
maskModel.addCharacters('7');
expect(maskModel.value).toBe('01234567891234567');
expect(maskModel.selection).toEqual([
'01234567891234567'.length,
'01234567891234567'.length,
]);
});
});
});
================================================
FILE: projects/core/src/lib/classes/mask-model/tests/mask-model-fixed-characters.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import {MASKITO_DEFAULT_OPTIONS} from '../../../constants';
import type {MaskitoMask, MaskitoOptions} from '../../../types';
import {MaskModel} from '../mask-model';
describe('MaskModel | Fixed characters', () => {
describe('New typed character is equal to the previous (already existing) fixed character', () => {
const phoneMaskitoOptions: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask: [
'+',
'7',
' ',
'(',
/\d/,
/\d/,
/\d/,
')',
' ',
/\d/,
'0',
/\d/,
'-',
/\d/,
/\d/,
'-',
/\d/,
/\d/,
],
};
const check = ({
initialValue,
addedCharacters,
expectedNewValue,
}: {
initialValue: string;
addedCharacters: string;
expectedNewValue: string;
}): void => {
const selection: [number, number] = [
initialValue.length,
initialValue.length,
];
const maskModel = new MaskModel(
{
selection,
value: initialValue,
},
phoneMaskitoOptions,
);
try {
maskModel.addCharacters(addedCharacters);
} finally {
expect(maskModel.value).toBe(expectedNewValue);
expect(maskModel.selection).toEqual([
expectedNewValue.length,
expectedNewValue.length,
]);
}
};
it('+7| => Type 7 => +7 (7', () => {
check({
initialValue: '+7',
addedCharacters: '7',
expectedNewValue: '+7 (7',
});
});
it('+7| => Type 9 => +7 (9', () => {
check({
initialValue: '+7',
addedCharacters: '9',
expectedNewValue: '+7 (9',
});
});
it('+7 | (space after seven) => Type 7 => +7 (7', () => {
check({
initialValue: '+7 ',
addedCharacters: '7',
expectedNewValue: '+7 (7',
});
});
it('+7 (7| => Type 7 => +7 (77', () => {
check({
initialValue: '+7 (7',
addedCharacters: '7',
expectedNewValue: '+7 (77',
});
});
it('+7 (900) 2| (next character is fixed character "0") => Type 1 => +7 (900) 201|', () => {
check({
initialValue: '+7 (900) 2',
addedCharacters: '1',
expectedNewValue: '+7 (900) 201',
});
});
it('+7 (900) 20| => Type 0 => +7 (900) 200|', () => {
check({
initialValue: '+7 (900) 20',
addedCharacters: '0',
expectedNewValue: '+7 (900) 200',
});
});
});
describe('Attempt to insert invalid characters for `overwriteMode: replace`', () => {
const testCases: Record = {
'["$", /d/, /d/]': ['$', /\d/, /\d/],
'dynamic mask': ({value}) => {
const digitsCount = value.replaceAll(/\D/g, '').length;
return [
'$',
...Array.from({length: digitsCount || 1}).fill(/\d/),
];
},
};
Object.entries(testCases).forEach(([title, mask]) => {
describe(`mask expression contains leading characters – ${title}`, () => {
const options: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask,
overwriteMode: 'replace',
};
it('$1|2 => Type A => $1|2', () => {
const value = '$12';
const selection = [2, 2] as const;
const maskModel = new MaskModel({value, selection}, options);
expect(() => maskModel.addCharacters('q')).toThrow();
expect(maskModel.value).toBe(value);
expect(maskModel.selection).toEqual(selection);
});
it('$|12 => Type $ => $|12', () => {
const value = '$12';
const selection = [1, 1] as const;
const maskModel = new MaskModel({value, selection}, options);
expect(() => maskModel.addCharacters('$')).toThrow();
expect(maskModel.value).toBe(value);
expect(maskModel.selection).toEqual(selection);
});
it('$|12 => Type X => $|12', () => {
const value = '$12';
const selection = [1, 1] as const;
const maskModel = new MaskModel({value, selection}, options);
expect(() => maskModel.addCharacters('X')).toThrow();
expect(maskModel.value).toBe(value);
expect(maskModel.selection).toEqual(selection);
});
});
});
});
describe('Dynamic mask expression + trailing fixed character', () => {
const postfix = 'left';
const timeMask: ReadonlyArray = [/\d/, /\d/, ':', /\d/, /\d/];
const timeWithPostfixMask: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask: ({value}) => {
let digitsCount = Math.min(value.replaceAll(/\D/g, '').length, 4);
const afterLastDigit =
timeMask.findIndex((x) => typeof x !== 'string' && !--digitsCount) +
1;
return afterLastDigit
? timeMask.slice(0, afterLastDigit).concat(...postfix)
: [];
},
overwriteMode: 'replace',
};
it('adds trailing postfix on enter of digit inside empty textfield', () => {
const maskModel = new MaskModel(
{
selection: [0, 0],
value: '',
},
timeWithPostfixMask,
);
maskModel.addCharacters('1');
expect(maskModel.value).toBe('1left');
expect(maskModel.selection).toEqual([1, 1]);
});
it('adds trailing postfix on paste of many digits inside empty textfield', () => {
const maskModel = new MaskModel(
{
selection: [0, 0],
value: '',
},
timeWithPostfixMask,
);
maskModel.addCharacters('123');
expect(maskModel.value).toBe('12:3left');
expect(maskModel.selection).toEqual(['12:3'.length, '12:3'.length]);
});
it('edits digit in the middle', () => {
const maskModel = new MaskModel(
{
selection: [1, 1],
value: '12:3left',
},
timeWithPostfixMask,
);
maskModel.addCharacters('4');
expect(maskModel.value).toBe('14:3left');
expect(maskModel.selection).toEqual([3, 3]);
});
it('erases the last digit without losing trailing fixed characters', () => {
const maskModel = new MaskModel(
{
selection: [3, 4],
value: '12:3left',
},
timeWithPostfixMask,
);
maskModel.deleteCharacters();
expect(maskModel.value).toBe('12left');
expect(maskModel.selection).toEqual([2, 2]);
});
it('erases the digit in the middle without losing trailing fixed character', () => {
const maskModel = new MaskModel(
{
selection: [3, 4],
value: '12:34 left',
},
timeWithPostfixMask,
);
maskModel.deleteCharacters();
expect(maskModel.value).toBe('12:4left');
expect(maskModel.selection).toEqual([3, 3]);
});
});
it('attempt to enter invalid character at the position of fixed character', () => {
const dateMask: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask: [/\d/, /\d/, '.', /\d/, /\d/],
};
const selection = [2, 2] as const;
const maskModel = new MaskModel(
{
selection,
value: '12',
},
dateMask,
);
expect(() => maskModel.addCharacters('#')).toThrow();
expect(maskModel.value).toBe('12');
expect(maskModel.selection).toEqual(selection);
});
it('accepts valid character at the position of fixed character', () => {
const dateMask: Required = {
...MASKITO_DEFAULT_OPTIONS,
mask: [/\d/, /\d/, ':', /\d/, /\d/],
};
const selection = [2, 2] as const;
const maskModel = new MaskModel(
{
selection,
value: '12',
},
dateMask,
);
maskModel.addCharacters(':');
expect(maskModel.value).toBe('12:');
expect(maskModel.selection).toEqual(['12:'.length, '12:'.length]);
});
});
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/apply-overwrite-mode.ts
================================================
import type {ElementState, MaskitoOptions} from '../../../types';
export function applyOverwriteMode(
{value, selection}: ElementState,
newCharacters: string,
mode: MaskitoOptions['overwriteMode'],
): ElementState {
const [from, to] = selection;
const computedMode = typeof mode === 'function' ? mode({value, selection}) : mode;
return {
value,
selection:
computedMode === 'replace'
? [from, Math.max(from + newCharacters.length, to)]
: [from, to],
};
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/calibrate-value-by-mask.ts
================================================
import type {ElementState, MaskitoMaskExpression} from '../../../types';
import {guessValidValueByPattern} from './guess-valid-value-by-pattern';
import {guessValidValueByRegExp} from './guess-valid-value-by-reg-exp';
import {validateValueWithMask} from './validate-value-with-mask';
export function calibrateValueByMask(
elementState: ElementState,
mask: MaskitoMaskExpression,
initialElementState: ElementState | null = null,
): ElementState {
if (validateValueWithMask(elementState.value, mask)) {
return elementState;
}
const {value, selection} = Array.isArray(mask)
? guessValidValueByPattern(elementState, mask, initialElementState)
: guessValidValueByRegExp(elementState, mask);
return {
selection,
value: Array.isArray(mask) ? value.slice(0, mask.length) : value,
};
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/get-leading-fixed-characters.ts
================================================
import type {ElementState} from '../../../types';
import {isFixedCharacter} from './is-fixed-character';
export function getLeadingFixedCharacters(
mask: Array,
validatedValuePart: string,
newCharacter: string,
initialElementState: ElementState | null,
): string {
let leadingFixedCharacters = '';
for (let i = validatedValuePart.length; i < mask.length; i++) {
const charConstraint = mask[i] || '';
const isInitiallyExisted = initialElementState?.value[i] === charConstraint;
if (
!isFixedCharacter(charConstraint) ||
(charConstraint === newCharacter && !isInitiallyExisted)
) {
return leadingFixedCharacters;
}
leadingFixedCharacters += charConstraint;
}
return leadingFixedCharacters;
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/guess-valid-value-by-pattern.ts
================================================
import type {ElementState} from '../../../types';
import {getLeadingFixedCharacters} from './get-leading-fixed-characters';
import {isFixedCharacter} from './is-fixed-character';
import {validateValueWithMask} from './validate-value-with-mask';
export function guessValidValueByPattern(
elementState: ElementState,
mask: Array,
initialElementState: ElementState | null,
): ElementState {
let maskedFrom: number | null = null;
let maskedTo: number | null = null;
const maskedValue = Array.from(elementState.value).reduce(
(validatedCharacters, char, charIndex) => {
const leadingCharacters = getLeadingFixedCharacters(
mask,
validatedCharacters,
char,
initialElementState,
);
const newValidatedChars = `${validatedCharacters}${leadingCharacters}`;
const charConstraint = mask[newValidatedChars.length] || '';
if (maskedFrom === null && charIndex >= elementState.selection[0]) {
maskedFrom = newValidatedChars.length;
}
if (maskedTo === null && charIndex >= elementState.selection[1]) {
maskedTo = newValidatedChars.length;
}
if (isFixedCharacter(charConstraint)) {
return `${newValidatedChars}${charConstraint}`;
}
if (char.match(charConstraint)) {
return `${newValidatedChars}${char}`;
}
return leadingCharacters.startsWith(char)
? newValidatedChars
: validatedCharacters;
},
'',
);
const trailingFixedCharacters = getLeadingFixedCharacters(
mask,
maskedValue,
'',
initialElementState,
);
return {
value: validateValueWithMask(`${maskedValue}${trailingFixedCharacters}`, mask)
? `${maskedValue}${trailingFixedCharacters}`
: maskedValue,
// issues: https://github.com/typescript-eslint/typescript-eslint/issues/12069
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
selection: [maskedFrom ?? maskedValue.length, maskedTo ?? maskedValue.length],
};
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/guess-valid-value-by-reg-exp.ts
================================================
import type {ElementState} from '../../../types';
export function guessValidValueByRegExp(
{value, selection}: ElementState,
maskRegExp: RegExp,
): ElementState {
const [from, to] = selection;
let newFrom = from;
let newTo = to;
const validatedValue = Array.from(value).reduce((validatedValuePart, char, i) => {
const newPossibleValue = `${validatedValuePart}${char}`;
if (from === i) {
newFrom = validatedValuePart.length;
}
if (to === i) {
newTo = validatedValuePart.length;
}
return newPossibleValue.match(maskRegExp) ? newPossibleValue : validatedValuePart;
}, '');
return {
value: validatedValue,
selection: [
Math.min(newFrom, validatedValue.length),
Math.min(newTo, validatedValue.length),
],
};
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/is-fixed-character.ts
================================================
export function isFixedCharacter(char: RegExp | string): char is string {
return typeof char === 'string';
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/remove-fixed-mask-characters.ts
================================================
import type {ElementState, MaskitoMaskExpression} from '../../../types';
import {isFixedCharacter} from './is-fixed-character';
export function removeFixedMaskCharacters(
initialElementState: ElementState,
mask: MaskitoMaskExpression,
): ElementState {
if (!Array.isArray(mask)) {
return initialElementState;
}
const [from, to] = initialElementState.selection;
const selection: number[] = [];
const unmaskedValue = Array.from(initialElementState.value).reduce(
(rawValue, char, i) => {
const charConstraint = mask[i] || '';
if (i === from) {
selection.push(rawValue.length);
}
if (i === to) {
selection.push(rawValue.length);
}
return isFixedCharacter(charConstraint) && charConstraint === char
? rawValue
: `${rawValue}${char}`;
},
'',
);
if (selection.length < 2) {
selection.push(
...Array.from({length: 2 - selection.length}).fill(
unmaskedValue.length,
),
);
}
return {
value: unmaskedValue,
selection: [selection[0]!, selection[1]!],
};
}
================================================
FILE: projects/core/src/lib/classes/mask-model/utils/validate-value-with-mask.ts
================================================
import type {MaskitoMaskExpression} from '../../../types';
import {isFixedCharacter} from './is-fixed-character';
export function validateValueWithMask(
value: string,
maskExpression: MaskitoMaskExpression,
): boolean {
if (Array.isArray(maskExpression)) {
return (
value.length === maskExpression.length &&
Array.from(value).every((char, i) => {
const charConstraint = maskExpression[i] || '';
return isFixedCharacter(charConstraint)
? char === charConstraint
: char.match(charConstraint);
})
);
}
return maskExpression.test(value);
}
================================================
FILE: projects/core/src/lib/constants/default-element-predicate.ts
================================================
import type {MaskitoElementPredicate} from '../types';
import {maskitoAdaptContentEditable} from '../utils/content-editable';
export const MASKITO_DEFAULT_ELEMENT_PREDICATE: MaskitoElementPredicate = (e) =>
e.isContentEditable
? maskitoAdaptContentEditable(e)
: e.querySelector('input,textarea') ||
(e as HTMLInputElement | HTMLTextAreaElement);
================================================
FILE: projects/core/src/lib/constants/default-options.ts
================================================
import type {MaskitoOptions} from '../types';
export const MASKITO_DEFAULT_OPTIONS: Required = {
mask: /^.*$/,
preprocessors: [],
postprocessors: [],
plugins: [],
overwriteMode: 'shift',
};
================================================
FILE: projects/core/src/lib/constants/index.ts
================================================
export * from './default-element-predicate';
export * from './default-options';
================================================
FILE: projects/core/src/lib/mask.ts
================================================
import {MaskHistory, MaskModel} from './classes';
import {MASKITO_DEFAULT_OPTIONS} from './constants';
import {createBrokenDefaultPlugin, createDoubleSpacePlugin} from './plugins';
import type {
ElementState,
MaskitoElement,
MaskitoOptions,
SelectionRange,
TypedInputEvent,
} from './types';
import {
areElementStatesEqual,
areElementValuesEqual,
EventListener,
getLineSelection,
getNotEmptySelection,
getWordSelection,
isRedo,
isUndo,
maskitoPipe,
maskitoTransform,
} from './utils';
const BUILT_IN_PLUGINS = [createDoubleSpacePlugin(), createBrokenDefaultPlugin()];
export class Maskito extends MaskHistory {
private readonly isTextArea = this.element.nodeName === 'TEXTAREA';
private readonly eventListener = new EventListener(this.element);
private readonly options: Required = {
...MASKITO_DEFAULT_OPTIONS,
...this.maskitoOptions,
};
private upcomingElementState: ElementState | null = null;
private readonly preprocessor = maskitoPipe(this.options.preprocessors);
private readonly postprocessor = maskitoPipe(this.options.postprocessors);
private readonly teardowns = this.options.plugins
.concat(BUILT_IN_PLUGINS)
.map((plugin) => plugin(this.element, this.options));
constructor(
private readonly element: MaskitoElement,
private readonly maskitoOptions: MaskitoOptions,
) {
super();
this.updateHistory(this.elementState);
this.eventListener.listen('keydown', (event) => {
if (isRedo(event)) {
event.preventDefault();
return this.redo();
}
if (isUndo(event)) {
event.preventDefault();
return this.undo();
}
});
this.eventListener.listen('beforeinput', (event) => {
const isForward = event.inputType.includes('Forward');
this.updateHistory(this.elementState);
switch (event.inputType) {
case 'deleteByCut':
case 'deleteContentBackward':
case 'deleteContentForward':
return this.handleDelete({
event,
isForward,
selection: getNotEmptySelection(this.elementState, isForward),
});
case 'deleteHardLineBackward':
case 'deleteHardLineForward':
case 'deleteSoftLineBackward':
case 'deleteSoftLineForward':
return this.handleDelete({
event,
isForward,
selection: getLineSelection(this.elementState, isForward),
force: true,
});
case 'deleteWordBackward':
case 'deleteWordForward':
return this.handleDelete({
event,
isForward,
selection: getWordSelection(this.elementState, isForward),
});
case 'historyRedo':
event.preventDefault();
return this.redo();
// historyUndo/historyRedo will not be triggered if value was modified programmatically
case 'historyUndo':
event.preventDefault();
return this.undo();
case 'insertCompositionText':
return; // will be handled inside `compositionend` event
case 'insertLineBreak':
case 'insertParagraph':
return this.handleEnter(event);
case 'insertReplacementText':
/**
* According {@link https://www.w3.org/TR/input-events-2 W3C specification}:
* > `insertReplacementText` – insert or replace existing text by means of a spell checker,
* > auto-correct, writing suggestions or similar.
* ___
* Firefox emits `insertReplacementText` event for its suggestion/autofill and for spell checker.
* However, it is impossible to detect which part of the textfield value is going to be replaced
* (`selectionStart` and `selectionEnd` just equal to the last caret position).
* ___
* Chrome does not fire `beforeinput` event for its suggestion/autofill.
* It emits only `input` event with `inputType` and `data` set to `undefined`.
* ___
* All these browser limitations make us to validate the result value later in `input` event.
*/
return;
case 'insertFromDrop':
case 'insertFromPaste':
case 'insertText':
default:
return this.handleInsert(
event,
event.data ??
// `event.data` for `contentEditable` is always `null` for paste/drop events
event.dataTransfer?.getData('text/plain') ??
'',
);
}
});
this.eventListener.listen(
'input',
() => {
if (
this.upcomingElementState &&
!areElementStatesEqual(this.upcomingElementState, this.elementState)
) {
this.updateElementState(this.upcomingElementState);
}
this.upcomingElementState = null;
},
{capture: true},
);
this.eventListener.listen('input', ({inputType}) => {
if (inputType === 'insertCompositionText') {
return; // will be handled inside `compositionend` event
}
this.ensureValueFitsMask();
this.updateHistory(this.elementState);
});
this.eventListener.listen('compositionend', () => {
this.ensureValueFitsMask();
this.updateHistory(this.elementState);
});
}
public destroy(): void {
this.eventListener.destroy();
this.teardowns.forEach((teardown) => teardown?.());
}
protected updateElementState(
{value, selection}: ElementState,
eventInit?: Pick,
): void {
const initialValue = this.elementState.value;
this.updateValue(value);
this.updateSelectionRange(selection);
if (eventInit && initialValue !== value) {
this.dispatchInputEvent(eventInit);
}
}
private get elementState(): ElementState {
const {value, selectionStart, selectionEnd} = this.element;
return {
value,
selection: [selectionStart ?? 0, selectionEnd ?? 0],
};
}
private get maxLength(): number {
const {maxLength} = this.element;
return maxLength === -1 ? Infinity : maxLength;
}
private updateSelectionRange([from, to]: SelectionRange): void {
const {element} = this;
if (
element.matches(':focus') &&
(element.selectionStart !== from || element.selectionEnd !== to)
) {
element.setSelectionRange(from, to);
}
}
private updateValue(value: string): void {
/**
* Don't "disturb" unnecessarily `value`-setter
* (i.e. it breaks React controlled input behavior)
*/
if (this.element.value !== value || this.element.isContentEditable) {
this.element.value = value;
}
}
private ensureValueFitsMask(): void {
this.updateElementState(maskitoTransform(this.elementState, this.options), {
inputType: 'insertText',
data: null,
});
}
private dispatchInputEvent(
eventInit: Pick = {
inputType: 'insertText',
data: null,
},
): void {
this.element.dispatchEvent(
new InputEvent('input', {
...eventInit,
bubbles: true,
cancelable: false,
}),
);
}
private handleDelete({
event,
selection,
isForward,
}: {
event: TypedInputEvent;
selection: SelectionRange;
isForward: boolean;
force?: boolean;
}): void {
const initialState: ElementState = {
value: this.elementState.value,
selection,
};
const {elementState} = this.preprocessor(
{
elementState: initialState,
data: '',
},
isForward ? 'deleteForward' : 'deleteBackward',
);
const maskModel = new MaskModel(elementState, this.options);
maskModel.deleteCharacters();
const newElementState = this.postprocessor(maskModel, initialState);
if (
areElementValuesEqual(initialState, elementState, maskModel, newElementState)
) {
const [from, to] = elementState.selection;
event.preventDefault();
// User presses Backspace/Delete for the fixed value
return this.updateSelectionRange(isForward ? [to, to] : [from, from]);
}
this.upcomingElementState = newElementState;
}
private handleInsert(event: TypedInputEvent, data: string): void {
const {options, maxLength, elementState: initialElementState} = this;
const [from, to] = initialElementState.selection;
const {elementState, data: insertedText = data} = this.preprocessor(
{
data,
elementState: initialElementState,
},
'insert',
);
const maskModel = new MaskModel(elementState, options);
try {
maskModel.addCharacters(insertedText);
} catch {
return event.preventDefault();
}
this.upcomingElementState = this.clampState(
this.postprocessor(maskModel, initialElementState),
);
/**
* When textfield value length is already equal to attribute `maxlength`,
* pressing any key (even with valid value) does not emit `input` event
* (except to the case when user replaces some characters by selection).
*/
const noInputEventDispatch =
initialElementState.value.length >= maxLength && from === to;
if (noInputEventDispatch) {
if (
options.overwriteMode === 'replace' &&
!areElementStatesEqual(this.upcomingElementState, initialElementState)
) {
this.dispatchInputEvent({inputType: 'insertText', data});
} else {
/**
* This `beforeinput` event will not be followed by `input` event –
* clear computed state to avoid any possible side effect
* for new possible `input` event without preceding `beforeinput` event
* (e.g. browser autofill, `document.execCommand('delete')` etc.)
*/
this.upcomingElementState = null;
}
}
}
private handleEnter(event: TypedInputEvent): void {
if (this.isTextArea || this.element.isContentEditable) {
this.handleInsert(event, '\n');
}
}
private clampState({value, selection}: ElementState): ElementState {
const [from, to] = selection;
const max = this.maxLength;
return {
value: value.slice(0, max),
selection: [Math.min(from, max), Math.min(to, max)],
};
}
}
================================================
FILE: projects/core/src/lib/plugins/broken-prevent-default.plugin.ts
================================================
import type {MaskitoPlugin} from '@maskito/core';
import type {TypedInputEvent} from '../types';
import {EventListener} from '../utils';
/**
* All `input` events with `inputType=deleteContentBackward` always follows `beforeinput` event with the same `inputType`.
* If `beforeinput[inputType=deleteContentBackward]` is prevented, subsequent `input[inputType=deleteContentBackward]` is prevented too.
* There is an exception – Android devices with Microsoft SwiftKey Keyboard in Mobile Chrome.
* These devices ignores `preventDefault` for `beforeinput` event if Backspace is pressed.
* @see https://github.com/taiga-family/maskito/issues/2135#issuecomment-2980729647
* ___
* TODO: track Chromium bug report and delete this plugin after bug fix
* https://issues.chromium.org/issues/40885402
*/
export function createBrokenDefaultPlugin(): MaskitoPlugin {
return (element) => {
const eventListener = new EventListener(element);
let isVirtualAndroidKeyboard = false;
let beforeinputEvent: TypedInputEvent;
let value = element.value;
eventListener.listen('keydown', ({key}) => {
isVirtualAndroidKeyboard = key === 'Unidentified';
});
eventListener.listen('beforeinput', (event) => {
beforeinputEvent = event;
value = element.value;
});
eventListener.listen(
'input',
(event) => {
if (
isVirtualAndroidKeyboard &&
beforeinputEvent.defaultPrevented &&
beforeinputEvent.inputType === 'deleteContentBackward' &&
event.inputType === 'deleteContentBackward'
) {
element.value = value;
}
},
{capture: true},
);
return () => eventListener.destroy();
};
}
================================================
FILE: projects/core/src/lib/plugins/change-event-plugin.ts
================================================
import type {MaskitoPlugin} from '../types';
export function maskitoChangeEventPlugin(): MaskitoPlugin {
return (element) => {
if (element.isContentEditable) {
return;
}
let value = element.value;
const valueListener = (): void => {
value = element.value;
};
const blurListener = (): void => {
if (element.value !== value) {
element.dispatchEvent(new Event('change', {bubbles: true}));
}
};
element.addEventListener('focus', valueListener);
element.addEventListener('change', valueListener);
element.addEventListener('blur', blurListener);
return () => {
element.removeEventListener('focus', valueListener);
element.removeEventListener('change', valueListener);
element.removeEventListener('blur', blurListener);
};
};
}
================================================
FILE: projects/core/src/lib/plugins/double-space.plugin.ts
================================================
import type {MaskitoPlugin} from '@maskito/core';
import type {TypedInputEvent} from '../types';
import {EventListener} from '../utils';
const SPACE = ' ';
/**
* 1. Android user (with G-board keyboard or similar) presses 1st space
* ```
* {type: "beforeinput", data: " ", inputType: "insertText"}
* ```
* 2. User presses 2nd space
* ```
* // Android tries to delete previously inserted space
* {type: "beforeinput", inputType: "deleteContentBackward"}
* {type: "beforeinput", data: ". ", inputType: "insertText"}
* ```
* ---------
* 1. MacOS user presses 1st space
* ```
* {type: "beforeinput", data: " ", inputType: "insertText"}
* ```
* 2. User presses 2nd space
* ```
* // MacOS automatically run `element.setSelectionRange(indexBeforeSpace, indexAfterSpace)` and then
* {type: "beforeinput", data: ". ", inputType: "insertText"}
* ```
* ---------
* @see https://github.com/taiga-family/maskito/issues/2023
*/
export function createDoubleSpacePlugin(): MaskitoPlugin {
let prevValue = '';
let prevCaretIndex = 0;
let prevEvent: TypedInputEvent | null = null;
let prevRejectedSpace = false;
return (element) => {
const eventListener = new EventListener(element);
eventListener.listen('beforeinput', (event) => {
const {value, selectionStart, selectionEnd} = element;
const rejectedSpace =
prevEvent?.inputType === 'insertText' &&
prevEvent.data === SPACE &&
!value.slice(0, Number(selectionEnd)).endsWith(SPACE);
if (event.inputType === 'insertText' && event.data === `.${SPACE}`) {
if (
prevEvent?.inputType === 'deleteContentBackward' &&
prevRejectedSpace
) {
// Android
element.value = prevValue;
element.setSelectionRange(prevCaretIndex, prevCaretIndex);
} else if (rejectedSpace) {
// Mac OS
element.setSelectionRange(selectionStart, selectionStart);
}
}
prevRejectedSpace = rejectedSpace;
prevEvent = event;
prevValue = value;
prevCaretIndex = Number(
(rejectedSpace ? prevCaretIndex : selectionEnd) === value.length
? selectionEnd
: selectionStart,
);
});
return () => eventListener.destroy();
};
}
================================================
FILE: projects/core/src/lib/plugins/index.ts
================================================
export * from './broken-prevent-default.plugin';
export * from './change-event-plugin';
export * from './double-space.plugin';
export * from './initial-calibration-plugin';
export * from './strict-composition-plugin';
================================================
FILE: projects/core/src/lib/plugins/initial-calibration-plugin.ts
================================================
import type {MaskitoOptions, MaskitoPlugin} from '../types';
import {maskitoTransform, maskitoUpdateElement} from '../utils';
export function maskitoInitialCalibrationPlugin(
customOptions?: MaskitoOptions,
): MaskitoPlugin {
return (element, options) => {
const from = element.selectionStart ?? 0;
const to = element.selectionEnd ?? 0;
maskitoUpdateElement(element, {
value: maskitoTransform(element.value, customOptions || options),
selection: [from, to],
});
};
}
================================================
FILE: projects/core/src/lib/plugins/strict-composition-plugin.ts
================================================
import type {ElementState, MaskitoPlugin, TypedInputEvent} from '../types';
import {areElementStatesEqual, maskitoTransform, maskitoUpdateElement} from '../utils';
export function maskitoStrictCompositionPlugin(): MaskitoPlugin {
return (element, maskitoOptions) => {
const listener = (event: TypedInputEvent): void => {
if (event.inputType !== 'insertCompositionText') {
return;
}
const selection = [
element.selectionStart ?? 0,
element.selectionEnd ?? 0,
] as const;
const elementState: ElementState = {
selection,
value: element.value,
};
const validatedState = maskitoTransform(elementState, maskitoOptions);
if (!areElementStatesEqual(elementState, validatedState)) {
event.preventDefault();
maskitoUpdateElement(element, validatedState);
}
};
element.addEventListener('input', listener as EventListener);
return () => element.removeEventListener('input', listener as EventListener);
};
}
================================================
FILE: projects/core/src/lib/types/element-predicate.ts
================================================
import type {MaskitoElement} from './maskito-element';
export type MaskitoElementPredicate = (
element: HTMLElement,
) => MaskitoElement | Promise;
================================================
FILE: projects/core/src/lib/types/element-state.ts
================================================
import type {SelectionRange} from './selection-range';
export interface ElementState {
readonly value: string;
readonly selection: SelectionRange;
}
================================================
FILE: projects/core/src/lib/types/index.ts
================================================
export * from './element-predicate';
export * from './element-state';
export * from './mask';
export * from './mask-options';
export * from './mask-processors';
export * from './maskito-element';
export * from './plugin';
export * from './selection-range';
export * from './typed-input-event';
================================================
FILE: projects/core/src/lib/types/mask-options.ts
================================================
import type {ElementState} from './element-state';
import type {MaskitoMask} from './mask';
import type {MaskitoPostprocessor, MaskitoPreprocessor} from './mask-processors';
import type {MaskitoPlugin} from './plugin';
export interface MaskitoOptions {
readonly mask: MaskitoMask;
readonly preprocessors?: readonly MaskitoPreprocessor[];
readonly postprocessors?: readonly MaskitoPostprocessor[];
readonly plugins?: readonly MaskitoPlugin[];
readonly overwriteMode?:
| 'replace'
| 'shift'
| ((elementState: ElementState) => 'replace' | 'shift');
}
================================================
FILE: projects/core/src/lib/types/mask-processors.ts
================================================
import type {ElementState} from './element-state';
export type MaskitoPreprocessor = (
_: {
elementState: ElementState;
data: string;
},
actionType: 'deleteBackward' | 'deleteForward' | 'insert' | 'validation',
) => {
elementState: ElementState;
data?: string;
};
export type MaskitoPostprocessor = (
elementState: ElementState,
initialElementState: ElementState,
) => ElementState;
================================================
FILE: projects/core/src/lib/types/mask.ts
================================================
import type {ElementState} from './element-state';
export type MaskitoMaskExpression = Array | RegExp;
export type MaskitoMask =
| MaskitoMaskExpression
| ((elementState: ElementState) => MaskitoMaskExpression);
================================================
FILE: projects/core/src/lib/types/maskito-element.ts
================================================
export type TextfieldLike = Pick<
HTMLInputElement,
| 'maxLength'
| 'select'
| 'selectionEnd'
| 'selectionStart'
| 'setSelectionRange'
| 'value'
>;
export type MaskitoElement = HTMLElement & TextfieldLike;
================================================
FILE: projects/core/src/lib/types/plugin.ts
================================================
import type {MaskitoOptions} from './mask-options';
import type {MaskitoElement} from './maskito-element';
export type MaskitoPlugin = (
element: MaskitoElement,
options: Required,
) => (() => void) | void;
================================================
FILE: projects/core/src/lib/types/selection-range.ts
================================================
export type SelectionRange = readonly [from: number, to: number];
================================================
FILE: projects/core/src/lib/types/typed-input-event.ts
================================================
export interface TypedInputEvent extends InputEvent {
inputType:
| 'deleteByCut' // Ctrl (Command) + X
| 'deleteContentBackward' // Backspace
| 'deleteContentForward' // Delete (Fn + Backspace)
| 'deleteHardLineBackward' // Ctrl (Command) + Backspace
| 'deleteHardLineForward'
| 'deleteSoftLineBackward' // Ctrl (Command) + Backspace
| 'deleteSoftLineForward'
| 'deleteWordBackward' // Alt (Option) + Backspace
| 'deleteWordForward' // Alt (Option) + Delete (Fn + Backspace)
| 'historyRedo' // Ctrl (Command) + Shift + Z
| 'historyUndo' // Ctrl (Command) + Z
| 'insertCompositionText'
| 'insertFromDrop'
| 'insertFromPaste' // Ctrl (Command) + V
| 'insertLineBreak'
| 'insertParagraph'
| 'insertReplacementText'
| 'insertText';
}
================================================
FILE: projects/core/src/lib/utils/content-editable.ts
================================================
import type {MaskitoElement, TextfieldLike} from '../types';
import {getContentEditableSelection} from './dom/get-content-editable-selection';
import {setContentEditableSelection} from './dom/set-content-editable-selection';
class ContentEditableAdapter implements TextfieldLike {
public maxLength = Infinity;
constructor(private readonly element: HTMLElement) {}
public get value(): string {
return this.element.innerText.replace(/\n\n$/, '\n');
}
public set value(value) {
// Setting into innerHTML of element with `white-space: pre;` style
this.element.innerHTML = value.replace(/\n$/, '\n\n');
}
public get selectionStart(): number | null {
return getContentEditableSelection(this.element)[0];
}
public get selectionEnd(): number | null {
return getContentEditableSelection(this.element)[1];
}
public setSelectionRange(from: number | null, to: number | null): void {
setContentEditableSelection(this.element, [from ?? 0, to ?? 0]);
}
public select(): void {
this.setSelectionRange(0, this.value.length);
}
}
export function maskitoAdaptContentEditable(element: HTMLElement): MaskitoElement {
const adapter = new ContentEditableAdapter(element);
return new Proxy(element, {
get(target, prop: keyof HTMLElement) {
if (prop in adapter) {
return adapter[prop as keyof ContentEditableAdapter];
}
const nativeProperty = target[prop];
return typeof nativeProperty === 'function'
? nativeProperty.bind(target)
: nativeProperty;
},
set(target, prop: keyof HTMLElement, val, receiver) {
return Reflect.set(prop in adapter ? adapter : target, prop, val, receiver);
},
}) as MaskitoElement;
}
================================================
FILE: projects/core/src/lib/utils/dom/event-listener.ts
================================================
import type {TypedInputEvent} from '../../types';
export class EventListener {
private readonly listeners: Array<() => void> = [];
constructor(private readonly element: HTMLElement) {}
public listen(
eventType: E,
fn: (
event: E extends 'beforeinput' | 'input'
? TypedInputEvent
: HTMLElementEventMap[E],
) => unknown,
options?: AddEventListenerOptions,
): void {
const untypedFn = fn as (event: HTMLElementEventMap[E]) => unknown;
this.element.addEventListener(eventType, untypedFn, options);
this.listeners.push(() =>
this.element.removeEventListener(eventType, untypedFn, options),
);
}
public destroy(): void {
this.listeners.forEach((stopListen) => stopListen());
}
}
================================================
FILE: projects/core/src/lib/utils/dom/get-content-editable-selection.ts
================================================
import type {SelectionRange} from '../../types';
export function getContentEditableSelection(element: HTMLElement): SelectionRange {
const {anchorOffset = 0, focusOffset = 0} =
element.ownerDocument.getSelection() || {};
const from = Math.min(anchorOffset, focusOffset);
const to = Math.max(anchorOffset, focusOffset);
return [from, to];
}
================================================
FILE: projects/core/src/lib/utils/dom/history-events.ts
================================================
import {HotkeyCode, HotkeyModifier, isHotkey} from './hotkey';
export function isRedo(event: KeyboardEvent): boolean {
return (
isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows
isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android
isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS
);
}
export function isUndo(event: KeyboardEvent): boolean {
return (
isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android
isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS
);
}
================================================
FILE: projects/core/src/lib/utils/dom/hotkey.ts
================================================
export const HotkeyModifier = {
CTRL: 1 << 0,
ALT: 1 << 1,
SHIFT: 1 << 2,
META: 1 << 3,
} as const;
// TODO add variants that can be processed correctly
export const HotkeyCode = {
Y: 89,
Z: 90,
} as const;
/**
* Checks if the passed keyboard event match the required hotkey.
*
* @example
* input.addEventListener('keydown', (event) => {
* if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) {
* // redo hotkey pressed
* }
* })
*
* @return will return `true` only if the {@link HotkeyCode} matches and only the necessary
* {@link HotkeyModifier modifiers} have been pressed
*/
export function isHotkey(
event: KeyboardEvent,
modifiers: (typeof HotkeyModifier)[keyof typeof HotkeyModifier],
hotkeyCode: (typeof HotkeyCode)[keyof typeof HotkeyCode],
): boolean {
return (
event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) &&
event.altKey === !!(modifiers & HotkeyModifier.ALT) &&
event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) &&
event.metaKey === !!(modifiers & HotkeyModifier.META) &&
/**
* We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more
* "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties.
* @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue}
*/
event.keyCode === hotkeyCode
);
}
================================================
FILE: projects/core/src/lib/utils/dom/set-content-editable-selection.ts
================================================
import type {SelectionRange} from '../../types';
export function setContentEditableSelection(
element: HTMLElement,
[from, to]: SelectionRange,
): void {
const document = element.ownerDocument;
const range = document.createRange();
range.setStart(
element.firstChild || element,
Math.min(from, element.textContent?.length ?? 0),
);
range.setEnd(
element.lastChild || element,
Math.min(to, element.textContent?.length ?? 0),
);
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}
================================================
FILE: projects/core/src/lib/utils/dom/update-element.ts
================================================
import type {ElementState, MaskitoElement} from '../../types';
/**
* Sets value to element, and dispatches input event
* if you passed ELementState, it also sets selection range
*
* @example
* maskitoUpdateElement(input, newValue);
* maskitoUpdateElement(input, elementState);
*
* @see {@link https://github.com/taiga-family/maskito/issues/804 issue}
*
* @return void
*/
export function maskitoUpdateElement(
element: MaskitoElement,
valueOrElementState: ElementState | string,
): void {
const initialValue = element.value;
if (typeof valueOrElementState === 'string') {
element.value = valueOrElementState;
} else {
const [from, to] = valueOrElementState.selection;
element.value = valueOrElementState.value;
if (element.matches(':focus')) {
element.setSelectionRange(from, to);
}
}
if (element.value !== initialValue) {
element.dispatchEvent(
new Event(
'input',
/**
* React handles this event only on bubbling phase
*
* here is the list of events that are processed in the capture stage, others are processed in the bubbling stage
* https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222
*/
{bubbles: true},
),
);
}
}
================================================
FILE: projects/core/src/lib/utils/element-states-equality.ts
================================================
import type {ElementState} from '../types';
export function areElementValuesEqual(
sampleState: ElementState,
...states: ElementState[]
): boolean {
return states.every(({value}) => value === sampleState.value);
}
export function areElementStatesEqual(
sampleState: ElementState,
...states: ElementState[]
): boolean {
return states.every(
({value, selection}) =>
value === sampleState.value &&
selection[0] === sampleState.selection[0] &&
selection[1] === sampleState.selection[1],
);
}
================================================
FILE: projects/core/src/lib/utils/get-line-selection.ts
================================================
import type {ElementState, SelectionRange} from '../types';
export function getLineSelection(
{value, selection}: ElementState,
isForward: boolean,
): SelectionRange {
const [from, to] = selection;
if (from !== to) {
return [from, to];
}
const nearestBreak = isForward
? value.slice(from).indexOf('\n') + 1 || value.length
: value.slice(0, to).lastIndexOf('\n') + 1;
const selectFrom = isForward ? from : nearestBreak;
const selectTo = isForward ? nearestBreak : to;
return [selectFrom, selectTo];
}
================================================
FILE: projects/core/src/lib/utils/get-not-empty-selection.ts
================================================
import type {ElementState, SelectionRange} from '../types';
export function getNotEmptySelection(
{value, selection}: ElementState,
isForward: boolean,
): SelectionRange {
const [from, to] = selection;
if (from !== to) {
return [from, to];
}
const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to];
return notEmptySelection.map((x) => Math.min(Math.max(x, 0), value.length)) as [
number,
number,
];
}
================================================
FILE: projects/core/src/lib/utils/get-word-selection.ts
================================================
import type {ElementState, SelectionRange} from '../types';
const TRAILING_SPACES_REG = /\s+$/g;
const LEADING_SPACES_REG = /^\s+/g;
const SPACE_REG = /\s/;
export function getWordSelection(
{value, selection}: ElementState,
isForward: boolean,
): SelectionRange {
const [from, to] = selection;
if (from !== to) {
return [from, to];
}
if (isForward) {
const valueAfterSelectionStart = value.slice(from);
const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [
'',
];
const nearestWordEndIndex = valueAfterSelectionStart
.trimStart()
.search(SPACE_REG);
return [
from,
nearestWordEndIndex === -1
? value.length
: from + leadingSpaces.length + nearestWordEndIndex,
];
}
const valueBeforeSelectionEnd = value.slice(0, to);
const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || [''];
const selectedWordLength = valueBeforeSelectionEnd
.trimEnd()
.split('')
.reverse()
.findIndex((char) => SPACE_REG.exec(char));
return [
selectedWordLength === -1 ? 0 : to - trailingSpaces.length - selectedWordLength,
to,
];
}
================================================
FILE: projects/core/src/lib/utils/index.ts
================================================
export * from './content-editable';
export * from './dom/event-listener';
export * from './dom/get-content-editable-selection';
export * from './dom/history-events';
export * from './dom/set-content-editable-selection';
export * from './dom/update-element';
export * from './element-states-equality';
export * from './get-line-selection';
export * from './get-not-empty-selection';
export * from './get-word-selection';
export * from './pipe';
export * from './transform';
================================================
FILE: projects/core/src/lib/utils/pipe.ts
================================================
import type {MaskitoPostprocessor, MaskitoPreprocessor} from '../types';
/**
* @internal
*/
export function maskitoPipe(
processors?: readonly MaskitoPreprocessor[],
): MaskitoPreprocessor;
/**
* @internal
*/
export function maskitoPipe(
processors?: readonly MaskitoPostprocessor[],
): MaskitoPostprocessor;
/**
* @internal
*/
export function maskitoPipe(
processors: ReadonlyArray<(state: State, ...args: Args) => Partial> = [],
): (state: State, ...args: Args) => State {
return (initialData: State, ...args: Args) =>
processors.reduce((data, fn) => ({...data, ...fn(data, ...args)}), initialData);
}
================================================
FILE: projects/core/src/lib/utils/test/get-not-empty-selection.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import type {ElementState} from '../../types';
import {getNotEmptySelection} from '../get-not-empty-selection';
describe('getNotEmptySelection', () => {
it('should return the same selection when selection positions are not equal', () => {
const elementStateStub: ElementState = {
value: 'testValue',
selection: [1, 3],
};
expect(getNotEmptySelection(elementStateStub, true)).toEqual(
elementStateStub.selection,
);
expect(getNotEmptySelection(elementStateStub, false)).toEqual(
elementStateStub.selection,
);
});
describe('backward direction', () => {
it('should decrease by one start position value', () => {
const elementStateStub: ElementState = {
value: 'testValue',
selection: [4, 4],
};
expect(getNotEmptySelection(elementStateStub, false)).toEqual([3, 4]);
});
it('should not change everything when start value is 0', () => {
const elementStateStub: ElementState = {
value: 'testValue',
selection: [0, 4],
};
expect(getNotEmptySelection(elementStateStub, false)).toEqual([0, 4]);
});
it('should decrease by one start value', () => {
const elementStateStub: ElementState = {
value: 'testValue',
selection: [1, 1],
};
expect(getNotEmptySelection(elementStateStub, false)).toEqual([0, 1]);
});
});
describe('forward direction', () => {
it('should increase by one end position, when value`s length more then end position', () => {
const elementStateStub: ElementState = {
value: 'testValue',
selection: [2, 2],
};
expect(getNotEmptySelection(elementStateStub, true)).toEqual([2, 3]);
});
it('should return value length as end position, when value`s length less or equal then end position', () => {
const elementStateStub: ElementState = {
value: 'sx',
selection: [4, 4],
};
expect(getNotEmptySelection(elementStateStub, true)).toEqual([2, 2]);
});
it('should increase by one end position, when value`s length equal end position increased by one', () => {
const elementStateStub: ElementState = {
value: 'test1',
selection: [4, 4],
};
expect(getNotEmptySelection(elementStateStub, true)).toEqual([4, 5]);
});
});
});
================================================
FILE: projects/core/src/lib/utils/test/get-word-selection.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import {getWordSelection} from '../get-word-selection';
describe('getWordSelection', () => {
describe('Backward', () => {
it('"1 23 456|" => select "456" => [5,8]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
8, // '1 23 456'.length
8,
],
},
false,
),
).toEqual([
5, // '1 23 '.length
8,
]);
});
it('"1 23 45|6" => select "45" => [5,7]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
7, // '1 23 45'.length
7,
],
},
false,
),
).toEqual([
5, // '1 23 '.length
7,
]);
});
it('"1 23 |456" => select "23 " => [2,5]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
5, // '1 23 '.length
5,
],
},
false,
),
).toEqual([
2, // '1 '.length
5,
]);
});
it('"1 23| 456" => select "23" => [2,4]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
4, // '1 23'.length
4,
],
},
false,
),
).toEqual([
2, // '1 '.length
4,
]);
});
it('"|1 23 456" => select "" => [0,0]', () => {
expect(
getWordSelection({value: '1 23 456', selection: [0, 0]}, false),
).toEqual([0, 0]);
});
it('"1 2 |3" (two spaces after 2) => select "2 " => [2,5]', () => {
expect(
getWordSelection(
{
value: '1 2 3',
selection: [
5, // space before 3
5,
],
},
false,
),
).toEqual([2, 5]);
});
});
describe('Forward', () => {
it('"1 23 456|" => select "" => [8,8]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
8, // '1 23 456'.length
8,
],
},
true,
),
).toEqual([8, 8]);
});
it('"1 23 45|6" => select "6" => [7,8]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
7, // '1 23 45'.length
7,
],
},
true,
),
).toEqual([7, 8]);
});
it('"1 23 |456" => select "456" => [5,8]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
5, // '1 23 '.length
5,
],
},
true,
),
).toEqual([5, 8]);
});
it('"1 23| 456" => select " 456" => [4,8]', () => {
expect(
getWordSelection(
{
value: '1 23 456',
selection: [
4, // '1 23'.length
4,
],
},
true,
),
).toEqual([4, 8]);
});
it('"1 |23 456" => select "23" => [2,4]', () => {
expect(
getWordSelection({value: '1 23 456', selection: [2, 2]}, true),
).toEqual([2, 4]);
});
it('"1| 23 456" => select " 23" => [1,4]', () => {
expect(
getWordSelection({value: '1 23 456', selection: [1, 1]}, true),
).toEqual([1, 4]);
});
it('"1 2| 3" (two spaces after 2) => select " 3" => [3,6]', () => {
expect(
getWordSelection(
{
value: '1 2 3',
selection: [3, 3],
},
true,
),
).toEqual([3, 6]);
});
});
});
================================================
FILE: projects/core/src/lib/utils/test/pipe.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import type {ElementState, MaskitoPostprocessor, MaskitoPreprocessor} from '../../types';
import {maskitoPipe} from '../pipe';
describe('maskitoPipe', () => {
describe('Preprocessor', () => {
const preprocessorData: Parameters[0] = {
elementState: {value: '2', selection: [2, 2]},
data: '0',
};
const add0ToValue: MaskitoPreprocessor = ({elementState}) => ({
elementState: {
...elementState,
value: `${elementState.value}0`,
},
});
const add1ToValue: MaskitoPreprocessor = ({elementState}) => ({
elementState: {
...elementState,
value: `${elementState.value}1`,
},
});
const add5ToData: MaskitoPreprocessor = ({elementState, data}) => ({
elementState,
data: `${data}5`,
});
const add3ToData: MaskitoPreprocessor = ({elementState, data}) => ({
elementState,
data: `${data}3`,
});
it('take the last valid `data` if the previous processor did not modify it', () => {
expect(
maskitoPipe([add3ToData, add0ToValue, add5ToData])(
preprocessorData,
'insert',
),
).toEqual({
elementState: {value: '20', selection: [2, 2]},
data: '035',
});
});
describe('Order matters', () => {
it('for `elementState`', () => {
expect(
maskitoPipe([add0ToValue, add1ToValue])(preprocessorData, 'insert'),
).toEqual({
elementState: {value: '201', selection: [2, 2]},
data: '0',
});
expect(
maskitoPipe([add1ToValue, add0ToValue])(preprocessorData, 'insert'),
).toEqual({
elementState: {value: '210', selection: [2, 2]},
data: '0',
});
});
it('for `data`', () => {
expect(
maskitoPipe([add3ToData, add5ToData])(preprocessorData, 'insert'),
).toEqual({
elementState: {value: '2', selection: [2, 2]},
data: '035',
});
expect(
maskitoPipe([add5ToData, add3ToData])(preprocessorData, 'insert'),
).toEqual({
elementState: {value: '2', selection: [2, 2]},
data: '053',
});
});
it('for `elementState` & `data` in one pipe', () => {
expect(
maskitoPipe([add5ToData, add0ToValue, add3ToData, add1ToValue])(
preprocessorData,
'insert',
),
).toEqual({
elementState: {value: '201', selection: [2, 2]},
data: '053',
});
});
});
});
describe('Postprocessor', () => {
const initialElementState: Parameters[1] = {
value: '',
selection: [0, 0],
};
const postprocessorData: Parameters[0] = {
value: '0',
selection: [5, 5],
};
const add3: MaskitoPostprocessor = ({value, selection}) => ({
selection,
value: `${value}3`,
});
const add5: MaskitoPostprocessor = ({value, selection}) => ({
selection,
value: `${value}5`,
});
const doubleCaretIndex: MaskitoPostprocessor = ({value, selection}) => ({
value,
selection: [selection[0] * 2, selection[1] * 2],
});
const shiftCaretIndexBy5: MaskitoPostprocessor = ({value, selection}) => ({
value,
selection: [selection[0] + 5, selection[1] + 5],
});
describe('Order matters', () => {
it('for `value`', () => {
expect(
maskitoPipe([add3, add5])(postprocessorData, initialElementState),
).toEqual({
value: '035',
selection: [5, 5],
});
expect(
maskitoPipe([add5, add3])(postprocessorData, initialElementState),
).toEqual({
value: '053',
selection: [5, 5],
});
});
it('for `selection`', () => {
expect(
maskitoPipe([doubleCaretIndex, shiftCaretIndexBy5])(
postprocessorData,
initialElementState,
),
).toEqual({
value: '0',
selection: [15, 15],
});
expect(
maskitoPipe([shiftCaretIndexBy5, doubleCaretIndex])(
postprocessorData,
initialElementState,
),
).toEqual({
value: '0',
selection: [20, 20],
});
});
it('for `value` & `selection` in one pipe', () => {
expect(
maskitoPipe([add5, doubleCaretIndex, add3, shiftCaretIndexBy5])(
postprocessorData,
initialElementState,
),
).toEqual({
value: '053',
selection: [15, 15],
});
});
});
});
describe('Readonly arguments are passed to all processors', () => {
const elementState: ElementState = {value: '', selection: [0, 0]};
it('preprocessor', () => {
const checkActionType: MaskitoPreprocessor = (data, actionType) => {
expect(actionType).toBe('deleteBackward');
return data;
};
maskitoPipe([checkActionType, checkActionType, checkActionType])(
{elementState, data: ''},
'deleteBackward',
);
});
it('postprocessor', () => {
const initialElementState: ElementState = {
value: '27',
selection: [2, 7],
};
const checkActionType: MaskitoPostprocessor = (data, actionType) => {
expect(actionType).toEqual(initialElementState);
return data;
};
maskitoPipe([checkActionType, checkActionType, checkActionType])(
elementState,
initialElementState,
);
});
});
});
================================================
FILE: projects/core/src/lib/utils/test/transform.spec.ts
================================================
import {describe, expect, it} from '@jest/globals';
import type {MaskitoOptions} from '../../types';
import {maskitoTransform} from '../transform';
describe('maskitoTransform', () => {
const numberOptions: MaskitoOptions = {
mask: /^\d+(,\d*)?$/,
preprocessors: [
({elementState, data}) => {
const {value, selection} = elementState;
return {
elementState: {
selection,
value: value.replace('.', ','),
},
data: data.replace('.', ','),
};
},
],
postprocessors: [
({value, selection}) => {
const newValue = value.replace(/^0+/, '0');
const deletedChars = value.length - newValue.length;
const [from, to] = selection;
return {
value: newValue,
selection: [from - deletedChars, to - deletedChars],
};
},
],
};
const usPhoneOptions: MaskitoOptions = {
mask: [
'+',
'1',
' ',
'(',
/\d/,
/\d/,
/\d/,
')',
' ',
/\d/,
/\d/,
/\d/,
'-',
/\d/,
/\d/,
/\d/,
/\d/,
],
};
describe('Basic API', () => {
it('returns string if the first argument is a string', () => {
expect(typeof maskitoTransform('100', numberOptions)).toBe('string');
});
it('returns ElementState if the first argument is a ElementState', () => {
const res = maskitoTransform({value: '', selection: [0, 0]}, numberOptions);
expect(res).toBeTruthy();
expect(typeof res).toBe('object');
});
it('formats value using preprocessor', () => {
expect(maskitoTransform('100.42', numberOptions)).toBe('100,42');
expect(
maskitoTransform(
{
value: '100.42',
selection: [2, 2],
},
numberOptions,
),
).toEqual({value: '100,42', selection: [2, 2]});
});
it('formats value using postprocessor', () => {
expect(maskitoTransform('0000,1234', numberOptions)).toBe('0,1234');
expect(
maskitoTransform(
{
value: '0000,1234',
selection: [6, 6],
},
numberOptions,
),
).toEqual({value: '0,1234', selection: [3, 3]});
});
it('drops invalid characters (mask expression works)', () => {
expect(maskitoTransform('42Taiga UI42', numberOptions)).toBe('4242');
expect(
maskitoTransform(
{
value: '42Taiga UI42',
selection: [11, 11],
},
numberOptions,
),
).toEqual({value: '4242', selection: [3, 3]});
});
});
describe('Drop / Browser autofill cases', () => {
it('`US` Phone mask | Drops "+1(21"', () => {
expect(maskitoTransform('+1(21', usPhoneOptions)).toBe('+1 (21');
});
});
});
================================================
FILE: projects/core/src/lib/utils/transform.ts
================================================
import {MaskModel} from '../classes';
import {MASKITO_DEFAULT_OPTIONS} from '../constants';
import type {ElementState, MaskitoOptions} from '../types';
import {maskitoPipe} from './pipe';
export function maskitoTransform(value: string, maskitoOptions: MaskitoOptions): string;
export function maskitoTransform(
state: ElementState,
maskitoOptions: MaskitoOptions,
): ElementState;
export function maskitoTransform(
valueOrState: ElementState | string,
maskitoOptions: MaskitoOptions,
): ElementState | string {
const options: Required = {
...MASKITO_DEFAULT_OPTIONS,
...maskitoOptions,
};
const preprocessor = maskitoPipe(options.preprocessors);
const postprocessor = maskitoPipe(options.postprocessors);
const initialElementState =
typeof valueOrState === 'string'
? {value: valueOrState, selection: [0, 0] as const}
: valueOrState;
const {elementState} = preprocessor(
{elementState: initialElementState, data: ''},
'validation',
);
const maskModel = new MaskModel(elementState, options);
const {value, selection} = postprocessor(maskModel, initialElementState);
return typeof valueOrState === 'string' ? value : {value, selection};
}
================================================
FILE: projects/demo/.gitignore
================================================
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
================================================
FILE: projects/demo/esbuild-plugins/maskito-as-taiga-ui-dep.plugin.js
================================================
const {resolve, join} = require('node:path');
const {existsSync} = require('node:fs');
/**
* Maskito repo uses Taiga UI to build demo application.
* Taiga UI uses Maskito packages as a dependency.
* Maskito <=> Taiga UI circular dependency.
* ESBuild is capable to use path aliases from tsconfig, to resolve local imports
* (e.g. source code of @maskito/kit imports @maskito/core utility).
* However, ESBuild ignores tsconfig paths when resolving packages inside node_modules.
*/
module.exports = {
name: 'maskito-as-taiga-ui-dep',
setup(build) {
build.onResolve({filter: /^@maskito/}, (args) => {
if (!args.importer.includes(join('node_modules', '@taiga-ui'))) {
// Ignore for local path aliases (ESBuild handles them properly)
return;
}
const library = args.path // e.g., '@maskito/kit'
.split('/')[1];
const entryPoint = resolve(
__dirname,
'../../../projects',
library,
'src/index.ts',
);
return existsSync(entryPoint)
? {
path: entryPoint,
}
: null;
});
},
};
================================================
FILE: projects/demo/esbuild-plugins/vue-esm.plugin.js
================================================
const {resolve} = require('node:path');
/**
* Otherwise, demo application logs warning:
* ```
* Component provided template option but runtime compilation is not supported in this build of Vue.
* Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js"`.
* ```
* Equivalent to webpack's alias: { vue$: 'vue/dist/vue.esm-bundler.js' }
*/
module.exports = {
name: 'vue-esm',
setup(build) {
build.onResolve({filter: /^vue$/}, () => ({
path: resolve(
build.initialOptions.absWorkingDir,
'./node_modules/vue/dist/vue.esm-bundler.js',
),
}));
},
};
================================================
FILE: projects/demo/jest.config.ts
================================================
export default {
displayName: 'demo',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['/src/test-setup.ts'],
coverageDirectory: '../../coverage/demo',
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};
================================================
FILE: projects/demo/package.json
================================================
{
"name": "@maskito/demo",
"private": true,
"devDependencies": {
"@angular/animations": "19.2.20",
"@angular/cdk": "19.2.19",
"@angular/common": "19.2.20",
"@angular/core": "19.2.20",
"@angular/forms": "19.2.20",
"@angular/platform-browser": "19.2.20",
"@angular/platform-browser-dynamic": "19.2.20",
"@angular/platform-server": "19.2.20",
"@angular/router": "19.2.20",
"@angular/ssr": "19.2.22",
"@maskito/angular": "*",
"@maskito/core": "*",
"@maskito/kit": "*",
"@ng-web-apis/common": "4.14.0",
"@ng-web-apis/universal": "4.14.0",
"@stackblitz/sdk": "1.11.0",
"@taiga-ui/addon-doc": "4.76.0",
"@taiga-ui/addon-mobile": "4.76.0",
"@taiga-ui/cdk": "4.76.0",
"@taiga-ui/core": "4.76.0",
"@taiga-ui/icons": "4.76.0",
"@taiga-ui/kit": "4.76.0",
"@taiga-ui/layout": "4.76.0",
"@taiga-ui/legacy": "4.76.0",
"@taiga-ui/styles": "4.76.0",
"@taiga-ui/testing": "4.76.0",
"browser-sync": "3.0.4",
"ngx-highlightjs": "10.0.0",
"react": "19.2.5",
"react-dom": "19.2.5",
"rxjs": "7.8.2",
"tslib": "2.8.1",
"vue": "3.5.32"
}
}
================================================
FILE: projects/demo/project.json
================================================
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"name": "demo",
"prefix": "app",
"projectType": "application",
"sourceRoot": "projects/demo/src",
"targets": {
"build": {
"configurations": {
"development": {
"extractLicenses": false,
"optimization": false,
"outputMode": "static",
"server": false,
"sourceMap": true,
"ssr": false
},
"production": {
"outputHashing": "all"
},
"typecheck": {
"tsConfig": "{projectRoot}/tsconfig.typecheck.json"
}
},
"defaultConfiguration": "production",
"executor": "@nx/angular:application",
"options": {
"allowedCommonJsDependencies": [
"react",
"react-dom/client",
"react/jsx-runtime",
"@vue/compiler-dom",
"@vue/runtime-dom",
"@vue/shared"
],
"assets": [
{
"glob": "**/*",
"input": "{projectRoot}/src/assets/",
"output": "./assets/"
},
{
"glob": "**/*",
"input": "node_modules/@taiga-ui/icons/src",
"output": "assets/taiga-ui/icons"
}
],
"baseHref": "/",
"browser": "{projectRoot}/src/main.ts",
"externalDependencies": ["react-hook-form"],
"index": "{projectRoot}/src/index.html",
"loader": {
".css": "text",
".html": "text",
".md": "text"
},
"outputMode": "server",
"outputPath": "dist/{projectName}",
"plugins": ["{projectRoot}/esbuild-plugins/maskito-as-taiga-ui-dep.plugin.js", "{projectRoot}/esbuild-plugins/vue-esm.plugin.js"],
"polyfills": ["zone.js"],
"server": "{projectRoot}/src/main.server.ts",
"ssr": {
"entry": "{projectRoot}/src/server.ts"
},
"styles": [
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
"node_modules/@taiga-ui/core/styles/taiga-ui-fonts.less",
"node_modules/@taiga-ui/styles/taiga-ui-global.less",
"{projectRoot}/src/styles.less"
],
"tsConfig": "{projectRoot}/tsconfig.app.json"
},
"outputs": ["{options.outputPath}"]
},
"build-gh-pages": {
"executor": "nx:run-commands",
"options": {
"commands": [
"echo 'Github pages require special 404.html'",
"echo 'Read more: https://angular.io/guide/deployment#deploy-to-github-pages'",
"echo ------",
"nx build {projectName} -c production",
"cp dist/{projectName}/browser/index.html dist/{projectName}/browser/404.html"
],
"parallel": false
}
},
"serve": {
"configurations": {
"development": {
"buildTarget": "{projectName}:build:development"
},
"production": {
"buildTarget": "{projectName}:build:production"
}
},
"continuous": true,
"defaultConfiguration": "development",
"executor": "@nx/angular:dev-server",
"options": {
"port": 3333,
"prebundle": {
"exclude": [
"@taiga-ui/addon-doc",
"@taiga-ui/addon-mobile",
"@taiga-ui/cdk",
"@taiga-ui/core",
"@taiga-ui/icons",
"@taiga-ui/kit",
"@taiga-ui/layout",
"@taiga-ui/legacy",
"@taiga-ui/styles"
]
}
}
},
"serve-ip": {
"executor": "nx:run-commands",
"options": {
"command": "nx serve {projectName} --open --host 0.0.0.0 --disable-host-check"
}
},
"serve-ssl": {
"executor": "nx:run-commands",
"options": {
"commands": [
"echo \"mkcert is a simple tool for making locally-trusted development certificates\"",
"echo \"Read about installation and more: https://github.com/FiloSottile/mkcert\"",
"echo ------",
"mkcert -install",
"mkdir -p .ssl",
"mkcert -key-file .ssl/localhost-key.pem -cert-file .ssl/localhost.pem localhost 127.0.0.1 ::1",
"nx serve --ssl"
],
"parallel": false
}
},
"serve-static": {
"continuous": true,
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "{projectName}:build",
"port": 3333,
"spa": true,
"staticFilePath": "dist/{projectName}/browser"
}
},
"test": {
"executor": "@nx/jest:jest",
"options": {
"jestConfig": "{projectRoot}/jest.config.ts",
"tsConfig": "tsconfig.spec.json"
},
"outputs": ["{workspaceRoot}/coverage/{projectName}"]
}
}
}
================================================
FILE: projects/demo/src/app/app.component.html
================================================
================================================
FILE: projects/demo/src/app/app.component.spec.ts
================================================
import {APP_BASE_HREF} from '@angular/common';
import {TestBed} from '@angular/core/testing';
import {RouterTestingHarness} from '@angular/router/testing';
import {DemoPath} from '@demo/constants';
import {beforeEach, describe, expect, it} from '@jest/globals';
import {App} from './app.component';
import {APP_CONFIG} from './app.config';
describe('Ensure unit tests work for demo application', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [App],
providers: [...APP_CONFIG.providers, {provide: APP_BASE_HREF, useValue: '/'}],
});
});
it('appComponent compiles properly', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Getting started');
expect(fixture.nativeElement.textContent).toContain('Core concepts');
});
it('router works', async () => {
const router = await RouterTestingHarness.create();
await router.navigateByUrl(DemoPath.CoreConceptsOverview);
expect(router.routeNativeElement?.textContent).toContain(
'The main entity of Maskito core library is Maskito class which accepts 2 arguments in constructor',
);
});
});
================================================
FILE: projects/demo/src/app/app.component.ts
================================================
import {ViewportScroller} from '@angular/common';
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {RouterLink} from '@angular/router';
import {DemoPath} from '@demo/constants';
import {ResizeObserverService} from '@ng-web-apis/resize-observer';
import {TUI_DOC_PAGE_LOADED, TuiDocMain} from '@taiga-ui/addon-doc';
import {tuiInjectElement} from '@taiga-ui/cdk';
import {TuiLink} from '@taiga-ui/core';
import {debounceTime, map, startWith} from 'rxjs';
@Component({
selector: 'app',
imports: [RouterLink, TuiDocMain, TuiLink],
templateUrl: './app.component.html',
styleUrl: './app.style.less',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
ResizeObserverService,
{
provide: TUI_DOC_PAGE_LOADED,
useFactory: () => {
const host = tuiInjectElement();
return inject(ResizeObserverService).pipe(
startWith(null),
debounceTime(0), // Synchronous scrollIntoView (after click) does not work https://stackoverflow.com/a/56971002
map(() => {
const exampleElements = Array.from(
host.querySelectorAll('tui-doc-example'),
);
const codeElements = Array.from(
host.querySelectorAll('tui-doc-code'),
);
return (
exampleElements.every((el) =>
el.querySelector('.t-example'),
) && codeElements.every((el) => el.querySelector('.t-code'))
);
}),
takeUntilDestroyed(),
);
},
},
],
})
export class App {
protected readonly stackblitzStarterPath = `/${DemoPath.Stackblitz}`;
constructor() {
inject(ViewportScroller).setOffset([0, 64]);
}
}
================================================
FILE: projects/demo/src/app/app.config.ts
================================================
import {isPlatformBrowser, LocationStrategy, PathLocationStrategy} from '@angular/common';
import {provideHttpClient} from '@angular/common/http';
import {type ApplicationConfig, inject, PLATFORM_ID} from '@angular/core';
import {provideAnimations} from '@angular/platform-browser/animations';
import {provideRouter, withInMemoryScrolling} from '@angular/router';
import {DocExamplePrimaryTab} from '@demo/constants';
import {
TUI_DOC_CODE_EDITOR,
TUI_DOC_DEFAULT_TABS,
TUI_DOC_EXAMPLE_CONTENT_PROCESSOR,
TUI_DOC_LOGO,
TUI_DOC_PAGES,
TUI_DOC_SOURCE_CODE,
TUI_DOC_TITLE,
TUI_DOC_TYPE_REFERENCE_HANDLER,
tuiDocExampleOptionsProvider,
type TuiDocSourceCodePathOptions,
} from '@taiga-ui/addon-doc';
import {NG_EVENT_PLUGINS} from '@taiga-ui/event-plugins';
import type {PolymorpheusContent} from '@taiga-ui/polymorpheus';
import {HIGHLIGHT_OPTIONS} from 'ngx-highlightjs';
import {DEMO_PAGES} from '../pages/pages';
import {StackblitzService} from '../pages/stackblitz';
import {ROUTES} from './app.routes';
import {
ANGULAR_LOGO,
JAVASCRIPT_LOGO,
REACT_LOGO,
} from './modules/example-primary-tabs-icons';
import {VUE_LOGO} from './modules/example-primary-tabs-icons/vue-logo.component';
import {LOGO_CONTENT} from './modules/logo/logo.component';
import {addDefaultTabsProcessor} from './utils';
export const APP_CONFIG: ApplicationConfig = {
providers: [
provideAnimations(),
provideRouter(
ROUTES,
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
}),
),
NG_EVENT_PLUGINS,
provideHttpClient(),
{
provide: LocationStrategy,
useClass: PathLocationStrategy,
},
{
provide: TUI_DOC_TITLE,
useValue: 'Maskito | ',
},
{
provide: TUI_DOC_LOGO,
useValue: LOGO_CONTENT,
},
{
provide: TUI_DOC_DEFAULT_TABS,
useValue: ['Description and examples', 'API'],
},
{
provide: TUI_DOC_PAGES,
useValue: DEMO_PAGES,
},
{
provide: TUI_DOC_SOURCE_CODE,
useValue: (context: TuiDocSourceCodePathOptions) => {
const link = 'https://github.com/taiga-family/maskito/tree/main/projects';
if (context.path) {
return `${link}/${context.path}`;
}
if (context.package.toLowerCase() !== 'kit') {
return null;
}
return `${link}/${context.package.toLowerCase()}/src/lib/masks/${`${context.header.slice(0, 1).toLowerCase()}${context.header.slice(1)}`.replaceAll(
/[A-Z]/g,
(m) => `-${m.toLowerCase()}`,
)}`;
},
},
{
provide: TUI_DOC_CODE_EDITOR,
useClass: StackblitzService,
},
{
provide: TUI_DOC_EXAMPLE_CONTENT_PROCESSOR,
useValue: addDefaultTabsProcessor,
},
tuiDocExampleOptionsProvider({
codeEditorVisibilityHandler: (files) => {
const fileNames = Object.keys(files);
return (
fileNames.includes(DocExamplePrimaryTab.MaskitoOptions) &&
fileNames.includes(DocExamplePrimaryTab.JavaScript)
);
},
tabTitles: new Map([
[DocExamplePrimaryTab.Angular, ANGULAR_LOGO],
[DocExamplePrimaryTab.JavaScript, JAVASCRIPT_LOGO],
[DocExamplePrimaryTab.React, REACT_LOGO],
[DocExamplePrimaryTab.Vue, VUE_LOGO],
]),
}),
{
provide: HIGHLIGHT_OPTIONS,
useFactory: () => {
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
return {
coreLibraryLoader: async () => import('highlight.js/lib/core'),
lineNumbersLoader: async () =>
// SSR ReferenceError: window is not defined
isBrowser
? import('ngx-highlightjs/line-numbers')
: Promise.resolve(),
languages: {
typescript: async () =>
import('highlight.js/lib/languages/typescript'),
less: async () => import('highlight.js/lib/languages/less'),
xml: async () => import('highlight.js/lib/languages/xml'),
},
};
},
},
{
provide: TUI_DOC_TYPE_REFERENCE_HANDLER,
useValue: (type: string) => {
if (type.toLowerCase().startsWith('maskito')) {
return `https://github.com/search?q=%2F%28enum%7Ctype%7Cinterface%7Cclass%7Cfunction%7Cconst%29+${type}%28%3C%7C%5Cs%29%2F+language%3ATypeScript+repo%3Ataiga-family%2Fmaskito+&type=code`;
}
switch (type) {
case 'Date':
return 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Date';
case 'MetadataJson':
return 'https://github.com/catamphetamine/libphonenumber-js?tab=readme-ov-file#min-vs-max-vs-mobile-vs-core';
default:
return null;
}
},
},
],
};
================================================
FILE: projects/demo/src/app/app.routes.ts
================================================
import type {Routes} from '@angular/router';
import {DemoPath} from '@demo/constants';
import {tuiProvideRoutePageTab} from '@taiga-ui/addon-doc';
/* eslint-disable @typescript-eslint/promise-function-async */
export const ROUTES: Routes = [
// Getting started
{
path: '',
loadComponent: () =>
import('../pages/documentation/what-is-maskito/what-is-maskito.component'),
title: 'What is Maskito?',
},
{
path: DemoPath.WhatIsMaskito,
loadComponent: () =>
import('../pages/documentation/what-is-maskito/what-is-maskito.component'),
title: 'What is Maskito?',
},
{
path: DemoPath.MaskitoLibraries,
loadComponent: () =>
import('../pages/documentation/maskito-libraries/maskito-libraries.component'),
title: 'Maskito libraries',
},
// Core concepts
{
path: DemoPath.CoreConceptsOverview,
loadComponent: () =>
import('../pages/documentation/core-concepts-overview/core-concepts-overview.component'),
title: 'Core concepts',
},
{
path: DemoPath.MaskExpression,
loadComponent: () =>
import('../pages/documentation/mask-expression/mask-expression.component'),
title: 'Mask expression',
},
{
path: DemoPath.ElementState,
loadComponent: () =>
import('../pages/documentation/element-state/element-state.component'),
title: 'Element state',
},
{
path: DemoPath.Processors,
loadComponent: () =>
import('../pages/documentation/processors/processors.component'),
title: 'Processors',
},
{
path: DemoPath.Plugins,
loadComponent: () => import('../pages/documentation/plugins/plugins.component'),
title: 'Plugins',
},
{
path: DemoPath.OverwriteMode,
loadComponent: () =>
import('../pages/documentation/overwrite-mode/overwrite-mode.component'),
title: 'Overwrite mode',
},
{
path: DemoPath.Transformer,
loadComponent: () =>
import('../pages/documentation/transformer/transformer.component'),
title: 'Transformer',
},
// Frameworks
{
path: DemoPath.Angular,
loadComponent: () => import('../pages/frameworks/angular/angular-doc.component'),
title: 'Angular',
},
{
path: DemoPath.React,
loadComponent: () => import('../pages/frameworks/react/react-doc.component'),
title: 'React',
},
{
path: DemoPath.Vue,
loadComponent: () => import('../pages/frameworks/vue/vue-doc.component'),
title: 'Vue',
},
// Kit
{
path: DemoPath.Number,
loadComponent: () => import('../pages/kit/number/number-mask-doc.component'),
title: 'Number',
},
{
path: DemoPath.Time,
loadComponent: () => import('../pages/kit/time/time-mask-doc.component'),
title: 'Time',
},
{
path: DemoPath.Date,
loadComponent: () => import('../pages/kit/date/date-mask-doc.component'),
title: 'Date',
},
{
path: DemoPath.DateRange,
loadComponent: () =>
import('../pages/kit/date-range/date-range-mask-doc.component'),
title: 'DateRange',
},
{
path: DemoPath.KitPlugins,
loadComponent: () => import('../pages/kit/plugins/kit-plugins-doc.component'),
title: 'Plugins | @maskito/kit',
},
// Recipes
{
path: DemoPath.DateTime,
loadComponent: () =>
import('../pages/kit/date-time/date-time-mask-doc.component'),
title: 'DateTime',
},
{
path: DemoPath.Card,
loadComponent: () => import('../pages/recipes/card/card-doc.component'),
title: 'Card',
},
{
path: DemoPath.Phone,
loadComponent: () => import('../pages/recipes/phone/phone-doc.component'),
title: 'Phone',
},
{
path: DemoPath.PhonePackage,
loadComponent: () => import('../pages/phone/phone-doc.component'),
title: 'Phone',
},
{
path: DemoPath.Textarea,
loadComponent: () => import('../pages/recipes/textarea/textarea-doc.component'),
title: 'Textarea',
},
{
path: DemoPath.ContentEditable,
loadComponent: () =>
import('../pages/recipes/content-editable/content-editable-doc.component'),
title: 'ContentEditable',
},
{
path: DemoPath.Prefix,
loadComponent: () => import('../pages/recipes/prefix/prefix-doc.component'),
title: 'With prefix',
},
{
path: DemoPath.Postfix,
loadComponent: () => import('../pages/recipes/postfix/postfix-doc.component'),
title: 'With postfix',
},
{
path: DemoPath.Placeholder,
loadComponent: () =>
import('../pages/recipes/placeholder/placeholder-doc.component'),
title: 'With placeholder',
},
{
path: DemoPath.NetworkAddress,
loadComponent: () =>
import('../pages/recipes/network-address/network-address-doc.component'),
title: 'Network address',
},
// Other
{
path: DemoPath.BrowserSupport,
loadComponent: () =>
import('../pages/documentation/browser-support/browser-support.component'),
title: 'Browser support',
},
{
path: DemoPath.SupportedInputTypes,
loadComponent: () =>
import('../pages/documentation/supported-input-types/supported-input-types.component'),
title: 'Supported types',
},
{
path: DemoPath.RealWorldForm,
loadComponent: () => import('../pages/documentation/real-world-form'),
title: 'Maskito in Real World Form',
},
{
path: DemoPath.Stackblitz,
loadComponent: () =>
import('../pages/stackblitz').then((m) => m.StackblitzStarterComponent),
title: 'Stackblitz Starter',
},
]
.map(tuiProvideRoutePageTab)
.concat({
path: '**',
redirectTo: DemoPath.WhatIsMaskito,
});
================================================
FILE: projects/demo/src/app/app.style.less
================================================
@import '@taiga-ui/core/styles/taiga-ui-local.less';
:host {
display: block;
font: var(--tui-font-text-m);
color: var(--tui-text-primary);
}
.link {
margin-inline-start: 1rem;
}
================================================
FILE: projects/demo/src/app/constants/demo-path.ts
================================================
export const DemoPath = {
WhatIsMaskito: 'getting-started/what-is-maskito',
MaskitoLibraries: 'getting-started/maskito-libraries',
CoreConceptsOverview: 'core-concepts/overview',
MaskExpression: 'core-concepts/mask-expression',
ElementState: 'core-concepts/element-state',
Processors: 'core-concepts/processors',
Plugins: 'core-concepts/plugins',
OverwriteMode: 'core-concepts/overwrite-mode',
Transformer: 'core-concepts/transformer',
Angular: 'frameworks/angular',
React: 'frameworks/react',
Vue: 'frameworks/vue',
Number: 'kit/number',
Time: 'kit/time',
Date: 'kit/date',
DateRange: 'kit/date-range',
DateTime: 'kit/date-time',
KitPlugins: 'kit/plugins',
PhonePackage: 'addons/phone',
Card: 'recipes/card',
Phone: 'recipes/phone',
Textarea: 'recipes/textarea',
ContentEditable: 'recipes/content-editable',
Prefix: 'recipes/prefix',
Postfix: 'recipes/postfix',
Placeholder: 'recipes/placeholder',
NetworkAddress: 'recipes/network-address',
BrowserSupport: 'browser-support',
SupportedInputTypes: 'supported-input-types',
RealWorldForm: 'real-world-form',
Stackblitz: 'stackblitz',
} as const;
================================================
FILE: projects/demo/src/app/constants/doc-example-primary-tab.ts
================================================
export const DocExamplePrimaryTab = {
MaskitoOptions: 'mask',
JavaScript: 'JavaScript',
Angular: 'Angular',
React: 'React',
Vue: 'Vue',
} as const;
================================================
FILE: projects/demo/src/app/constants/index.ts
================================================
export * from './demo-path';
export * from './doc-example-primary-tab';
================================================
FILE: projects/demo/src/app/modules/example-primary-tabs-icons/angular-logo.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@Component({
selector: 'angular-logo',
template: ' ',
styles: ['img {display: flex; width: 1.5rem}'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AngularLogoComponent {}
export const ANGULAR_LOGO = new PolymorpheusComponent(AngularLogoComponent);
================================================
FILE: projects/demo/src/app/modules/example-primary-tabs-icons/index.ts
================================================
export * from './angular-logo.component';
export * from './javascript-logo.component';
export * from './react-logo.component';
================================================
FILE: projects/demo/src/app/modules/example-primary-tabs-icons/javascript-logo.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@Component({
selector: 'javascript-logo',
template: ' ',
styles: ['img {display: flex; width: 1.5rem}'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JavaScriptLogoComponent {}
export const JAVASCRIPT_LOGO = new PolymorpheusComponent(JavaScriptLogoComponent);
================================================
FILE: projects/demo/src/app/modules/example-primary-tabs-icons/react-logo.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@Component({
selector: 'react-logo',
template: ' ',
styles: ['img {display: flex; width: 1.5rem}'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReactLogoComponent {}
export const REACT_LOGO = new PolymorpheusComponent(ReactLogoComponent);
================================================
FILE: projects/demo/src/app/modules/example-primary-tabs-icons/vue-logo.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@Component({
selector: 'vue-logo',
template: ' ',
styles: ['img {display: flex; width: 1.5rem}'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VueLogoComponent {}
export const VUE_LOGO = new PolymorpheusComponent(VueLogoComponent);
================================================
FILE: projects/demo/src/app/modules/logo/logo.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {RouterLink} from '@angular/router';
import {TuiLink} from '@taiga-ui/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
@Component({
selector: 'logo',
imports: [RouterLink, TuiLink],
templateUrl: './logo.template.html',
styleUrl: './logo.style.less',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LogoComponent {}
export const LOGO_CONTENT = new PolymorpheusComponent(LogoComponent);
================================================
FILE: projects/demo/src/app/modules/logo/logo.style.less
================================================
@import '@taiga-ui/core/styles/taiga-ui-local.less';
:host {
display: flex;
align-items: center;
@media @tui-mobile {
font-size: 0;
}
}
.logo-link {
display: flex;
}
.logo-name {
display: flex;
color: var(--tui-text-primary);
}
.logo {
margin-inline-end: 0.625rem;
}
.by {
margin-inline-start: 0.875rem;
@media @tui-mobile {
display: none;
}
}
================================================
FILE: projects/demo/src/app/modules/logo/logo.template.html
================================================
Maskito
================================================
FILE: projects/demo/src/app/server-error-handler.ts
================================================
import {type ErrorHandler, Injectable} from '@angular/core';
// TODO
const KNOWN_ISSUES: ReadonlyArray = [
/**
* ```
* // mask.ts
* export default {mask: '...'}
*
* // another-file.ts
* import('./mask.ts', {with: {loader: 'text'}})
* .then(x => x.default)
* ```
* During SERVER side rendering, `x.default` invalidly equals to `{mask: '...'}` object.
* During CLIENT side rendering, `x.default` correctly equals to raw file content.
*
* TODO(v6): no more relevant for Angular >= 20
*/
'Input data should be a String',
// Same here
/Cannot find (module|package) 'react-hook-form' imported from/, // TODO(v6): remove after Angular bump to >= 20
];
@Injectable()
export class ServerErrorHandler implements ErrorHandler {
public handleError(error: Error | string): void {
const errorMessage = (typeof error === 'string' ? error : error.message) || '';
if (
KNOWN_ISSUES.some((issue) =>
typeof issue === 'string'
? errorMessage.includes(issue)
: errorMessage.match(issue),
)
) {
return;
}
console.error(errorMessage);
if (
// Default environment variables for GitHub CI
process.env.CI // https://docs.github.com/en/actions/reference/workflows-and-actions/variables#default-environment-variables
) {
process.exit(1);
}
}
}
================================================
FILE: projects/demo/src/app/utils/add-default-tabs-processor/add-default-tabs-processor.ts
================================================
import {DocExamplePrimaryTab} from '@demo/constants';
import type {TuiHandler} from '@taiga-ui/cdk';
import {ANGULAR_DEFAULT_TAB} from './default-tabs/angular-default-tab';
import {JS_DEFAULT_TAB} from './default-tabs/js-default-tab';
import {REACT_DEFAULT_TAB} from './default-tabs/react-default-tab';
import {VUE_DEFAULT_TAB} from './default-tabs/vue-default-tab';
export const addDefaultTabsProcessor: TuiHandler<
Record,
Record
> = (files) => {
const fileNames = Object.keys(files);
return fileNames.length === 1 && fileNames[0] === DocExamplePrimaryTab.MaskitoOptions
? {
...files,
[DocExamplePrimaryTab.JavaScript]: JS_DEFAULT_TAB,
[DocExamplePrimaryTab.Angular]: ANGULAR_DEFAULT_TAB,
[DocExamplePrimaryTab.React]: REACT_DEFAULT_TAB,
[DocExamplePrimaryTab.Vue]: VUE_DEFAULT_TAB,
}
: files;
};
================================================
FILE: projects/demo/src/app/utils/add-default-tabs-processor/default-tabs/angular-default-tab.ts
================================================
export const ANGULAR_DEFAULT_TAB = `import {Component} from '@angular/core';
import {MaskitoDirective} from '@maskito/angular';
import type {MaskitoOptions} from '@maskito/core';
import mask from './mask';
@Component({
selector: 'my-app',
imports: [MaskitoDirective],
template: ' ',
})
export class App {
readonly options: MaskitoOptions = mask;
}`;
================================================
FILE: projects/demo/src/app/utils/add-default-tabs-processor/default-tabs/js-default-tab.ts
================================================
export const JS_DEFAULT_TAB = `import {Maskito, MaskitoOptions} from '@maskito/core';
import maskitoOptions from './mask';
const element = document.querySelector('input,textarea')!;
const maskedInput = new Maskito(element, maskitoOptions);
// Call this function when the element is detached from DOM
maskedInput.destroy();`;
================================================
FILE: projects/demo/src/app/utils/add-default-tabs-processor/default-tabs/react-default-tab.ts
================================================
export const REACT_DEFAULT_TAB = `import * as React from 'react';
import {useMaskito} from '@maskito/react';
import options from './mask';
export default function App() {
const maskedInputRef = useMaskito({options});
return ;
}`;
================================================
FILE: projects/demo/src/app/utils/add-default-tabs-processor/default-tabs/vue-default-tab.ts
================================================
export const VUE_DEFAULT_TAB = `import {createApp} from 'vue';
import {maskito} from '@maskito/vue';
import options from './mask';
const app = createApp({
template: ' ',
directives: {maskito},
data: () => ({ options }),
});`;
================================================
FILE: projects/demo/src/app/utils/index.ts
================================================
export * from './add-default-tabs-processor/add-default-tabs-processor';
================================================
FILE: projects/demo/src/assets/manifest.webmanifest
================================================
{
"icons": [
{"src": "/favicon-192.png", "type": "image/png", "sizes": "192x192"},
{"src": "/favicon-512.png", "type": "image/png", "sizes": "512x512"}
]
}
================================================
FILE: projects/demo/src/environments/environment.prod.ts
================================================
export const environment = {production: true};
================================================
FILE: projects/demo/src/environments/environment.ts
================================================
export const environment = {production: false};
================================================
FILE: projects/demo/src/index.html
================================================
Maskito
Loading...
================================================
FILE: projects/demo/src/main.server.ts
================================================
import {type ApplicationRef, ErrorHandler, mergeApplicationConfig} from '@angular/core';
import {bootstrapApplication, type BootstrapContext} from '@angular/platform-browser';
import {provideServerRendering} from '@angular/platform-server';
import {provideServerRouting, RenderMode, type ServerRoute} from '@angular/ssr';
import {DemoPath} from '@demo/constants';
import {UNIVERSAL_PROVIDERS} from '@ng-web-apis/universal';
import {App} from './app/app.component';
import {APP_CONFIG} from './app/app.config';
import {ROUTES} from './app/app.routes';
import {ServerErrorHandler} from './app/server-error-handler';
/* eslint-disable @typescript-eslint/require-await */
const serverConfig = mergeApplicationConfig(APP_CONFIG, {
providers: [
provideServerRendering(),
provideServerRouting(
ROUTES.map((route) => {
const path = route.path ?? '';
switch (path) {
case DemoPath.Angular:
return withTabs(path, ['Setup']);
case DemoPath.Number:
return withTabs(path, ['API', 'Helpers']);
case DemoPath.Plugins:
return withTabs(path, ['Built-in_core_plugins']);
default:
return path.startsWith('kit') || path.startsWith('addons')
? withTabs(path, ['API'])
: {
path,
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
return [];
},
};
}
}),
),
UNIVERSAL_PROVIDERS,
{provide: ErrorHandler, useClass: ServerErrorHandler},
],
});
function withTabs(path: string, tabs: string[]): ServerRoute {
return {
path: `${path}/:tab`,
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
return tabs.map((tab) => ({tab}));
},
};
}
export default async (context: BootstrapContext): Promise =>
bootstrapApplication(App, serverConfig, context);
================================================
FILE: projects/demo/src/main.ts
================================================
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app/app.component';
import {APP_CONFIG} from './app/app.config';
bootstrapApplication(App, APP_CONFIG).catch((err: unknown) => console.error(err));
================================================
FILE: projects/demo/src/pages/documentation/browser-support/browser-support.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {TuiAddonDoc} from '@taiga-ui/addon-doc';
@Component({
selector: 'browser-support',
imports: [TuiAddonDoc],
templateUrl: './browser-support.template.html',
styles: ['td {width: 18.75rem}'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class BrowserSupportComponent {
protected readonly desktopBrowsers = [
{name: 'Google Chrome', version: '88+'},
{name: 'Mozilla Firefox', version: '120+'},
{name: 'Safari', version: '14.1+'},
{name: 'Opera', version: '74+'},
{name: 'Edge', version: '88+'},
{name: 'Yandex Browser', version: '21.2+'},
{name: 'Microsoft Internet Explorer', version: null},
] as const;
protected readonly mobileBrowsers = [
{name: 'Google Chrome', version: '88+'},
{name: 'Mozilla Firefox', version: '120+'},
{name: 'Safari', version: '14.5+'},
{name: 'Opera', version: '63+'},
{name: 'Samsung Mobile', version: '15+'},
{name: 'Yandex Browser', version: '21.2+'},
];
}
================================================
FILE: projects/demo/src/pages/documentation/browser-support/browser-support.template.html
================================================
Desktop
Browser
Version
@for (browser of desktopBrowsers; track browser) {
{{ browser.name }}
@if (browser.version) {
{{ browser.version }}
} @else {
Not supported
}
}
Mobile
Browser
Version
@for (browser of mobileBrowsers; track browser) {
{{ browser.name }}
{{ browser.version }}
}
================================================
FILE: projects/demo/src/pages/documentation/core-concepts-overview/core-concepts-overview.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {RouterLink} from '@angular/router';
import {DemoPath} from '@demo/constants';
import {TuiAddonDoc} from '@taiga-ui/addon-doc';
import {TuiIcon, TuiLink, TuiNotification, TuiSurface, TuiTitle} from '@taiga-ui/core';
import {TuiTooltip} from '@taiga-ui/kit';
import {TuiCardLarge, TuiHeader} from '@taiga-ui/layout';
@Component({
selector: 'core-concepts-overview-doc-page',
imports: [
RouterLink,
TuiAddonDoc,
TuiCardLarge,
TuiHeader,
TuiIcon,
TuiLink,
TuiNotification,
TuiSurface,
TuiTitle,
TuiTooltip,
],
templateUrl: './core-concepts-overview.template.html',
styleUrl: './core-concepts-overview.styles.less',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class CoreConceptsOverviewDocPageComponent {
protected readonly maskitoPublicApiDemo =
import('./examples/maskito-public-api-demo.md');
protected readonly maskExpressionDocPage = `/${DemoPath.MaskExpression}`;
protected readonly processorsDocPage = `/${DemoPath.Processors}`;
protected readonly pluginsDocPage = `/${DemoPath.Plugins}`;
protected readonly overwriteModeDocPage = `/${DemoPath.OverwriteMode}`;
protected readonly transformerDocPage = `/${DemoPath.Transformer}`;
protected readonly supportedInputTypesDocPage = `/${DemoPath.SupportedInputTypes}`;
}
================================================
FILE: projects/demo/src/pages/documentation/core-concepts-overview/core-concepts-overview.styles.less
================================================
@import '@taiga-ui/core/styles/taiga-ui-local.less';
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
@media @tui-mobile {
flex-direction: column;
}
& [tuiCardLarge] {
flex: 1;
min-inline-size: 14rem;
@media @tui-desktop-min {
max-inline-size: 40%;
}
}
}
================================================
FILE: projects/demo/src/pages/documentation/core-concepts-overview/core-concepts-overview.template.html
================================================
The main entity of Maskito core library is
Maskito
class which accepts 2 arguments in constructor:
native
HTMLInputElement
Maskito
supports only limited types of
HTMLInputElement
due to some browser limitations!
See a full list of supported types
or
HTMLTextAreaElement
set of configurable
MaskitoOptions
Avoid wasting computation power or memory resources!
The only available public method
destroy
removes all created event listeners. Call it to clean everything up when the work is finished.
To understand the capabilities of the Maskito library, you need to learn about the following features and
concepts:
================================================
FILE: projects/demo/src/pages/documentation/core-concepts-overview/examples/maskito-public-api-demo.md
================================================
```ts
import {Maskito, maskitoInitialCalibrationPlugin} from '@maskito/core';
const maskedInput = new Maskito(element, {
mask: /^\d+$/,
preprocessors: [preprocessor1, preprocessor2],
postprocessors: [
({value, selection}) => {
// ...
},
],
plugins: [myCustomPlugin, maskitoInitialCalibrationPlugin()],
overwriteMode: 'shift',
});
// Call it when the element is destroyed
maskedInput.destroy();
```
================================================
FILE: projects/demo/src/pages/documentation/element-state/element-state.component.ts
================================================
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {RouterLink} from '@angular/router';
import {DemoPath} from '@demo/constants';
import {TuiAddonDoc} from '@taiga-ui/addon-doc';
import {TuiLink} from '@taiga-ui/core';
@Component({
selector: 'element-state-doc-page',
imports: [RouterLink, TuiAddonDoc, TuiLink],
templateUrl: './element-state.template.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class ElementStateDocPageComponent {
protected readonly elementStateDemo = import('./examples/element-state-demo.md');
protected readonly maskExpressionDocPage = `/${DemoPath.MaskExpression}`;
protected readonly processorsDocPage = `/${DemoPath.Processors}`;
protected readonly overwriteModeDocPage = `/${DemoPath.OverwriteMode}`;
}
================================================
FILE: projects/demo/src/pages/documentation/element-state/element-state.template.html
================================================
Element state
is a concept which describes the main properties of the masked element at the certain period of time.
It is an object which implements the following interface:
This concept is actively used throughout
Maskito
libraries, and you can find its usage in the following topics:
================================================
FILE: projects/demo/src/pages/documentation/element-state/examples/element-state-demo.md
================================================
```ts
interface ElementState {
// the value of a masked or