imask
Version:
vanilla javascript input mask
375 lines (312 loc) • 10.8 kB
JavaScript
import {conform, DIRECTION, indexInDirection} from '../core/utils.js';
import Masked from './base.js';
import PatternDefinition from './pattern/definition.js';
import PatternGroup from './pattern/group.js';
export default
class MaskedPattern extends Masked {
constructor (opts={}) {
opts.placeholder = Object.assign({}, MaskedPattern.DEFAULT_PLACEHOLDER, opts.placeholder);
opts.definitions = Object.assign({}, PatternDefinition.DEFAULTS, opts.definitions);
super(opts);
}
_update (opts) {
opts.placeholder = Object.assign({}, this.placeholder, opts.placeholder);
opts.definitions = Object.assign({}, this.definitions, opts.definitions);
super._update(opts);
this._updateMask();
}
_updateMask () {
const defs = this.definitions;
this._charDefs = [];
this._groupDefs = [];
let pattern = this.mask;
if (!pattern || !defs) return;
let unmaskingBlock = false;
let optionalBlock = false;
let stopAlign = false;
for (let i=0; i<pattern.length; ++i) {
if (this.groups) {
const p = pattern.slice(i);
const gNames = Object.keys(this.groups).filter(gName => p.indexOf(gName) === 0);
// order by key length
gNames.sort((a, b) => b.length - a.length);
// use group name with max length
const gName = gNames[0];
if (gName) {
const group = this.groups[gName];
this._groupDefs.push(new PatternGroup(this, {
name: gName,
offset: this._charDefs.length,
mask: group.mask,
validate: group.validate
}));
pattern = pattern.replace(gName, group.mask);
}
}
let char = pattern[i];
let type = !unmaskingBlock && char in defs ?
PatternDefinition.TYPES.INPUT :
PatternDefinition.TYPES.FIXED;
const unmasking = type === PatternDefinition.TYPES.INPUT || unmaskingBlock;
const optional = type === PatternDefinition.TYPES.INPUT && optionalBlock;
if (char === MaskedPattern.STOP_CHAR) {
stopAlign = true;
continue;
}
if (char === '{' || char === '}') {
unmaskingBlock = !unmaskingBlock;
continue;
}
if (char === '[' || char === ']') {
optionalBlock = !optionalBlock;
continue;
}
if (char === MaskedPattern.ESCAPE_CHAR) {
++i;
char = pattern[i];
// TODO validation
if (!char) break;
type = PatternDefinition.TYPES.FIXED;
}
this._charDefs.push(new PatternDefinition({
char,
type,
optional,
stopAlign,
unmasking,
mask: type === PatternDefinition.TYPES.INPUT ?
defs[char] :
(value => value === char)
}));
stopAlign = false;
}
}
doValidate (soft) {
return this._groupDefs.every(g => g.doValidate(soft)) && super.doValidate(soft);
}
clone () {
const m = new MaskedPattern(this);
m._value = this.value;
m._charDefs.forEach((d, i) => Object.assign(d, this._charDefs[i]));
m._groupDefs.forEach((d, i) => Object.assign(d, this._groupDefs[i]));
return m;
}
reset () {
super.reset();
this._charDefs.forEach(d => {delete d.isHollow;});
}
get isComplete () {
return !this._charDefs.some((d, i) =>
d.isInput && !d.optional && (d.isHollow || !this.extractInput(i, i+1)));
}
hiddenHollowsBefore (defIndex) {
return this._charDefs
.slice(0, defIndex)
.filter(d => d.isHiddenHollow)
.length;
}
mapDefIndexToPos (defIndex) {
if (defIndex == null) return;
return defIndex - this.hiddenHollowsBefore(defIndex);
}
mapPosToDefIndex (pos) {
if (pos == null) return;
let defIndex = pos;
for (let di=0; di<this._charDefs.length; ++di) {
const def = this._charDefs[di];
if (di >= defIndex) break;
if (def.isHiddenHollow) ++defIndex;
}
return defIndex;
}
_unmask () {
const str = this.value;
let unmasked = '';
for (let ci=0, di=0; ci<str.length && di<this._charDefs.length; ++di) {
const ch = str[ci];
const def = this._charDefs[di];
if (def.isHiddenHollow) continue;
if (def.unmasking && !def.isHollow) unmasked += ch;
++ci;
}
return unmasked;
}
_appendTail (tail) {
return (!tail || this._appendChunks(tail)) && this._appendPlaceholder();
}
_append (str, soft) {
const oldValueLength = this.value.length;
for (let ci=0, di=this.mapPosToDefIndex(this.value.length); ci < str.length;) {
const ch = str[ci];
const def = this._charDefs[di];
// check overflow
if (!def) return false;
// reset
def.isHollow = false;
let resolved, skipped;
let chres = conform(def.resolve(ch), ch);
if (def.type === PatternDefinition.TYPES.INPUT) {
if (chres) {
this._value += chres;
if (!this.doValidate()) {
chres = '';
this._value = this.value.slice(0, -1);
}
}
resolved = !!chres;
skipped = !chres && !def.optional;
if (!chres) {
if (!def.optional && !soft) {
this._value += this.placeholder.char;
skipped = false;
}
if (!skipped) def.isHollow = true;
}
} else {
this._value += def.char;
resolved = chres && (def.unmasking || soft);
}
if (!skipped) ++di;
if (resolved || skipped) ++ci;
}
return this.value.length - oldValueLength;
}
_appendChunks (chunks, soft) {
for (let ci=0; ci < chunks.length; ++ci) {
const [fromDefIndex, input] = chunks[ci];
if (fromDefIndex != null) this._appendPlaceholder(fromDefIndex);
if (this._append(input, soft) === false) return false;
}
return true;
}
extractTail (fromPos, toPos) {
return this.extractInputChunks(fromPos, toPos);
}
extractInput (fromPos=0, toPos=this.value.length) {
// TODO fromPos === toPos
const str = this.value;
let input = '';
const toDefIndex = this.mapPosToDefIndex(toPos);
for (
let ci=fromPos, di=this.mapPosToDefIndex(fromPos);
ci<toPos && ci<str.length && di < toDefIndex;
++di)
{
const ch = str[ci];
const def = this._charDefs[di];
if (!def) break;
if (def.isHiddenHollow) continue;
if (def.isInput && !def.isHollow) input += ch;
++ci;
}
return input;
}
extractInputChunks (fromPos=0, toPos=this.value.length) {
// TODO fromPos === toPos
const fromDefIndex = this.mapPosToDefIndex(fromPos);
const toDefIndex = this.mapPosToDefIndex(toPos);
const stopDefIndices = this._charDefs
.map((d, i) => [d, i])
.slice(fromDefIndex, toDefIndex)
.filter(([d]) => d.stopAlign)
.map(([, i]) => i);
const stops = [
fromDefIndex,
...stopDefIndices,
toDefIndex
];
return stops.map((s, i) => [
stopDefIndices.indexOf(s) >= 0 ?
s :
null,
this.extractInput(
this.mapDefIndexToPos(s),
this.mapDefIndexToPos(stops[++i]))
]).filter(([stop, input]) => stop != null || input);
}
_appendPlaceholder (toDefIndex) {
const maxDefIndex = toDefIndex || this._charDefs.length;
for (let di=this.mapPosToDefIndex(this.value.length); di < maxDefIndex; ++di) {
const def = this._charDefs[di];
if (def.isInput) def.isHollow = true;
if (!this.placeholder.lazy || toDefIndex) {
this._value += !def.isInput ?
def.char :
!def.optional ?
this.placeholder.char :
'';
}
}
}
clear (from=0, to=this.value.length) {
this._value = this.value.slice(0, from) + this.value.slice(to);
const fromDefIndex = this.mapPosToDefIndex(from);
const toDefIndex = this.mapPosToDefIndex(to);
this._charDefs
.slice(fromDefIndex, toDefIndex)
.forEach(d => d.reset());
}
nearestInputPos (cursorPos, direction=DIRECTION.LEFT) {
if (!direction) return cursorPos;
const initialDefIndex = this.mapPosToDefIndex(cursorPos);
let di = initialDefIndex;
let firstInputIndex,
firstFilledInputIndex,
firstVisibleHollowIndex,
nextdi;
// search forward
for (nextdi = indexInDirection(di, direction); 0 <= nextdi && nextdi < this._charDefs.length; di += direction, nextdi += direction) {
const nextDef = this._charDefs[nextdi];
if (firstInputIndex == null && nextDef.isInput) firstInputIndex = di;
if (firstVisibleHollowIndex == null && nextDef.isHollow && !nextDef.isHiddenHollow) firstVisibleHollowIndex = di;
if (nextDef.isInput && !nextDef.isHollow) {
firstFilledInputIndex = di;
break;
}
}
if (direction === DIRECTION.LEFT || firstInputIndex == null) {
// search backwards
direction = -direction;
let overflow = false;
// find hollows only before initial pos
for (nextdi = indexInDirection(di, direction);
0 <= nextdi && nextdi < this._charDefs.length;
di += direction, nextdi += direction)
{
const nextDef = this._charDefs[nextdi];
if (nextDef.isInput) {
firstInputIndex = di;
if (nextDef.isHollow && !nextDef.isHiddenHollow) break;
}
// if hollow not found before start position - set `overflow`
// and try to find just any input
if (di === initialDefIndex) overflow = true;
// first input found
if (overflow && firstInputIndex != null) break;
}
// process overflow
overflow = overflow || nextdi >= this._charDefs.length;
if (overflow && firstInputIndex != null) di = firstInputIndex;
} else if (firstFilledInputIndex == null) {
// adjust index if delete at right and filled input not found at right
di = firstVisibleHollowIndex != null ?
firstVisibleHollowIndex :
firstInputIndex;
}
return this.mapDefIndexToPos(di);
}
group (name) {
return this.groupsByName(name)[0];
}
groupsByName (name) {
return this._groupDefs.filter(g => g.name === name);
}
}
MaskedPattern.DEFAULT_PLACEHOLDER = {
lazy: true,
char: '_'
};
MaskedPattern.STOP_CHAR = '`';
MaskedPattern.ESCAPE_CHAR = '\\';
MaskedPattern.Definition = PatternDefinition;
MaskedPattern.Group = PatternGroup;