imask
Version:
vanilla javascript input mask
465 lines (442 loc) • 15.1 kB
JavaScript
import ChangeDetails from '../core/change-details.js';
import IMask from '../core/holder.js';
import { DIRECTION } from '../core/utils.js';
import Masked from './base.js';
import createMask, { normalizeOpts } from './factory.js';
import ChunksTailDetails from './pattern/chunk-tail-details.js';
import PatternCursor from './pattern/cursor.js';
import PatternFixedDefinition from './pattern/fixed-definition.js';
import PatternInputDefinition from './pattern/input-definition.js';
import './regexp.js';
import '../core/continuous-tail-details.js';
/** Pattern mask */
class MaskedPattern extends Masked {
/** */
/** */
/** Single char for empty input */
/** Single char for filled input */
/** Show placeholder only when needed */
/** Enable characters overwriting */
/** */
/** */
/** */
constructor(opts) {
super({
...MaskedPattern.DEFAULTS,
...opts,
definitions: Object.assign({}, PatternInputDefinition.DEFAULT_DEFINITIONS, opts == null ? void 0 : opts.definitions)
});
}
updateOptions(opts) {
super.updateOptions(opts);
}
_update(opts) {
opts.definitions = Object.assign({}, this.definitions, opts.definitions);
super._update(opts);
this._rebuildMask();
}
_rebuildMask() {
const defs = this.definitions;
this._blocks = [];
this.exposeBlock = undefined;
this._stops = [];
this._maskedBlocks = {};
const pattern = this.mask;
if (!pattern || !defs) return;
let unmaskingBlock = false;
let optionalBlock = false;
for (let i = 0; i < pattern.length; ++i) {
if (this.blocks) {
const p = pattern.slice(i);
const bNames = Object.keys(this.blocks).filter(bName => p.indexOf(bName) === 0);
// order by key length
bNames.sort((a, b) => b.length - a.length);
// use block name with max length
const bName = bNames[0];
if (bName) {
const {
expose,
repeat,
...bOpts
} = normalizeOpts(this.blocks[bName]); // TODO type Opts<Arg & Extra>
const blockOpts = {
lazy: this.lazy,
eager: this.eager,
placeholderChar: this.placeholderChar,
displayChar: this.displayChar,
overwrite: this.overwrite,
autofix: this.autofix,
...bOpts,
repeat,
parent: this
};
const maskedBlock = repeat != null ? new IMask.RepeatBlock(blockOpts /* TODO */) : createMask(blockOpts);
if (maskedBlock) {
this._blocks.push(maskedBlock);
if (expose) this.exposeBlock = maskedBlock;
// store block index
if (!this._maskedBlocks[bName]) this._maskedBlocks[bName] = [];
this._maskedBlocks[bName].push(this._blocks.length - 1);
}
i += bName.length - 1;
continue;
}
}
let char = pattern[i];
let isInput = (char in defs);
if (char === MaskedPattern.STOP_CHAR) {
this._stops.push(this._blocks.length);
continue;
}
if (char === '{' || char === '}') {
unmaskingBlock = !unmaskingBlock;
continue;
}
if (char === '[' || char === ']') {
optionalBlock = !optionalBlock;
continue;
}
if (char === MaskedPattern.ESCAPE_CHAR) {
++i;
char = pattern[i];
if (!char) break;
isInput = false;
}
const def = isInput ? new PatternInputDefinition({
isOptional: optionalBlock,
lazy: this.lazy,
eager: this.eager,
placeholderChar: this.placeholderChar,
displayChar: this.displayChar,
...normalizeOpts(defs[char]),
parent: this
}) : new PatternFixedDefinition({
char,
eager: this.eager,
isUnmasking: unmaskingBlock
});
this._blocks.push(def);
}
}
get state() {
return {
...super.state,
_blocks: this._blocks.map(b => b.state)
};
}
set state(state) {
if (!state) {
this.reset();
return;
}
const {
_blocks,
...maskedState
} = state;
this._blocks.forEach((b, bi) => b.state = _blocks[bi]);
super.state = maskedState;
}
reset() {
super.reset();
this._blocks.forEach(b => b.reset());
}
get isComplete() {
return this.exposeBlock ? this.exposeBlock.isComplete : this._blocks.every(b => b.isComplete);
}
get isFilled() {
return this._blocks.every(b => b.isFilled);
}
get isFixed() {
return this._blocks.every(b => b.isFixed);
}
get isOptional() {
return this._blocks.every(b => b.isOptional);
}
doCommit() {
this._blocks.forEach(b => b.doCommit());
super.doCommit();
}
get unmaskedValue() {
return this.exposeBlock ? this.exposeBlock.unmaskedValue : this._blocks.reduce((str, b) => str += b.unmaskedValue, '');
}
set unmaskedValue(unmaskedValue) {
if (this.exposeBlock) {
const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length);
this.exposeBlock.unmaskedValue = unmaskedValue;
this.appendTail(tail);
this.doCommit();
} else super.unmaskedValue = unmaskedValue;
}
get value() {
return this.exposeBlock ? this.exposeBlock.value :
// TODO return _value when not in change?
this._blocks.reduce((str, b) => str += b.value, '');
}
set value(value) {
if (this.exposeBlock) {
const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length);
this.exposeBlock.value = value;
this.appendTail(tail);
this.doCommit();
} else super.value = value;
}
get typedValue() {
return this.exposeBlock ? this.exposeBlock.typedValue : super.typedValue;
}
set typedValue(value) {
if (this.exposeBlock) {
const tail = this.extractTail(this._blockStartPos(this._blocks.indexOf(this.exposeBlock)) + this.exposeBlock.displayValue.length);
this.exposeBlock.typedValue = value;
this.appendTail(tail);
this.doCommit();
} else super.typedValue = value;
}
get displayValue() {
return this._blocks.reduce((str, b) => str += b.displayValue, '');
}
appendTail(tail) {
return super.appendTail(tail).aggregate(this._appendPlaceholder());
}
_appendEager() {
var _this$_mapPosToBlock;
const details = new ChangeDetails();
let startBlockIndex = (_this$_mapPosToBlock = this._mapPosToBlock(this.displayValue.length)) == null ? void 0 : _this$_mapPosToBlock.index;
if (startBlockIndex == null) return details;
// TODO test if it works for nested pattern masks
if (this._blocks[startBlockIndex].isFilled) ++startBlockIndex;
for (let bi = startBlockIndex; bi < this._blocks.length; ++bi) {
const d = this._blocks[bi]._appendEager();
if (!d.inserted) break;
details.aggregate(d);
}
return details;
}
_appendCharRaw(ch, flags) {
if (flags === void 0) {
flags = {};
}
const blockIter = this._mapPosToBlock(this.displayValue.length);
const details = new ChangeDetails();
if (!blockIter) return details;
for (let bi = blockIter.index, block; block = this._blocks[bi]; ++bi) {
var _flags$_beforeTailSta;
const blockDetails = block._appendChar(ch, {
...flags,
_beforeTailState: (_flags$_beforeTailSta = flags._beforeTailState) == null || (_flags$_beforeTailSta = _flags$_beforeTailSta._blocks) == null ? void 0 : _flags$_beforeTailSta[bi]
});
details.aggregate(blockDetails);
if (blockDetails.consumed) break; // go next char
}
return details;
}
extractTail(fromPos, toPos) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
const chunkTail = new ChunksTailDetails();
if (fromPos === toPos) return chunkTail;
this._forEachBlocksInRange(fromPos, toPos, (b, bi, bFromPos, bToPos) => {
const blockChunk = b.extractTail(bFromPos, bToPos);
blockChunk.stop = this._findStopBefore(bi);
blockChunk.from = this._blockStartPos(bi);
if (blockChunk instanceof ChunksTailDetails) blockChunk.blockIndex = bi;
chunkTail.extend(blockChunk);
});
return chunkTail;
}
extractInput(fromPos, toPos, flags) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
if (flags === void 0) {
flags = {};
}
if (fromPos === toPos) return '';
let input = '';
this._forEachBlocksInRange(fromPos, toPos, (b, _, fromPos, toPos) => {
input += b.extractInput(fromPos, toPos, flags);
});
return input;
}
_findStopBefore(blockIndex) {
let stopBefore;
for (let si = 0; si < this._stops.length; ++si) {
const stop = this._stops[si];
if (stop <= blockIndex) stopBefore = stop;else break;
}
return stopBefore;
}
/** Appends placeholder depending on laziness */
_appendPlaceholder(toBlockIndex) {
const details = new ChangeDetails();
if (this.lazy && toBlockIndex == null) return details;
const startBlockIter = this._mapPosToBlock(this.displayValue.length);
if (!startBlockIter) return details;
const startBlockIndex = startBlockIter.index;
const endBlockIndex = toBlockIndex != null ? toBlockIndex : this._blocks.length;
this._blocks.slice(startBlockIndex, endBlockIndex).forEach(b => {
if (!b.lazy || toBlockIndex != null) {
var _blocks2;
details.aggregate(b._appendPlaceholder((_blocks2 = b._blocks) == null ? void 0 : _blocks2.length));
}
});
return details;
}
/** Finds block in pos */
_mapPosToBlock(pos) {
let accVal = '';
for (let bi = 0; bi < this._blocks.length; ++bi) {
const block = this._blocks[bi];
const blockStartPos = accVal.length;
accVal += block.displayValue;
if (pos <= accVal.length) {
return {
index: bi,
offset: pos - blockStartPos
};
}
}
}
_blockStartPos(blockIndex) {
return this._blocks.slice(0, blockIndex).reduce((pos, b) => pos += b.displayValue.length, 0);
}
_forEachBlocksInRange(fromPos, toPos, fn) {
if (toPos === void 0) {
toPos = this.displayValue.length;
}
const fromBlockIter = this._mapPosToBlock(fromPos);
if (fromBlockIter) {
const toBlockIter = this._mapPosToBlock(toPos);
// process first block
const isSameBlock = toBlockIter && fromBlockIter.index === toBlockIter.index;
const fromBlockStartPos = fromBlockIter.offset;
const fromBlockEndPos = toBlockIter && isSameBlock ? toBlockIter.offset : this._blocks[fromBlockIter.index].displayValue.length;
fn(this._blocks[fromBlockIter.index], fromBlockIter.index, fromBlockStartPos, fromBlockEndPos);
if (toBlockIter && !isSameBlock) {
// process intermediate blocks
for (let bi = fromBlockIter.index + 1; bi < toBlockIter.index; ++bi) {
fn(this._blocks[bi], bi, 0, this._blocks[bi].displayValue.length);
}
// process last block
fn(this._blocks[toBlockIter.index], toBlockIter.index, 0, toBlockIter.offset);
}
}
}
remove(fromPos, toPos) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
const removeDetails = super.remove(fromPos, toPos);
this._forEachBlocksInRange(fromPos, toPos, (b, _, bFromPos, bToPos) => {
removeDetails.aggregate(b.remove(bFromPos, bToPos));
});
return removeDetails;
}
nearestInputPos(cursorPos, direction) {
if (direction === void 0) {
direction = DIRECTION.NONE;
}
if (!this._blocks.length) return 0;
const cursor = new PatternCursor(this, cursorPos);
if (direction === DIRECTION.NONE) {
// -------------------------------------------------
// NONE should only go out from fixed to the right!
// -------------------------------------------------
if (cursor.pushRightBeforeInput()) return cursor.pos;
cursor.popState();
if (cursor.pushLeftBeforeInput()) return cursor.pos;
return this.displayValue.length;
}
// FORCE is only about a|* otherwise is 0
if (direction === DIRECTION.LEFT || direction === DIRECTION.FORCE_LEFT) {
// try to break fast when *|a
if (direction === DIRECTION.LEFT) {
cursor.pushRightBeforeFilled();
if (cursor.ok && cursor.pos === cursorPos) return cursorPos;
cursor.popState();
}
// forward flow
cursor.pushLeftBeforeInput();
cursor.pushLeftBeforeRequired();
cursor.pushLeftBeforeFilled();
// backward flow
if (direction === DIRECTION.LEFT) {
cursor.pushRightBeforeInput();
cursor.pushRightBeforeRequired();
if (cursor.ok && cursor.pos <= cursorPos) return cursor.pos;
cursor.popState();
if (cursor.ok && cursor.pos <= cursorPos) return cursor.pos;
cursor.popState();
}
if (cursor.ok) return cursor.pos;
if (direction === DIRECTION.FORCE_LEFT) return 0;
cursor.popState();
if (cursor.ok) return cursor.pos;
cursor.popState();
if (cursor.ok) return cursor.pos;
return 0;
}
if (direction === DIRECTION.RIGHT || direction === DIRECTION.FORCE_RIGHT) {
// forward flow
cursor.pushRightBeforeInput();
cursor.pushRightBeforeRequired();
if (cursor.pushRightBeforeFilled()) return cursor.pos;
if (direction === DIRECTION.FORCE_RIGHT) return this.displayValue.length;
// backward flow
cursor.popState();
if (cursor.ok) return cursor.pos;
cursor.popState();
if (cursor.ok) return cursor.pos;
return this.nearestInputPos(cursorPos, DIRECTION.LEFT);
}
return cursorPos;
}
totalInputPositions(fromPos, toPos) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
let total = 0;
this._forEachBlocksInRange(fromPos, toPos, (b, _, bFromPos, bToPos) => {
total += b.totalInputPositions(bFromPos, bToPos);
});
return total;
}
/** Get block by name */
maskedBlock(name) {
return this.maskedBlocks(name)[0];
}
/** Get all blocks by name */
maskedBlocks(name) {
const indices = this._maskedBlocks[name];
if (!indices) return [];
return indices.map(gi => this._blocks[gi]);
}
pad(flags) {
const details = new ChangeDetails();
this._forEachBlocksInRange(0, this.displayValue.length, b => details.aggregate(b.pad(flags)));
return details;
}
}
MaskedPattern.DEFAULTS = {
...Masked.DEFAULTS,
lazy: true,
placeholderChar: '_'
};
MaskedPattern.STOP_CHAR = '`';
MaskedPattern.ESCAPE_CHAR = '\\';
MaskedPattern.InputDefinition = PatternInputDefinition;
MaskedPattern.FixedDefinition = PatternFixedDefinition;
IMask.MaskedPattern = MaskedPattern;
export { MaskedPattern as default };