UNPKG

imask

Version:

vanilla javascript input mask

1,605 lines (1,449 loc) 117 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** Checks if value is string */ function isString(str) { return typeof str === 'string' || str instanceof String; } /** Checks if value is object */ function isObject(obj) { var _obj$constructor; return typeof obj === 'object' && obj != null && (obj == null || (_obj$constructor = obj.constructor) == null ? void 0 : _obj$constructor.name) === 'Object'; } function pick(obj, keys) { if (Array.isArray(keys)) return pick(obj, (_, k) => keys.includes(k)); return Object.entries(obj).reduce((acc, _ref) => { let [k, v] = _ref; if (keys(v, k)) acc[k] = v; return acc; }, {}); } /** Direction */ const DIRECTION = { NONE: 'NONE', LEFT: 'LEFT', FORCE_LEFT: 'FORCE_LEFT', RIGHT: 'RIGHT', FORCE_RIGHT: 'FORCE_RIGHT' }; /** Direction */ function forceDirection(direction) { switch (direction) { case DIRECTION.LEFT: return DIRECTION.FORCE_LEFT; case DIRECTION.RIGHT: return DIRECTION.FORCE_RIGHT; default: return direction; } } /** Escapes regular expression control chars */ function escapeRegExp(str) { return str.replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'); } // cloned from https://github.com/epoberezkin/fast-deep-equal with small changes function objectIncludes(b, a) { if (a === b) return true; const arrA = Array.isArray(a), arrB = Array.isArray(b); let i; if (arrA && arrB) { if (a.length != b.length) return false; for (i = 0; i < a.length; i++) if (!objectIncludes(a[i], b[i])) return false; return true; } if (arrA != arrB) return false; if (a && b && typeof a === 'object' && typeof b === 'object') { const dateA = a instanceof Date, dateB = b instanceof Date; if (dateA && dateB) return a.getTime() == b.getTime(); if (dateA != dateB) return false; const regexpA = a instanceof RegExp, regexpB = b instanceof RegExp; if (regexpA && regexpB) return a.toString() == b.toString(); if (regexpA != regexpB) return false; const keys = Object.keys(a); // if (keys.length !== Object.keys(b).length) return false; for (i = 0; i < keys.length; i++) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = 0; i < keys.length; i++) if (!objectIncludes(b[keys[i]], a[keys[i]])) return false; return true; } else if (a && b && typeof a === 'function' && typeof b === 'function') { return a.toString() === b.toString(); } return false; } /** Selection range */ /** Provides details of changing input */ class ActionDetails { /** Current input value */ /** Current cursor position */ /** Old input value */ /** Old selection */ constructor(opts) { Object.assign(this, opts); // double check if left part was changed (autofilling, other non-standard input triggers) while (this.value.slice(0, this.startChangePos) !== this.oldValue.slice(0, this.startChangePos)) { --this.oldSelection.start; } if (this.insertedCount) { // double check right part while (this.value.slice(this.cursorPos) !== this.oldValue.slice(this.oldSelection.end)) { if (this.value.length - this.cursorPos < this.oldValue.length - this.oldSelection.end) ++this.oldSelection.end;else ++this.cursorPos; } } } /** Start changing position */ get startChangePos() { return Math.min(this.cursorPos, this.oldSelection.start); } /** Inserted symbols count */ get insertedCount() { return this.cursorPos - this.startChangePos; } /** Inserted symbols */ get inserted() { return this.value.substr(this.startChangePos, this.insertedCount); } /** Removed symbols count */ get removedCount() { // Math.max for opposite operation return Math.max(this.oldSelection.end - this.startChangePos || // for Delete this.oldValue.length - this.value.length, 0); } /** Removed symbols */ get removed() { return this.oldValue.substr(this.startChangePos, this.removedCount); } /** Unchanged head symbols */ get head() { return this.value.substring(0, this.startChangePos); } /** Unchanged tail symbols */ get tail() { return this.value.substring(this.startChangePos + this.insertedCount); } /** Remove direction */ get removeDirection() { if (!this.removedCount || this.insertedCount) return DIRECTION.NONE; // align right if delete at right return (this.oldSelection.end === this.cursorPos || this.oldSelection.start === this.cursorPos) && // if not range removed (event with backspace) this.oldSelection.end === this.oldSelection.start ? DIRECTION.RIGHT : DIRECTION.LEFT; } } /** Applies mask on element */ function IMask(el, opts) { // currently available only for input-like elements return new IMask.InputMask(el, opts); } // TODO can't use overloads here because of https://github.com/microsoft/TypeScript/issues/50754 // export function maskedClass(mask: string): typeof MaskedPattern; // export function maskedClass(mask: DateConstructor): typeof MaskedDate; // export function maskedClass(mask: NumberConstructor): typeof MaskedNumber; // export function maskedClass(mask: Array<any> | ArrayConstructor): typeof MaskedDynamic; // export function maskedClass(mask: MaskedDate): typeof MaskedDate; // export function maskedClass(mask: MaskedNumber): typeof MaskedNumber; // export function maskedClass(mask: MaskedEnum): typeof MaskedEnum; // export function maskedClass(mask: MaskedRange): typeof MaskedRange; // export function maskedClass(mask: MaskedRegExp): typeof MaskedRegExp; // export function maskedClass(mask: MaskedFunction): typeof MaskedFunction; // export function maskedClass(mask: MaskedPattern): typeof MaskedPattern; // export function maskedClass(mask: MaskedDynamic): typeof MaskedDynamic; // export function maskedClass(mask: Masked): typeof Masked; // export function maskedClass(mask: typeof Masked): typeof Masked; // export function maskedClass(mask: typeof MaskedDate): typeof MaskedDate; // export function maskedClass(mask: typeof MaskedNumber): typeof MaskedNumber; // export function maskedClass(mask: typeof MaskedEnum): typeof MaskedEnum; // export function maskedClass(mask: typeof MaskedRange): typeof MaskedRange; // export function maskedClass(mask: typeof MaskedRegExp): typeof MaskedRegExp; // export function maskedClass(mask: typeof MaskedFunction): typeof MaskedFunction; // export function maskedClass(mask: typeof MaskedPattern): typeof MaskedPattern; // export function maskedClass(mask: typeof MaskedDynamic): typeof MaskedDynamic; // export function maskedClass<Mask extends typeof Masked> (mask: Mask): Mask; // export function maskedClass(mask: RegExp): typeof MaskedRegExp; // export function maskedClass(mask: (value: string, ...args: any[]) => boolean): typeof MaskedFunction; /** Get Masked class by mask type */ function maskedClass(mask) /* TODO */{ if (mask == null) throw new Error('mask property should be defined'); if (mask instanceof RegExp) return IMask.MaskedRegExp; if (isString(mask)) return IMask.MaskedPattern; if (mask === Date) return IMask.MaskedDate; if (mask === Number) return IMask.MaskedNumber; if (Array.isArray(mask) || mask === Array) return IMask.MaskedDynamic; if (IMask.Masked && mask.prototype instanceof IMask.Masked) return mask; if (IMask.Masked && mask instanceof IMask.Masked) return mask.constructor; if (mask instanceof Function) return IMask.MaskedFunction; console.warn('Mask not found for mask', mask); // eslint-disable-line no-console return IMask.Masked; } function normalizeOpts(opts) { if (!opts) throw new Error('Options in not defined'); if (IMask.Masked) { if (opts.prototype instanceof IMask.Masked) return { mask: opts }; /* handle cases like: 1) opts = Masked 2) opts = { mask: Masked, ...instanceOpts } */ const { mask = undefined, ...instanceOpts } = opts instanceof IMask.Masked ? { mask: opts } : isObject(opts) && opts.mask instanceof IMask.Masked ? opts : {}; if (mask) { const _mask = mask.mask; return { ...pick(mask, (_, k) => !k.startsWith('_')), mask: mask.constructor, _mask, ...instanceOpts }; } } if (!isObject(opts)) return { mask: opts }; return { ...opts }; } // TODO can't use overloads here because of https://github.com/microsoft/TypeScript/issues/50754 // From masked // export default function createMask<Opts extends Masked, ReturnMasked=Opts> (opts: Opts): ReturnMasked; // // From masked class // export default function createMask<Opts extends MaskedOptions<typeof Masked>, ReturnMasked extends Masked=InstanceType<Opts['mask']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedDate>, ReturnMasked extends MaskedDate=MaskedDate<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedNumber>, ReturnMasked extends MaskedNumber=MaskedNumber<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedEnum>, ReturnMasked extends MaskedEnum=MaskedEnum<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedRange>, ReturnMasked extends MaskedRange=MaskedRange<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedRegExp>, ReturnMasked extends MaskedRegExp=MaskedRegExp<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedFunction>, ReturnMasked extends MaskedFunction=MaskedFunction<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedPattern>, ReturnMasked extends MaskedPattern=MaskedPattern<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<typeof MaskedDynamic>, ReturnMasked extends MaskedDynamic=MaskedDynamic<Opts['parent']>> (opts: Opts): ReturnMasked; // // From mask opts // export default function createMask<Opts extends MaskedOptions<Masked>, ReturnMasked=Opts extends MaskedOptions<infer M> ? M : never> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedNumberOptions, ReturnMasked extends MaskedNumber=MaskedNumber<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedDateFactoryOptions, ReturnMasked extends MaskedDate=MaskedDate<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedEnumOptions, ReturnMasked extends MaskedEnum=MaskedEnum<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedRangeOptions, ReturnMasked extends MaskedRange=MaskedRange<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedPatternOptions, ReturnMasked extends MaskedPattern=MaskedPattern<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedDynamicOptions, ReturnMasked extends MaskedDynamic=MaskedDynamic<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<RegExp>, ReturnMasked extends MaskedRegExp=MaskedRegExp<Opts['parent']>> (opts: Opts): ReturnMasked; // export default function createMask<Opts extends MaskedOptions<Function>, ReturnMasked extends MaskedFunction=MaskedFunction<Opts['parent']>> (opts: Opts): ReturnMasked; /** Creates new {@link Masked} depending on mask type */ function createMask(opts) { if (IMask.Masked && opts instanceof IMask.Masked) return opts; const nOpts = normalizeOpts(opts); const MaskedClass = maskedClass(nOpts.mask); if (!MaskedClass) throw new Error("Masked class is not found for provided mask " + nOpts.mask + ", appropriate module needs to be imported manually before creating mask."); if (nOpts.mask === MaskedClass) delete nOpts.mask; if (nOpts._mask) { nOpts.mask = nOpts._mask; delete nOpts._mask; } return new MaskedClass(nOpts); } IMask.createMask = createMask; /** Generic element API to use with mask */ class MaskElement { /** */ /** */ /** */ /** Safely returns selection start */ get selectionStart() { let start; try { start = this._unsafeSelectionStart; } catch {} return start != null ? start : this.value.length; } /** Safely returns selection end */ get selectionEnd() { let end; try { end = this._unsafeSelectionEnd; } catch {} return end != null ? end : this.value.length; } /** Safely sets element selection */ select(start, end) { if (start == null || end == null || start === this.selectionStart && end === this.selectionEnd) return; try { this._unsafeSelect(start, end); } catch {} } /** */ get isActive() { return false; } /** */ /** */ /** */ } IMask.MaskElement = MaskElement; const KEY_Z = 90; const KEY_Y = 89; /** Bridge between HTMLElement and {@link Masked} */ class HTMLMaskElement extends MaskElement { /** HTMLElement to use mask on */ constructor(input) { super(); this.input = input; this._onKeydown = this._onKeydown.bind(this); this._onInput = this._onInput.bind(this); this._onBeforeinput = this._onBeforeinput.bind(this); this._onCompositionEnd = this._onCompositionEnd.bind(this); } get rootElement() { var _this$input$getRootNo, _this$input$getRootNo2, _this$input; return (_this$input$getRootNo = (_this$input$getRootNo2 = (_this$input = this.input).getRootNode) == null ? void 0 : _this$input$getRootNo2.call(_this$input)) != null ? _this$input$getRootNo : document; } /** Is element in focus */ get isActive() { return this.input === this.rootElement.activeElement; } /** Binds HTMLElement events to mask internal events */ bindEvents(handlers) { this.input.addEventListener('keydown', this._onKeydown); this.input.addEventListener('input', this._onInput); this.input.addEventListener('beforeinput', this._onBeforeinput); this.input.addEventListener('compositionend', this._onCompositionEnd); this.input.addEventListener('drop', handlers.drop); this.input.addEventListener('click', handlers.click); this.input.addEventListener('focus', handlers.focus); this.input.addEventListener('blur', handlers.commit); this._handlers = handlers; } _onKeydown(e) { if (this._handlers.redo && (e.keyCode === KEY_Z && e.shiftKey && (e.metaKey || e.ctrlKey) || e.keyCode === KEY_Y && e.ctrlKey)) { e.preventDefault(); return this._handlers.redo(e); } if (this._handlers.undo && e.keyCode === KEY_Z && (e.metaKey || e.ctrlKey)) { e.preventDefault(); return this._handlers.undo(e); } if (!e.isComposing) this._handlers.selectionChange(e); } _onBeforeinput(e) { if (e.inputType === 'historyUndo' && this._handlers.undo) { e.preventDefault(); return this._handlers.undo(e); } if (e.inputType === 'historyRedo' && this._handlers.redo) { e.preventDefault(); return this._handlers.redo(e); } } _onCompositionEnd(e) { this._handlers.input(e); } _onInput(e) { if (!e.isComposing) this._handlers.input(e); } /** Unbinds HTMLElement events to mask internal events */ unbindEvents() { this.input.removeEventListener('keydown', this._onKeydown); this.input.removeEventListener('input', this._onInput); this.input.removeEventListener('beforeinput', this._onBeforeinput); this.input.removeEventListener('compositionend', this._onCompositionEnd); this.input.removeEventListener('drop', this._handlers.drop); this.input.removeEventListener('click', this._handlers.click); this.input.removeEventListener('focus', this._handlers.focus); this.input.removeEventListener('blur', this._handlers.commit); this._handlers = {}; } } IMask.HTMLMaskElement = HTMLMaskElement; /** Bridge between InputElement and {@link Masked} */ class HTMLInputMaskElement extends HTMLMaskElement { /** InputElement to use mask on */ constructor(input) { super(input); this.input = input; } /** Returns InputElement selection start */ get _unsafeSelectionStart() { return this.input.selectionStart != null ? this.input.selectionStart : this.value.length; } /** Returns InputElement selection end */ get _unsafeSelectionEnd() { return this.input.selectionEnd; } /** Sets InputElement selection */ _unsafeSelect(start, end) { this.input.setSelectionRange(start, end); } get value() { return this.input.value; } set value(value) { this.input.value = value; } } IMask.HTMLMaskElement = HTMLMaskElement; class HTMLContenteditableMaskElement extends HTMLMaskElement { /** Returns HTMLElement selection start */ get _unsafeSelectionStart() { const root = this.rootElement; const selection = root.getSelection && root.getSelection(); const anchorOffset = selection && selection.anchorOffset; const focusOffset = selection && selection.focusOffset; if (focusOffset == null || anchorOffset == null || anchorOffset < focusOffset) { return anchorOffset; } return focusOffset; } /** Returns HTMLElement selection end */ get _unsafeSelectionEnd() { const root = this.rootElement; const selection = root.getSelection && root.getSelection(); const anchorOffset = selection && selection.anchorOffset; const focusOffset = selection && selection.focusOffset; if (focusOffset == null || anchorOffset == null || anchorOffset > focusOffset) { return anchorOffset; } return focusOffset; } /** Sets HTMLElement selection */ _unsafeSelect(start, end) { if (!this.rootElement.createRange) return; const range = this.rootElement.createRange(); range.setStart(this.input.firstChild || this.input, start); range.setEnd(this.input.lastChild || this.input, end); const root = this.rootElement; const selection = root.getSelection && root.getSelection(); if (selection) { selection.removeAllRanges(); selection.addRange(range); } } /** HTMLElement value */ get value() { return this.input.textContent || ''; } set value(value) { this.input.textContent = value; } } IMask.HTMLContenteditableMaskElement = HTMLContenteditableMaskElement; class InputHistory { constructor() { this.states = []; this.currentIndex = 0; } get currentState() { return this.states[this.currentIndex]; } get isEmpty() { return this.states.length === 0; } push(state) { // if current index points before the last element then remove the future if (this.currentIndex < this.states.length - 1) this.states.length = this.currentIndex + 1; this.states.push(state); if (this.states.length > InputHistory.MAX_LENGTH) this.states.shift(); this.currentIndex = this.states.length - 1; } go(steps) { this.currentIndex = Math.min(Math.max(this.currentIndex + steps, 0), this.states.length - 1); return this.currentState; } undo() { return this.go(-1); } redo() { return this.go(+1); } clear() { this.states.length = 0; this.currentIndex = 0; } } InputHistory.MAX_LENGTH = 100; /** Listens to element events and controls changes between element and {@link Masked} */ class InputMask { /** View element */ /** Internal {@link Masked} model */ constructor(el, opts) { this.el = el instanceof MaskElement ? el : el.isContentEditable && el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA' ? new HTMLContenteditableMaskElement(el) : new HTMLInputMaskElement(el); this.masked = createMask(opts); this._listeners = {}; this._value = ''; this._unmaskedValue = ''; this._rawInputValue = ''; this.history = new InputHistory(); this._saveSelection = this._saveSelection.bind(this); this._onInput = this._onInput.bind(this); this._onChange = this._onChange.bind(this); this._onDrop = this._onDrop.bind(this); this._onFocus = this._onFocus.bind(this); this._onClick = this._onClick.bind(this); this._onUndo = this._onUndo.bind(this); this._onRedo = this._onRedo.bind(this); this.alignCursor = this.alignCursor.bind(this); this.alignCursorFriendly = this.alignCursorFriendly.bind(this); this._bindEvents(); // refresh this.updateValue(); this._onChange(); } maskEquals(mask) { var _this$masked; return mask == null || ((_this$masked = this.masked) == null ? void 0 : _this$masked.maskEquals(mask)); } /** Masked */ get mask() { return this.masked.mask; } set mask(mask) { if (this.maskEquals(mask)) return; if (!(mask instanceof IMask.Masked) && this.masked.constructor === maskedClass(mask)) { // TODO "any" no idea this.masked.updateOptions({ mask }); return; } const masked = mask instanceof IMask.Masked ? mask : createMask({ mask }); masked.unmaskedValue = this.masked.unmaskedValue; this.masked = masked; } /** Raw value */ get value() { return this._value; } set value(str) { if (this.value === str) return; this.masked.value = str; this.updateControl('auto'); } /** Unmasked value */ get unmaskedValue() { return this._unmaskedValue; } set unmaskedValue(str) { if (this.unmaskedValue === str) return; this.masked.unmaskedValue = str; this.updateControl('auto'); } /** Raw input value */ get rawInputValue() { return this._rawInputValue; } set rawInputValue(str) { if (this.rawInputValue === str) return; this.masked.rawInputValue = str; this.updateControl(); this.alignCursor(); } /** Typed unmasked value */ get typedValue() { return this.masked.typedValue; } set typedValue(val) { if (this.masked.typedValueEquals(val)) return; this.masked.typedValue = val; this.updateControl('auto'); } /** Display value */ get displayValue() { return this.masked.displayValue; } /** Starts listening to element events */ _bindEvents() { this.el.bindEvents({ selectionChange: this._saveSelection, input: this._onInput, drop: this._onDrop, click: this._onClick, focus: this._onFocus, commit: this._onChange, undo: this._onUndo, redo: this._onRedo }); } /** Stops listening to element events */ _unbindEvents() { if (this.el) this.el.unbindEvents(); } /** Fires custom event */ _fireEvent(ev, e) { const listeners = this._listeners[ev]; if (!listeners) return; listeners.forEach(l => l(e)); } /** Current selection start */ get selectionStart() { return this._cursorChanging ? this._changingCursorPos : this.el.selectionStart; } /** Current cursor position */ get cursorPos() { return this._cursorChanging ? this._changingCursorPos : this.el.selectionEnd; } set cursorPos(pos) { if (!this.el || !this.el.isActive) return; this.el.select(pos, pos); this._saveSelection(); } /** Stores current selection */ _saveSelection( /* ev */ ) { if (this.displayValue !== this.el.value) { console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console } this._selection = { start: this.selectionStart, end: this.cursorPos }; } /** Syncronizes model value from view */ updateValue() { this.masked.value = this.el.value; this._value = this.masked.value; this._unmaskedValue = this.masked.unmaskedValue; this._rawInputValue = this.masked.rawInputValue; } /** Syncronizes view from model value, fires change events */ updateControl(cursorPos) { const newUnmaskedValue = this.masked.unmaskedValue; const newValue = this.masked.value; const newRawInputValue = this.masked.rawInputValue; const newDisplayValue = this.displayValue; const isChanged = this.unmaskedValue !== newUnmaskedValue || this.value !== newValue || this._rawInputValue !== newRawInputValue; this._unmaskedValue = newUnmaskedValue; this._value = newValue; this._rawInputValue = newRawInputValue; if (this.el.value !== newDisplayValue) this.el.value = newDisplayValue; if (cursorPos === 'auto') this.alignCursor();else if (cursorPos != null) this.cursorPos = cursorPos; if (isChanged) this._fireChangeEvents(); if (!this._historyChanging && (isChanged || this.history.isEmpty)) this.history.push({ unmaskedValue: newUnmaskedValue, selection: { start: this.selectionStart, end: this.cursorPos } }); } /** Updates options with deep equal check, recreates {@link Masked} model if mask type changes */ updateOptions(opts) { const { mask, ...restOpts } = opts; // TODO types, yes, mask is optional const updateMask = !this.maskEquals(mask); const updateOpts = this.masked.optionsIsChanged(restOpts); if (updateMask) this.mask = mask; if (updateOpts) this.masked.updateOptions(restOpts); // TODO if (updateMask || updateOpts) this.updateControl(); } /** Updates cursor */ updateCursor(cursorPos) { if (cursorPos == null) return; this.cursorPos = cursorPos; // also queue change cursor for mobile browsers this._delayUpdateCursor(cursorPos); } /** Delays cursor update to support mobile browsers */ _delayUpdateCursor(cursorPos) { this._abortUpdateCursor(); this._changingCursorPos = cursorPos; this._cursorChanging = setTimeout(() => { if (!this.el) return; // if was destroyed this.cursorPos = this._changingCursorPos; this._abortUpdateCursor(); }, 10); } /** Fires custom events */ _fireChangeEvents() { this._fireEvent('accept', this._inputEvent); if (this.masked.isComplete) this._fireEvent('complete', this._inputEvent); } /** Aborts delayed cursor update */ _abortUpdateCursor() { if (this._cursorChanging) { clearTimeout(this._cursorChanging); delete this._cursorChanging; } } /** Aligns cursor to nearest available position */ alignCursor() { this.cursorPos = this.masked.nearestInputPos(this.masked.nearestInputPos(this.cursorPos, DIRECTION.LEFT)); } /** Aligns cursor only if selection is empty */ alignCursorFriendly() { if (this.selectionStart !== this.cursorPos) return; // skip if range is selected this.alignCursor(); } /** Adds listener on custom event */ on(ev, handler) { if (!this._listeners[ev]) this._listeners[ev] = []; this._listeners[ev].push(handler); return this; } /** Removes custom event listener */ off(ev, handler) { if (!this._listeners[ev]) return this; if (!handler) { delete this._listeners[ev]; return this; } const hIndex = this._listeners[ev].indexOf(handler); if (hIndex >= 0) this._listeners[ev].splice(hIndex, 1); return this; } /** Handles view input event */ _onInput(e) { this._inputEvent = e; this._abortUpdateCursor(); const details = new ActionDetails({ // new state value: this.el.value, cursorPos: this.cursorPos, // old state oldValue: this.displayValue, oldSelection: this._selection }); const oldRawValue = this.masked.rawInputValue; const offset = this.masked.splice(details.startChangePos, details.removed.length, details.inserted, details.removeDirection, { input: true, raw: true }).offset; // force align in remove direction only if no input chars were removed // otherwise we still need to align with NONE (to get out from fixed symbols for instance) const removeDirection = oldRawValue === this.masked.rawInputValue ? details.removeDirection : DIRECTION.NONE; let cursorPos = this.masked.nearestInputPos(details.startChangePos + offset, removeDirection); if (removeDirection !== DIRECTION.NONE) cursorPos = this.masked.nearestInputPos(cursorPos, DIRECTION.NONE); this.updateControl(cursorPos); delete this._inputEvent; } /** Handles view change event and commits model value */ _onChange() { if (this.displayValue !== this.el.value) this.updateValue(); this.masked.doCommit(); this.updateControl(); this._saveSelection(); } /** Handles view drop event, prevents by default */ _onDrop(ev) { ev.preventDefault(); ev.stopPropagation(); } /** Restore last selection on focus */ _onFocus(ev) { this.alignCursorFriendly(); } /** Restore last selection on focus */ _onClick(ev) { this.alignCursorFriendly(); } _onUndo() { this._applyHistoryState(this.history.undo()); } _onRedo() { this._applyHistoryState(this.history.redo()); } _applyHistoryState(state) { if (!state) return; this._historyChanging = true; this.unmaskedValue = state.unmaskedValue; this.el.select(state.selection.start, state.selection.end); this._saveSelection(); this._historyChanging = false; } /** Unbind view events and removes element reference */ destroy() { this._unbindEvents(); this._listeners.length = 0; delete this.el; } } IMask.InputMask = InputMask; /** Provides details of changing model value */ class ChangeDetails { /** Inserted symbols */ /** Additional offset if any changes occurred before tail */ /** Raw inserted is used by dynamic mask */ /** Can skip chars */ static normalize(prep) { return Array.isArray(prep) ? prep : [prep, new ChangeDetails()]; } constructor(details) { Object.assign(this, { inserted: '', rawInserted: '', tailShift: 0, skip: false }, details); } /** Aggregate changes */ aggregate(details) { this.inserted += details.inserted; this.rawInserted += details.rawInserted; this.tailShift += details.tailShift; this.skip = this.skip || details.skip; return this; } /** Total offset considering all changes */ get offset() { return this.tailShift + this.inserted.length; } get consumed() { return Boolean(this.rawInserted) || this.skip; } equals(details) { return this.inserted === details.inserted && this.tailShift === details.tailShift && this.rawInserted === details.rawInserted && this.skip === details.skip; } } IMask.ChangeDetails = ChangeDetails; /** Provides details of continuous extracted tail */ class ContinuousTailDetails { /** Tail value as string */ /** Tail start position */ /** Start position */ constructor(value, from, stop) { if (value === void 0) { value = ''; } if (from === void 0) { from = 0; } this.value = value; this.from = from; this.stop = stop; } toString() { return this.value; } extend(tail) { this.value += String(tail); } appendTo(masked) { return masked.append(this.toString(), { tail: true }).aggregate(masked._appendPlaceholder()); } get state() { return { value: this.value, from: this.from, stop: this.stop }; } set state(state) { Object.assign(this, state); } unshift(beforePos) { if (!this.value.length || beforePos != null && this.from >= beforePos) return ''; const shiftChar = this.value[0]; this.value = this.value.slice(1); return shiftChar; } shift() { if (!this.value.length) return ''; const shiftChar = this.value[this.value.length - 1]; this.value = this.value.slice(0, -1); return shiftChar; } } /** Append flags */ /** Extract flags */ // see https://github.com/microsoft/TypeScript/issues/6223 /** Provides common masking stuff */ class Masked { /** */ /** */ /** Transforms value before mask processing */ /** Transforms each char before mask processing */ /** Validates if value is acceptable */ /** Does additional processing at the end of editing */ /** Format typed value to string */ /** Parse string to get typed value */ /** Enable characters overwriting */ /** */ /** */ /** */ /** */ constructor(opts) { this._value = ''; this._update({ ...Masked.DEFAULTS, ...opts }); this._initialized = true; } /** Sets and applies new options */ updateOptions(opts) { if (!this.optionsIsChanged(opts)) return; this.withValueRefresh(this._update.bind(this, opts)); } /** Sets new options */ _update(opts) { Object.assign(this, opts); } /** Mask state */ get state() { return { _value: this.value, _rawInputValue: this.rawInputValue }; } set state(state) { this._value = state._value; } /** Resets value */ reset() { this._value = ''; } get value() { return this._value; } set value(value) { this.resolve(value, { input: true }); } /** Resolve new value */ resolve(value, flags) { if (flags === void 0) { flags = { input: true }; } this.reset(); this.append(value, flags, ''); this.doCommit(); } get unmaskedValue() { return this.value; } set unmaskedValue(value) { this.resolve(value, {}); } get typedValue() { return this.parse ? this.parse(this.value, this) : this.unmaskedValue; } set typedValue(value) { if (this.format) { this.value = this.format(value, this); } else { this.unmaskedValue = String(value); } } /** Value that includes raw user input */ get rawInputValue() { return this.extractInput(0, this.displayValue.length, { raw: true }); } set rawInputValue(value) { this.resolve(value, { raw: true }); } get displayValue() { return this.value; } get isComplete() { return true; } get isFilled() { return this.isComplete; } /** Finds nearest input position in direction */ nearestInputPos(cursorPos, direction) { return cursorPos; } totalInputPositions(fromPos, toPos) { if (fromPos === void 0) { fromPos = 0; } if (toPos === void 0) { toPos = this.displayValue.length; } return Math.min(this.displayValue.length, toPos - fromPos); } /** Extracts value in range considering flags */ extractInput(fromPos, toPos, flags) { if (fromPos === void 0) { fromPos = 0; } if (toPos === void 0) { toPos = this.displayValue.length; } return this.displayValue.slice(fromPos, toPos); } /** Extracts tail in range */ extractTail(fromPos, toPos) { if (fromPos === void 0) { fromPos = 0; } if (toPos === void 0) { toPos = this.displayValue.length; } return new ContinuousTailDetails(this.extractInput(fromPos, toPos), fromPos); } /** Appends tail */ appendTail(tail) { if (isString(tail)) tail = new ContinuousTailDetails(String(tail)); return tail.appendTo(this); } /** Appends char */ _appendCharRaw(ch, flags) { if (!ch) return new ChangeDetails(); this._value += ch; return new ChangeDetails({ inserted: ch, rawInserted: ch }); } /** Appends char */ _appendChar(ch, flags, checkTail) { if (flags === void 0) { flags = {}; } const consistentState = this.state; let details; [ch, details] = this.doPrepareChar(ch, flags); if (ch) { details = details.aggregate(this._appendCharRaw(ch, flags)); // TODO handle `skip`? // try `autofix` lookahead if (!details.rawInserted && this.autofix === 'pad') { const noFixState = this.state; this.state = consistentState; let fixDetails = this.pad(flags); const chDetails = this._appendCharRaw(ch, flags); fixDetails = fixDetails.aggregate(chDetails); // if fix was applied or // if details are equal use skip restoring state optimization if (chDetails.rawInserted || fixDetails.equals(details)) { details = fixDetails; } else { this.state = noFixState; } } } if (details.inserted) { let consistentTail; let appended = this.doValidate(flags) !== false; if (appended && checkTail != null) { // validation ok, check tail const beforeTailState = this.state; if (this.overwrite === true) { consistentTail = checkTail.state; for (let i = 0; i < details.rawInserted.length; ++i) { checkTail.unshift(this.displayValue.length - details.tailShift); } } let tailDetails = this.appendTail(checkTail); appended = tailDetails.rawInserted.length === checkTail.toString().length; // not ok, try shift if (!(appended && tailDetails.inserted) && this.overwrite === 'shift') { this.state = beforeTailState; consistentTail = checkTail.state; for (let i = 0; i < details.rawInserted.length; ++i) { checkTail.shift(); } tailDetails = this.appendTail(checkTail); appended = tailDetails.rawInserted.length === checkTail.toString().length; } // if ok, rollback state after tail if (appended && tailDetails.inserted) this.state = beforeTailState; } // revert all if something went wrong if (!appended) { details = new ChangeDetails(); this.state = consistentState; if (checkTail && consistentTail) checkTail.state = consistentTail; } } return details; } /** Appends optional placeholder at the end */ _appendPlaceholder() { return new ChangeDetails(); } /** Appends optional eager placeholder at the end */ _appendEager() { return new ChangeDetails(); } /** Appends symbols considering flags */ append(str, flags, tail) { if (!isString(str)) throw new Error('value should be string'); const checkTail = isString(tail) ? new ContinuousTailDetails(String(tail)) : tail; if (flags != null && flags.tail) flags._beforeTailState = this.state; let details; [str, details] = this.doPrepare(str, flags); for (let ci = 0; ci < str.length; ++ci) { const d = this._appendChar(str[ci], flags, checkTail); if (!d.rawInserted && !this.doSkipInvalid(str[ci], flags, checkTail)) break; details.aggregate(d); } if ((this.eager === true || this.eager === 'append') && flags != null && flags.input && str) { details.aggregate(this._appendEager()); } // append tail but aggregate only tailShift if (checkTail != null) { details.tailShift += this.appendTail(checkTail).tailShift; // TODO it's a good idea to clear state after appending ends // but it causes bugs when one append calls another (when dynamic dispatch set rawInputValue) // this._resetBeforeTailState(); } return details; } remove(fromPos, toPos) { if (fromPos === void 0) { fromPos = 0; } if (toPos === void 0) { toPos = this.displayValue.length; } this._value = this.displayValue.slice(0, fromPos) + this.displayValue.slice(toPos); return new ChangeDetails(); } /** Calls function and reapplies current value */ withValueRefresh(fn) { if (this._refreshing || !this._initialized) return fn(); this._refreshing = true; const rawInput = this.rawInputValue; const value = this.value; const ret = fn(); this.rawInputValue = rawInput; // append lost trailing chars at the end if (this.value && this.value !== value && value.indexOf(this.value) === 0) { this.append(value.slice(this.displayValue.length), {}, ''); this.doCommit(); } delete this._refreshing; return ret; } runIsolated(fn) { if (this._isolated || !this._initialized) return fn(this); this._isolated = true; const state = this.state; const ret = fn(this); this.state = state; delete this._isolated; return ret; } doSkipInvalid(ch, flags, checkTail) { return Boolean(this.skipInvalid); } /** Prepares string before mask processing */ doPrepare(str, flags) { if (flags === void 0) { flags = {}; } return ChangeDetails.normalize(this.prepare ? this.prepare(str, this, flags) : str); } /** Prepares each char before mask processing */ doPrepareChar(str, flags) { if (flags === void 0) { flags = {}; } return ChangeDetails.normalize(this.prepareChar ? this.prepareChar(str, this, flags) : str); } /** Validates if value is acceptable */ doValidate(flags) { return (!this.validate || this.validate(this.value, this, flags)) && (!this.parent || this.parent.doValidate(flags)); } /** Does additional processing at the end of editing */ doCommit() { if (this.commit) this.commit(this.value, this); } splice(start, deleteCount, inserted, removeDirection, flags) { if (inserted === void 0) { inserted = ''; } if (removeDirection === void 0) { removeDirection = DIRECTION.NONE; } if (flags === void 0) { flags = { input: true }; } const tailPos = start + deleteCount; const tail = this.extractTail(tailPos); const eagerRemove = this.eager === true || this.eager === 'remove'; let oldRawValue; if (eagerRemove) { removeDirection = forceDirection(removeDirection); oldRawValue = this.extractInput(0, tailPos, { raw: true }); } let startChangePos = start; const details = new ChangeDetails(); // if it is just deletion without insertion if (removeDirection !== DIRECTION.NONE) { startChangePos = this.nearestInputPos(start, deleteCount > 1 && start !== 0 && !eagerRemove ? DIRECTION.NONE : removeDirection); // adjust tailShift if start was aligned details.tailShift = startChangePos - start; } details.aggregate(this.remove(startChangePos)); if (eagerRemove && removeDirection !== DIRECTION.NONE && oldRawValue === this.rawInputValue) { if (removeDirection === DIRECTION.FORCE_LEFT) { let valLength; while (oldRawValue === this.rawInputValue && (valLength = this.displayValue.length)) { details.aggregate(new ChangeDetails({ tailShift: -1 })).aggregate(this.remove(valLength - 1)); } } else if (removeDirection === DIRECTION.FORCE_RIGHT) { tail.unshift(); } } return details.aggregate(this.append(inserted, flags, tail)); } maskEquals(mask) { return this.mask === mask; } optionsIsChanged(opts) { return !objectIncludes(this, opts); } typedValueEquals(value) { const tval = this.typedValue; return value === tval || Masked.EMPTY_VALUES.includes(value) && Masked.EMPTY_VALUES.includes(tval) || (this.format ? this.format(value, this) === this.format(this.typedValue, this) : false); } pad(flags) { return new ChangeDetails(); } } Masked.DEFAULTS = { skipInvalid: true }; Masked.EMPTY_VALUES = [undefined, null, '']; IMask.Masked = Masked; class ChunksTailDetails { /** */ constructor(chunks, from) { if (chunks === void 0) { chunks = []; } if (from === void 0) { from = 0; } this.chunks = chunks; this.from = from; } toString() { return this.chunks.map(String).join(''); } extend(tailChunk) { if (!String(tailChunk)) return; tailChunk = isString(tailChunk) ? new ContinuousTailDetails(String(tailChunk)) : tailChunk; const lastChunk = this.chunks[this.chunks.length - 1]; const extendLast = lastChunk && ( // if stops are same or tail has no stop lastChunk.stop === tailChunk.stop || tailChunk.stop == null) && // if tail chunk goes just after last chunk tailChunk.from === lastChunk.from + lastChunk.toString().length; if (tailChunk instanceof ContinuousTailDetails) { // check the ability to extend previous chunk if (extendLast) { // extend previous chunk lastChunk.extend(tailChunk.toString()); } else { // append new chunk this.chunks.push(tailChunk); } } else if (tailChunk instanceof ChunksTailDetails) { if (tailChunk.stop == null) { // unwrap floating chunks to parent, keeping `from` pos let firstTailChunk; while (tailChunk.chunks.length && tailChunk.chunks[0].stop == null) { firstTailChunk = tailChunk.chunks.shift(); // not possible to be `undefined` because length was checked above firstTailChunk.from += tailChunk.from; this.extend(firstTailChunk); } } // if tail chunk still has value if (tailChunk.toString()) { // if chunks contains stops, then popup stop to container tailChunk.stop = tailChunk.blockIndex; this.chunks.push(tailChunk); } } } appendTo(masked) { if (!(masked instanceof IMask.MaskedPattern)) { const tail = new ContinuousTailDetails(this.toString()); return tail.appendTo(masked); } const details = new ChangeDetails(); for (let ci = 0; ci < this.chunks.length; ++ci) { const chunk = this.chunks[ci]; const lastBlockIter = masked._mapPosToBlock(masked.displayValue.length); const stop = chunk.stop; let chunkBlock; if (stop != null && ( // if block not found or stop is behind lastBlock !lastBlockIter || lastBlockIter.index <= stop)) { if (chunk instanceof ChunksTailDetails || // for continuous block also check if stop is exist masked._stops.indexOf(stop) >= 0) { details.aggregate(masked._appendPlaceholder(stop)); } chunkBlock = chunk instanceof ChunksTailDetails && masked._blocks[stop]; } if (chunkBlock) { const tailDetails = chunkBlock.appendTail(chunk); details.aggregate(tailDetails); // get not inserted chars const remainChars = chunk.toString().slice(tailDetails.rawInserted.length); if (remainChars) details.aggregate(masked.append(remainChars, { tail: true })); } else { details.aggregate(masked.append(chunk.toString(), { tail: true })); } } return details; } get state() { return { chunks: this.chunks.map(c => c.state), from: this.from, stop: this.stop, blockIndex: this.blockIndex }; } set state(state) { const { chunks, ...props } = state; Object.assign(this, props); this.chunks = chunks.map(cstate => { const chunk = "chunks" in cstate ? new ChunksTailDetails() : new ContinuousTailDetails(); chunk.state = cstate; return chunk; }); } unshift(beforePos) { if (!this.chunks.length || beforePos != null && this.from >= beforePos) return ''; const chunkShiftPos = beforePos != null ? beforePos - this.from : beforePos; let ci = 0; while (ci < this.chunks.length) { const chunk = this.chunks[ci]; const shiftChar = chunk.unshift(chunkShiftPos); if (chunk.toString()) { // chunk still contains value // but not shifted - means no more available chars to shift if (!shiftChar) break; ++ci; } else { // clean if chunk has no value this.chunks.splice(ci, 1); } if (shiftChar) return shiftChar; } return ''; } shift() { if (!this.chunks.length) return ''; let ci = this.chunks.length - 1; while (0 <= ci) { const chunk = this.chunks[ci]; const shiftChar = chunk.shift(); if (chunk.toString()) { // chunk still contains value // but not shifted - means no more available chars to shift if (!shiftChar) break; --ci; } else { // clean if chunk has no value this.chunks.splice(ci, 1); } if (shiftChar) return shiftChar; } return ''; } } class PatternCursor { constructor(masked, pos) { this.masked = masked; this._log = []; const { offset, index } = masked._mapPosToBlock(pos) || (pos < 0 ? // first { index: 0, offset: 0 } : // last { index: this.masked._blocks.length, offset: 0 }); this.offset = offset; this.index = index; this.ok = false; } get block() { return this.masked._blocks[this.index]; } get pos() { return this.masked._blockStartPos(this.index) + this.offset; } get state() { return { index: this.index, offset: this.offset, ok: this.ok }; } set state(s) { Object.assign(this, s); } pushState() { this._log.push(this.state); } popState() { const s = this._log.pop(); if (s) this.state = s; return s; } bindBlock() { if (this.block) return; if (this.index < 0) { this.index = 0; this.offset = 0; } if (this.index >= this.masked._blocks.length) { this.index = this.masked._blocks.length - 1; this.offset = this.block.displayValue.length; // TODO this is stupid type error, `block` depends on index that was changed above } } _pushLeft(fn) { this.pushState(); for (this.bindBlock(); 0 <= this.index; --this.index, this.offset = ((_this$block = this.block) == null ? void 0 : _this$block.displayValue.length) || 0) { var _this$block; if (fn()) return this.ok = true; } return this.ok = false; } _pushRight(fn) { this.pushState(); for (this.bindBlock(); this.index < this.masked._blocks.length; ++this.index, this.offset = 0) { if (fn()) return this.ok = true; } return this.ok = false; } pushLeftBeforeFilled() { return this._pushLeft(() => { if (this.block.isFixed || !this.block.value) return; this.offset