From b9499d4cdd32ab2b5081d9aa778cff501c8f3147 Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 30 Sep 2026 14:00:48 +0200 Subject: [PATCH] Make `InputMask` native, without IMask --- .changeset/native-input-mask.md | 7 + core/js/src/input-mask.ts | 262 ++++++++++++++++++++-- core/js/tests/unit/input-mask.spec.ts | 286 +++++++++++++++++++------ docs/content/ui/plugins/input-mask.mdx | 94 +++++--- preview/pages/form-elements.astro | 4 +- shared/ui/form/InputMask.astro | 6 +- 6 files changed, 543 insertions(+), 116 deletions(-) create mode 100644 .changeset/native-input-mask.md diff --git a/.changeset/native-input-mask.md b/.changeset/native-input-mask.md new file mode 100644 index 000000000..2d0d4e384 --- /dev/null +++ b/.changeset/native-input-mask.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/docs": patch +"@tabler/preview": patch +--- + +`InputMask` no longer needs IMask. It formats `data-mask` patterns natively, with the same `0`, `a` and `*` characters, and adds the `tokens` and `placeholderChar` options, a function as `mask`, the `unmaskedValue` and `isComplete` properties and the `accept.bs.input-mask` and `complete.bs.input-mask` events. IMask object masks (`Number`, `Date`, a RegExp) still work when IMask is loaded, but are deprecated. diff --git a/core/js/src/input-mask.ts b/core/js/src/input-mask.ts index c7edbf019..8af0df137 100644 --- a/core/js/src/input-mask.ts +++ b/core/js/src/input-mask.ts @@ -6,10 +6,13 @@ */ import BaseComponent from './bootstrap/base-component' +import EventHandler from './bootstrap/dom/event-handler' import { initAll } from './bootstrap/util/component-functions' import type { ElementSelector } from './bootstrap/types' -// The part of an IMask instance the component uses +type MaskFunction = (unmaskedValue: string) => string + +// The part of an IMask instance the legacy fallback uses interface IMaskInstance { value: string unmaskedValue: string @@ -19,9 +22,14 @@ interface IMaskInstance { } type ComponentConfig = { - /** an IMask mask: a pattern string, `Number`, `Date`, a RegExp or a mask object */ - mask: unknown + /** a pattern string, or a function that returns one for the current unmasked value */ + mask: string | MaskFunction + /** show the mask only as far as the user has typed */ lazy: boolean + /** the character that stands for an empty slot when `lazy` is `false` */ + placeholderChar: string + /** extra pattern characters, each with the RegExp one typed character has to match */ + tokens: Record } type ComponentConfigInput = Partial & Record @@ -31,43 +39,96 @@ type ComponentConfigInput = Partial & Record */ const NAME = 'input-mask' +const DATA_KEY = `bs.${NAME}` +const EVENT_KEY = `.${DATA_KEY}` + +const EVENT_ACCEPT = `accept${EVENT_KEY}` +const EVENT_COMPLETE = `complete${EVENT_KEY}` const ATTRIBUTE_MASK = 'data-mask' const ATTRIBUTE_VISIBLE = 'data-mask-visible' +const ATTRIBUTE_PLACEHOLDER_CHAR = 'data-mask-placeholder-char' const SELECTOR_DATA_MASK = `[${ATTRIBUTE_MASK}]` +const ESCAPE = '\\' + +const TOKENS: Record = { + '0': /\d/, + 'a': /\p{L}/u, + '*': /./, +} + const Default: ComponentConfig = { mask: '', lazy: true, + placeholderChar: '_', + tokens: {}, } const DefaultType: Record = { mask: '(string|function|regexp|object|array)', lazy: 'boolean', + placeholderChar: 'string', + tokens: 'object', +} + +const isLegacyMask = (mask: unknown): boolean => typeof mask !== 'string' && (typeof mask !== 'function' || mask === Number || mask === Date) + +type Slot = { token: RegExp | null; char: string } + +// `\0` makes a literal of a token character; every other character outside the token map is a literal +const parse = (mask: string, tokens: Record): Slot[] => { + const slots: Slot[] = [] + + for (let i = 0; i < mask.length; i++) { + const char = mask[i] + + if (char === ESCAPE && i + 1 < mask.length) { + slots.push({ token: null, char: mask[++i] }) + } else { + slots.push({ token: tokens[char] ?? null, char }) + } + } + + return slots } /** * Class definition * - * Wraps the IMask plugin (https://imask.js.org), loaded separately as - * `window.IMask`. Without the plugin the component is inert. + * A native input mask. Pattern characters: `0` a digit, `a` a letter, `*` any + * character; `\` makes the next one a literal. Add more with the `tokens` option. */ class InputMask extends BaseComponent { declare _element: HTMLInputElement declare _config: ComponentConfig - _mask: IMaskInstance | null = null + _legacy: IMaskInstance | null = null + _unmasked = '' + _complete = false + _onInput = (event: Event): void => this._handleInput(event as InputEvent) constructor(element: ElementSelector, config?: ComponentConfigInput) { super(element, config) - if (!this._element || !this._config.mask || !window.IMask) { + if (!this._element || !this._config.mask) { return } - // Every option from the config reaches IMask, not only the two typed ones. - this._mask = new window.IMask(this._element, { ...this._config }) + // deprecated(2.0): IMask object masks (`Number`, `Date`, a RegExp) stay on the IMask library when it is loaded + if (isLegacyMask(this._config.mask)) { + if (window.IMask) { + this._legacy = new window.IMask(this._element, { ...this._config }) + } else { + console.warn('Tabler InputMask: only string and function masks are supported without IMask.') + } + + return + } + + this._element.addEventListener('input', this._onInput) + this.update() } // Getters @@ -83,32 +144,203 @@ class InputMask extends BaseComponent { return NAME } - /** The IMask instance, for options the component does not expose. */ - get mask(): IMaskInstance | null { - return this._mask + /** The value as shown in the input, with the literals. */ + get value(): string { + return this._legacy ? this._legacy.value : this._element.value + } + + /** Only what the user typed, without the literals and the placeholders. */ + get unmaskedValue(): string { + return this._legacy ? this._legacy.unmaskedValue : this._unmasked + } + + /** `true` when every slot of the mask is filled. */ + get isComplete(): boolean { + return this._complete + } + + // deprecated(2.0): the IMask instance; the component itself answers the same members + get mask(): this | IMaskInstance | null { + return this._legacy ?? (this._config.mask ? this : null) } // Public + /** Re-applies the mask after the value was changed from code. */ update(): void { - this._mask?.updateValue() + if (this._legacy) { + this._legacy.updateValue() + return + } + + this._unmasked = this._extract(this._element.value).unmasked + this._render(this._unmasked, null) + } + + // deprecated(2.0): IMask method names + updateValue(): void { + this.update() + } + + destroy(): void { + this.dispose() } dispose(): void { - this._mask?.destroy() + this._legacy?.destroy() + this._element.removeEventListener('input', this._onInput) super.dispose() } // Private + _slots(unmasked: string): Slot[] { + const { mask, tokens } = this._config + + return parse(typeof mask === 'function' ? mask(unmasked) : mask, { ...TOKENS, ...tokens }) + } + + _extract(raw: string, caret: number | null = null): { unmasked: string; count: number | null } { + // A dynamic mask is resolved by what the value holds, so the first pass reads it with the previous mask + let slots = this._slots(this._unmasked) + let unmasked = '' + let count: number | null = null + let position = 0 + + for (let i = 0; i <= raw.length; i++) { + if (i === caret) { + count = unmasked.length + } + + const char = raw[i] + + if (char === undefined) { + break + } + + while (position < slots.length && !slots[position].token && slots[position].char !== char) { + position++ + } + + if (position >= slots.length) { + break + } + + const { token } = slots[position] + + if (!token) { + position++ + continue + } + + if (char !== this._config.placeholderChar && token.test(char)) { + unmasked += char + position++ + slots = this._slots(unmasked) + } + } + + return { unmasked, count } + } + + _format(unmasked: string): { text: string; ends: number[]; complete: boolean } { + const slots = this._slots(unmasked) + const { lazy, placeholderChar } = this._config + const ends: number[] = [] + let text = '' + let used = 0 + let complete = true + + for (const slot of slots) { + if (!slot.token) { + if (!lazy || used < unmasked.length) { + text += slot.char + } + + continue + } + + if (used < unmasked.length) { + text += unmasked[used++] + ends.push(text.length) + } else { + complete = false + + if (lazy) { + break + } + + text += placeholderChar + } + } + + return { text, ends, complete: complete && slots.some((slot) => slot.token) } + } + + _render(unmasked: string, typed: number | null): void { + const { text, ends, complete } = this._format(unmasked) + const changed = text !== this._element.value + + if (changed) { + this._element.value = text + } + + if (typed !== null && document.activeElement === this._element) { + const caret = typed === 0 ? 0 : (ends[Math.min(typed, ends.length) - 1] ?? text.length) + this._element.setSelectionRange(caret, caret) + } + + if (complete !== this._complete) { + this._complete = complete + + if (complete) { + EventHandler.trigger(this._element, EVENT_COMPLETE, { value: text, unmaskedValue: unmasked }) + } + } + } + + _handleInput(event: InputEvent): void { + if (event.isComposing) { + return + } + + const raw = this._element.value + const caret = this._element.selectionStart ?? raw.length + const previous = this._unmasked + let { unmasked, count } = this._extract(raw, caret) + let typed = count ?? unmasked.length + + // Deleting a literal changes nothing after re-masking, so it removes the digit next to it + if (unmasked === previous && event.inputType.startsWith('delete') && typed > 0) { + const at = event.inputType === 'deleteContentForward' ? typed : typed - 1 + + if (at < unmasked.length) { + unmasked = unmasked.slice(0, at) + unmasked.slice(at + 1) + typed = at + } + } + + this._unmasked = unmasked + this._render(unmasked, typed) + + if (unmasked !== previous) { + EventHandler.trigger(this._element, EVENT_ACCEPT, { value: this._element.value, unmaskedValue: unmasked }) + } + } + _mergeConfigObj(config?: Record, element?: HTMLElement): Record { // `data-mask` holds the pattern; `data-mask-visible="true"` shows it - // before typing, which is IMask's `lazy: false`. + // before typing, which is `lazy: false`. const dataOptions: Record = {} const mask = element?.getAttribute(ATTRIBUTE_MASK) + const placeholderChar = element?.getAttribute(ATTRIBUTE_PLACEHOLDER_CHAR) if (mask) { dataOptions.mask = mask } + if (placeholderChar) { + dataOptions.placeholderChar = placeholderChar + } + // A bare `data-mask-visible` means visible, like `="true"`. if (element?.hasAttribute(ATTRIBUTE_VISIBLE)) { dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) === 'false' diff --git a/core/js/tests/unit/input-mask.spec.ts b/core/js/tests/unit/input-mask.spec.ts index fa1320c16..ec5b7cb48 100644 --- a/core/js/tests/unit/input-mask.spec.ts +++ b/core/js/tests/unit/input-mask.spec.ts @@ -4,28 +4,26 @@ import InputMask from '../../src/input-mask' describe('InputMask', () => { let fixtureEl: HTMLElement - let plugin: ReturnType beforeAll(() => { fixtureEl = getFixture() }) beforeEach(() => { - plugin = vi.fn(function (this: Record) { - this.updateValue = vi.fn() - this.destroy = vi.fn() - }) - window.IMask = plugin as unknown as Window['IMask'] - fixtureEl.innerHTML = '' + fixtureEl.innerHTML = '' }) afterEach(() => { clearFixture() - delete window.IMask }) const input = (): HTMLInputElement => fixtureEl.querySelector('input')! - const lastInstance = (): Record> => plugin.mock.instances[plugin.mock.instances.length - 1] as Record> + + const type = (el: HTMLInputElement, value: string, inputType = 'insertText', caret = value.length): void => { + el.value = value + el.setSelectionRange(caret, caret) + el.dispatchEvent(new InputEvent('input', { inputType, bubbles: true })) + } describe('NAME', () => { it('should return plugin name', () => { @@ -33,62 +31,249 @@ describe('InputMask', () => { }) }) - describe('constructor', () => { - it('should create the mask from data-mask and data-mask-visible', () => { - const instance = new InputMask(input()) + describe('formatting', () => { + it('should add literals as the user types', () => { + const el = input() + const instance = new InputMask(el) - expect(plugin).toHaveBeenCalledWith(input(), { mask: '00/00/0000', lazy: false }) - expect(instance.mask).toBe(plugin.mock.instances[0]) + type(el, '1') + expect(el.value).toBe('(1') + + type(el, '(12') + type(el, '(123') + expect(el.value).toBe('(12) 3') + expect(instance.unmaskedValue).toBe('123') }) - it('should default to a lazy mask', () => { - fixtureEl.innerHTML = '' + it('should drop characters the token does not accept', () => { + const el = input() + new InputMask(el) + type(el, 'a1b2') + expect(el.value).toBe('(12') + }) + + it('should format a pasted value and cut it to the mask', () => { + const el = input() + const instance = new InputMask(el) + + type(el, '+48 (12) 3456-7890 1234', 'insertFromPaste') + expect(el.value).toBe('(48) 1234-5678') + expect(instance.unmaskedValue).toBe('4812345678') + }) + + it('should mask a value set before the component was created', () => { + fixtureEl.innerHTML = '' new InputMask(input()) - expect(plugin).toHaveBeenCalledWith(input(), { mask: '(00) 0000-0000', lazy: true }) + expect(input().value).toBe('31/12/2026') }) - it('should let the config object win over the attributes', () => { - new InputMask(input(), { mask: '0000', lazy: true }) + it('should re-apply the mask on update()', () => { + const instance = new InputMask(input()) - expect(plugin).toHaveBeenCalledWith(input(), { mask: '0000', lazy: true }) + input().value = '1234567890' + instance.update() + + expect(input().value).toBe('(12) 3456-7890') + }) + + it('should keep the caret after the typed character in the middle', () => { + const el = input() + new InputMask(el) + el.focus() + + type(el, '(12) 3456-7890') + type(el, '(192) 3456-7890', 'insertText', 3) + + expect(el.value).toBe('(19) 2345-6789') + expect(el.selectionStart).toBe(3) + }) + }) + + describe('deleting', () => { + it('should remove the digit before a deleted literal', () => { + const el = input() + const instance = new InputMask(el) + + type(el, '(12) 3') + type(el, '(12)3', 'deleteContentBackward', 4) + + expect(instance.unmaskedValue).toBe('13') + expect(el.value).toBe('(13') + }) + + it('should clear a lazy mask when the value is emptied', () => { + const el = input() + new InputMask(el) + + type(el, '(1') + type(el, '', 'deleteContentBackward', 0) + + expect(el.value).toBe('') + }) + }) + + describe('visible mask', () => { + it('should show placeholders from the start', () => { + fixtureEl.innerHTML = '' + new InputMask(input()) + + expect(input().value).toBe('__/__') + }) + + it('should fill the slots and treat a bare attribute as visible', () => { + fixtureEl.innerHTML = '' + const el = input() + const instance = new InputMask(el) + + type(el, '1__/__') + expect(el.value).toBe('1_/__') + expect(instance.unmaskedValue).toBe('1') + }) + + it('should use the placeholder character from the attribute', () => { + fixtureEl.innerHTML = '' + new InputMask(input()) + + expect(input().value).toBe('##') + }) + }) + + describe('tokens', () => { + it('should support letters and any character', () => { + fixtureEl.innerHTML = '' + const el = input() + new InputMask(el) + + type(el, '1ab-9z') + expect(el.value).toBe('ab-9z') + }) + + it('should use custom tokens', () => { + const el = input() + new InputMask(el, { mask: 'HH', tokens: { H: /[0-9a-f]/i } }) + + type(el, 'fg9') + expect(el.value).toBe('f9') + }) + + it('should treat an escaped token as a literal', () => { + fixtureEl.innerHTML = '' + const el = fixtureEl.appendChild(document.createElement('input')) + new InputMask(el, { mask: '\\000', lazy: false }) + + expect(el.value).toBe('0__') + }) + }) + + describe('dynamic mask', () => { + it('should pick the mask from the unmasked value', () => { + const el = input() + new InputMask(el, { mask: (value: string) => (value.startsWith('34') || value.startsWith('37') ? '0000 000000 00000' : '0000 0000 0000 0000') }) + + type(el, '3782822463100005') + expect(el.value).toBe('3782 822463 10000') + }) + }) + + describe('events', () => { + it('should fire accept with the values', () => { + const el = input() + new InputMask(el) + const spy = vi.fn() + el.addEventListener('accept.bs.input-mask', ((event: CustomEvent) => spy(event)) as EventListener) + + type(el, '12') + + expect(spy).toHaveBeenCalledTimes(1) + const event = spy.mock.calls[0][0] as Event & { value: string; unmaskedValue: string } + expect(event.value).toBe('(12') + expect(event.unmaskedValue).toBe('12') + }) + + it('should fire complete once when every slot is filled', () => { + const el = input() + const instance = new InputMask(el) + const spy = vi.fn() + el.addEventListener('complete.bs.input-mask', spy) + + type(el, '(12) 3456-789') + expect(spy).not.toHaveBeenCalled() + expect(instance.isComplete).toBe(false) + + type(el, '(12) 3456-7890') + expect(spy).toHaveBeenCalledTimes(1) + expect(instance.isComplete).toBe(true) + }) + }) + + describe('constructor', () => { + it('should let the config object win over the attributes', () => { + const el = input() + new InputMask(el, { mask: '0000' }) + + type(el, '12345') + expect(el.value).toBe('1234') }) it('should stay inert without a mask', () => { fixtureEl.innerHTML = '' - const instance = new InputMask(input()) - expect(plugin).not.toHaveBeenCalled() + type(input(), 'abc') + expect(input().value).toBe('abc') expect(instance.mask).toBeNull() }) - it('should stay inert when the plugin is not loaded', () => { - delete window.IMask + it('should warn and stay inert for an IMask object mask without IMask', () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + fixtureEl.innerHTML = '' + new InputMask(input(), { mask: Number as unknown as string }) - expect(() => new InputMask(input())).not.toThrow() - expect(InputMask.getInstance(input())).toBeInstanceOf(InputMask) + expect(warn).toHaveBeenCalled() + warn.mockRestore() + }) + }) + + describe('legacy IMask fallback', () => { + afterEach(() => { + delete window.IMask + }) + + it('should hand an object mask to IMask when it is loaded', () => { + const plugin = vi.fn(function (this: Record) { + this.updateValue = vi.fn() + this.destroy = vi.fn() + }) + window.IMask = plugin as unknown as Window['IMask'] + fixtureEl.innerHTML = '' + + const instance = new InputMask(input(), { mask: Number as unknown as string }) + + expect(plugin).toHaveBeenCalledTimes(1) + expect(instance.mask).toBe(plugin.mock.instances[0]) }) }) describe('public API', () => { - it('should forward update to the mask', () => { - const instance = new InputMask(input()) - instance.update() - - expect(lastInstance().updateValue).toHaveBeenCalledTimes(1) - }) - - it('should destroy the mask and remove the instance on dispose', () => { - const instance = new InputMask(input()) - const element = input() - const destroy = lastInstance().destroy + it('should remove the listeners and the instance on dispose', () => { + const el = input() + const instance = new InputMask(el) instance.dispose() + type(el, 'abc') - expect(destroy).toHaveBeenCalledTimes(1) - expect(InputMask.getInstance(element)).toBeNull() + expect(el.value).toBe('abc') + expect(InputMask.getInstance(el)).toBeNull() + }) + + it('should answer the IMask member names', () => { + const instance = new InputMask(input()) + + expect(instance.mask).toBe(instance) + expect(typeof instance.updateValue).toBe('function') + expect(typeof instance.destroy).toBe('function') }) }) @@ -98,29 +283,6 @@ describe('InputMask', () => { const second = InputMask.getOrCreateInstance(input()) expect(second).toBe(first) - expect(plugin).toHaveBeenCalledTimes(1) - }) - }) - - describe('config passthrough', () => { - it('hands every config option to IMask', () => { - fixtureEl.innerHTML = '' - new InputMask(input(), { mask: '000', placeholderChar: '#' }) - - expect(plugin).toHaveBeenCalledWith(input(), { mask: '000', lazy: true, placeholderChar: '#' }) - }) - - it('accepts a non-string mask', () => { - fixtureEl.innerHTML = '' - expect(() => new InputMask(input(), { mask: Number })).not.toThrow() - expect(() => new InputMask(fixtureEl.appendChild(document.createElement('input')), { mask: /^\d+$/ })).not.toThrow() - }) - - it('treats a bare data-mask-visible as visible', () => { - fixtureEl.innerHTML = '' - new InputMask(input()) - - expect(plugin).toHaveBeenCalledWith(input(), { mask: '00', lazy: false }) }) }) }) diff --git a/docs/content/ui/plugins/input-mask.mdx b/docs/content/ui/plugins/input-mask.mdx index 5c6565211..89aa29f3d 100644 --- a/docs/content/ui/plugins/input-mask.mdx +++ b/docs/content/ui/plugins/input-mask.mdx @@ -3,34 +3,12 @@ title: Input mask order: 11 summary: An input mask formats what the user types to a pattern, such as a phone number or a date, which makes the expected format clear and cuts validation errors. description: Format user input automatically with input masks for dates, phone numbers, and other patterns to reduce validation errors. -docs-libs: [imask] related: [/ui/forms/elements] --- import Example from '@components/Example.astro' -import TabsPackage from '@components/TabsPackage.astro' import CodeDocs from '@components/CodeDocs.astro'; -## Installation - -Install the imask library: - - - -And import or require: - -```javascript -import IMask from 'imask' -``` - -Or include it from a CDN: - -```html - -``` - -The [IMask documentation](https://imask.js.org/guide.html#installation) covers other ways to install it. - ## Default markup Use an input mask where the value has a fixed format, such as a phone number, a date or a card number. @@ -54,39 +32,89 @@ To create an input mask, add the `data-mask` attribute to the input element: -## More examples +## Pattern -If you need more examples of input masks, you can find them in the [IMask documentation](https://imask.js.org/guide.html#masked-input). +| Character | Accepts | +| --- | --- | +| `0` | a digit | +| `a` | a letter | +| `*` | any character | +| `\` | makes the next character a literal, so `\0` is a plain `0` | + +Every other character is a literal, and Tabler adds it for the user as they type. Paste works too: the value is cut to the pattern and the characters that do not fit are dropped. + + + + + + + + +## Options + +| Option | Attribute | Default | Description | +| --- | --- | --- | --- | +| `mask` | `data-mask` | `''` | The pattern, or a function that gets the unmasked value and returns the pattern. | +| `lazy` | `data-mask-visible="true"` | `true` | With `data-mask-visible` the whole pattern shows before the user types. | +| `placeholderChar` | `data-mask-placeholder-char` | `_` | The character for an empty slot when the pattern is visible. | +| `tokens` | | `{}` | Extra pattern characters, each with the RegExp one character has to match. | ## JavaScript -Tabler wraps the plugin in an `InputMask` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-mask`. This is the code that runs: +Tabler creates an `InputMask` for every element with `data-mask`, the same way as the Bootstrap components. This is the code that runs: -Create the component yourself for a field added later. The pattern comes from `data-mask`, or from the config object, which wins: +Create the component yourself for a field added later. The config object wins over the attributes: ```js const phone = new tabler.InputMask(document.getElementById('phone'), { mask: '(00) 0000-0000' }); ``` -`data-mask-visible="true"` shows the pattern before the user types. In the config object that is `lazy: false`. - -The `mask` getter returns the IMask instance, so everything from the [IMask API](https://imask.js.org/guide.html#api) is available: +A custom token, here a hexadecimal digit: ```js -const phone = tabler.InputMask.getOrCreateInstance(document.getElementById('phone')); -console.log(phone.mask.unmaskedValue); +new tabler.InputMask(document.getElementById('color'), { + mask: '#HHHHHH', + tokens: { H: /[0-9a-f]/i }, +}); ``` -| Method | Description | +A function as the mask changes the pattern while the user types, for example for a card number: + +```js +new tabler.InputMask(document.getElementById('card'), { + mask: (value) => (/^3[47]/.test(value) ? '0000 000000 00000' : '0000 0000 0000 0000'), +}); +``` + +### Methods and properties + +| Name | Description | | --- | --- | +| `value` | The value as shown, with the literals. | +| `unmaskedValue` | Only what the user typed. Send this one to a server. | +| `isComplete` | `true` when every slot is filled. | | `update()` | Re-applies the mask after the value was changed from code. | -| `mask` | Getter. The IMask instance, or `null` when the library is not loaded. | | `dispose()` | Removes the mask and the component from the element. | | `getInstance(element)` | Static. Returns the component for the element, or `null`. | | `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. | +### Events + +The events are triggered on the input. + +| Event | Description | +| --- | --- | +| `accept.bs.input-mask` | The unmasked value changed. Has `value` and `unmaskedValue`. | +| `complete.bs.input-mask` | The last slot was filled. Has `value` and `unmaskedValue`. | + +```js +input.addEventListener('complete.bs.input-mask', (event) => { + console.log(event.unmaskedValue); +}); +``` + ## Accessibility - A mask changes what the field accepts as the user types, which is easy to lose track of without sight. Say the expected format in a hint tied to the field with `aria-describedby`. diff --git a/preview/pages/form-elements.astro b/preview/pages/form-elements.astro index a2cfb8f44..e4b368fa2 100644 --- a/preview/pages/form-elements.astro +++ b/preview/pages/form-elements.astro @@ -52,7 +52,7 @@ const rows = [ ] --- - +
@@ -133,7 +133,7 @@ const rows = [ - + diff --git a/shared/ui/form/InputMask.astro b/shared/ui/form/InputMask.astro index 06c75681d..33faa95ca 100644 --- a/shared/ui/form/InputMask.astro +++ b/shared/ui/form/InputMask.astro @@ -1,14 +1,12 @@ --- -// IMask initializes from the data-mask attributes (tabler.js). interface Props { mask?: string visible?: boolean placeholder?: string - reverse?: boolean } -const { mask = '00/00/0000', visible, placeholder, reverse } = Astro.props +const { mask = '00/00/0000', visible, placeholder } = Astro.props const name = 'mask' --- - +