imask
Version:
vanilla javascript input mask
1,605 lines (1,449 loc) • 117 kB
JavaScript
'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