UNPKG

imask

Version:

vanilla javascript input mask

423 lines (375 loc) 11.7 kB
import ChangeDetails from '../core/change-details.js'; import ContinuousTailDetails from '../core/continuous-tail-details.js'; import { isString, DIRECTION, objectIncludes, forceDirection } from '../core/utils.js'; import IMask from '../core/holder.js'; /** 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; export { Masked as default };