web-ui-pack
Version:
Web package with UI elements
415 lines (414 loc) • 15.5 kB
JavaScript
export default class MaskTextInput {
pattern;
options;
#ctr = this.constructor;
value = "";
prefix = "";
chunks = [];
lastChunk = { index: -1, pattern: "" };
isCompleted = false;
isCompletedFull = false;
leftLength = 0;
constructor(pattern, rawValue, options) {
this.pattern = pattern;
this.options = options;
this.options = { prediction: true, lazy: true, ...options };
this.parse(rawValue);
this.prefix = !this.chunks[0].isVar ? this.chunks[0].pattern : "";
}
static testDigit(str, charIndex) {
const ascii = str.charCodeAt(charIndex);
const isNum = ascii > 47 && ascii < 58;
return isNum;
}
static testAnyChar() {
return true;
}
static parsePattern(pattern) {
const chunks = [];
let lastChunk = null;
const setToChunk = (char, isVar, testFn) => {
if (!lastChunk || lastChunk.test !== testFn) {
lastChunk = { pattern: char };
if (isVar) {
lastChunk.test = testFn;
lastChunk.isVar = true;
lastChunk.max = 0;
lastChunk.min = 0;
lastChunk.value = "";
}
lastChunk.index = chunks.push(lastChunk) - 1;
}
else {
lastChunk.pattern += char;
}
return lastChunk;
};
const pr = pattern
.replace(/([^|]|^)\|0/g, "$1\x00")
.replace(/([^|]|^)\|#/g, "$1\x01")
.replace(/([^|]|^)\|\*/g, "$1\x02")
.replace(/([^|]|^)\|\//g, "$1\x03")
.replace(/\/\//g, "\x04")
.replace(/\|\|/g, "|");
for (let i = 0; i < pr.length; ++i) {
const p = pr[i];
switch (p) {
case "*":
++setToChunk(p, true, this.testAnyChar).max;
++lastChunk.min;
break;
case "0":
++setToChunk(p, true, this.testDigit).max;
++lastChunk.min;
break;
case "#":
++setToChunk(p, true, this.testDigit).max;
break;
case "\x04":
{
const end = pr.indexOf("\x04", i + 1);
if (end === -1) {
setToChunk("//");
break;
}
const reg = new RegExp(pr.substring(i + 1, end));
++setToChunk("*", true, (s, si) => {
let v = s[si];
if (reg.test(v)) {
return true;
}
v = s[si].toLowerCase();
if (reg.test(v)) {
return { value: v };
}
v = s[si].toUpperCase();
if (reg.test(v)) {
return { value: v };
}
return false;
}).max;
++lastChunk.min;
i = end;
}
break;
case "\x00":
setToChunk("0");
break;
case "\x01":
setToChunk("#");
break;
case "\x02":
setToChunk("*");
break;
case "\x03":
setToChunk("/");
break;
case "{": {
if (lastChunk.isVar) {
const end = pr.indexOf("}", i + 1);
if (end === -1) {
setToChunk(p);
break;
}
const [min, max] = pr.substring(i + 1, end).split(/, */);
const lc = lastChunk;
lc.min = Math.max(lc.min, +min);
lc.max = Math.max(lc.max, lc.min, +(max ?? 0));
lc.pattern = lc.pattern.repeat(lc.max);
i = end;
break;
}
setToChunk(p);
break;
}
default:
setToChunk(p);
break;
}
}
if (chunks.some((c, i, arr) => i > 0 && c.isVar === arr[i - 1].isVar)) {
const msg = `MaskInput. Pattern ${pattern} is wrong: static & variable chunks must alternate`;
const details = { chunks, pattern };
console.warn(msg, details);
throw new SyntaxError(msg);
}
return chunks;
}
parse(value, lazy, caret) {
const chunks = MaskTextInput.parsePattern(this.pattern);
lazy ??= this.options.lazy;
caret ??= 0;
const caretWas = caret;
let canShift = null;
let pi = 0;
for (let i = 0; pi < chunks.length; ++pi) {
const chunk = chunks[pi];
chunk.isTouched = true;
if (chunk.isVar) {
chunk.value = "";
for (let ci = 0; ci < chunk.max && i < value.length; ++ci, ++i) {
const char = value[i];
const r = chunk.test(value, i);
if (r) {
if (chunk.isCompleted && chunks[pi + 1]?.pattern[0] === char) {
break;
}
chunk.value += r.value || value[i];
chunk.isCompleted = chunk.value.length >= chunk.min;
}
else if (canShift != null && chunks[pi - 1].pattern[0] === char) {
const prev = chunks[pi - 1];
while (prev.pattern.length > ++canShift && prev.pattern[canShift] === value[i + 1]) {
++i;
}
--ci;
continue;
}
else if (chunk.isCompleted && chunks[pi + 1]?.pattern[0] === char) {
break;
}
else if (lazy && /[., _+-/\\]/.test(char) && ci && chunk.test === this.#ctr.testDigit) {
const cnt = chunk.min - ci;
if (cnt > 0) {
chunk.value = "0".repeat(chunk.min - ci) + chunk.value;
}
chunk.isCompleted = true;
break;
}
else {
--ci;
if (i < caretWas) {
--caret;
}
}
}
}
else {
canShift = 0;
for (let ci = 0; ci < chunk.pattern.length && i < value.length; ++ci) {
if (chunk.pattern[ci] === value[i] || /[., _+-/\\]/.test(value[i])) {
++i;
canShift = null;
}
else if (i < caretWas) {
++caret;
}
}
}
if (i === value.length) {
break;
}
}
this.chunks = chunks;
this.updateState();
caret = Math.min(this.value.length, caret);
return caret;
}
updateState() {
let l = this.chunks.findIndex((c) => !c.isTouched) - 1;
const endIndex = this.chunks.length - 1;
if (l === -2) {
l = endIndex;
}
else if (l === endIndex - 1 && this.chunks[l].isCompleted) {
++l;
}
else if (this.options.prediction && l !== endIndex) {
const last = this.chunks[l];
if (last.isVar && last.max === last.value.length) {
++l;
}
}
this.lastChunk = this.chunks[l];
this.lastChunk.isTouched = true;
const last = this.lastChunk;
this.leftLength = last.isVar ? last.max - last.value.length : 0;
for (let i = last.index + 1; i <= endIndex; ++i) {
this.leftLength += this.chunks[i].pattern.length;
}
this.value = "";
for (let i = 0; i <= last.index; ++i) {
this.value += this.chunks[i].value ?? this.chunks[i].pattern;
}
const isLeftRequired = this.chunks.some((c, i) => i > last.index && c.min);
this.isCompleted = (last.index === endIndex || !isLeftRequired) && (!last.isVar || !!last.isCompleted);
this.isCompletedFull = this.isCompleted && last.index === endIndex;
}
handleBeforeInput(e) {
const el = e.target;
if (el.selectionStart !== el.selectionEnd) {
delete el._maskPrev;
return;
}
let pos = el.selectionStart || 0;
switch (e.inputType) {
case "insertText":
case "insertFromPaste": {
const nPos = this.adjustCaret(pos);
nPos !== pos && el.setSelectionRange(nPos, nPos);
pos = nPos;
}
}
el._maskPrev = {
position: el.selectionStart || 0,
value: el.value,
insertText: e.data,
};
}
handleInput(e) {
const el = e.target;
const v = el.value;
let pos = el.selectionStart || 0;
let declinedAdd = 0;
let isProcessed = false;
const saved = el._maskPrev;
if (saved) {
switch (e.inputType) {
case "deleteContentForward":
pos = this.deleteAfter(saved.position);
declinedAdd = Math.min(v.length - this.value.length, 0);
isProcessed = true;
break;
case "deleteContentBackward":
pos = this.deleteBefore(saved.position);
declinedAdd = Math.min(v.length - this.value.length, 0);
isProcessed = true;
break;
default:
break;
}
}
if (!isProcessed) {
const lazy = e.inputType !== "insertText" ? false : this.options.lazy;
pos = this.parse(v, lazy, pos);
pos = this.adjustCaret(pos);
declinedAdd = Math.max(v.length - this.value.length, 0);
}
delete el._maskPrev;
return { position: pos, declinedAdd };
}
findChunkByCaret(pos) {
const chunk = this.chunks.find((c) => {
pos -= (c.value ?? c.pattern).length;
return pos <= 0;
});
return { chunk, posChunk: (chunk.value ?? chunk.pattern).length + pos };
}
adjustCaret(pos) {
const r = this.findChunkByCaret(pos);
if (r.chunk.isVar &&
r.chunk.value.length === r.chunk.max &&
r.posChunk === r.chunk.max &&
r.chunk.index < this.chunks.length - 2) {
pos += r.chunk.max - r.posChunk;
r.chunk = this.chunks[r.chunk.index + 1];
r.posChunk = r.chunk.pattern.length;
pos += r.posChunk;
}
if (!r.chunk.isVar) {
if (r.chunk.index === this.chunks.length - 1) {
pos -= r.posChunk;
}
else {
const shiftRight = (r.chunk.value ?? r.chunk.pattern).length - r.posChunk;
pos += shiftRight;
}
}
return pos;
}
resetChunk(c) {
if (!c) {
return;
}
delete c.isTouched;
if (c.isVar) {
c.value = "";
}
}
deleteChar(pos, isBefore) {
let { chunk, posChunk } = this.findChunkByCaret(pos);
if (!isBefore && chunk.isVar && chunk.value.length === posChunk && this.chunks[chunk.index + 1]) {
chunk = this.chunks[chunk.index + 1];
posChunk = 0;
}
if (!chunk.isVar) {
if (chunk.index === 0 && isBefore) {
return chunk.pattern.length;
}
const next = this.chunks[chunk.index + 1];
const prev = this.chunks[chunk.index - 1];
const isPrevPartial = prev && prev.value.length !== prev.max;
const isNextEmpty = !next?.value || !next.isTouched;
if (isBefore) {
const canDel = !next || isNextEmpty;
if (canDel) {
this.resetChunk(next);
this.resetChunk(chunk);
}
pos -= posChunk;
posChunk = prev.value.length;
const isLast = chunk.index === this.chunks.length - 1;
const canDelPrev = canDel ? !isPrevPartial || isLast : true;
if (canDelPrev) {
chunk = prev;
}
}
else {
const canDel = next && isNextEmpty && isPrevPartial;
if (canDel) {
this.resetChunk(chunk);
}
else if (next) {
pos += chunk.pattern.length - posChunk;
posChunk = 0;
chunk = next;
}
}
}
if (chunk.isVar) {
if (isBefore) {
--pos;
--posChunk;
}
if (chunk.value.length === 1) {
const nextIsPostfix = chunk.index === this.chunks.length - 2;
const nextVal = this.value.substring(0, pos) + (nextIsPostfix ? "" : this.value.substring(pos + 1));
this.parse(nextVal, false);
return pos;
}
chunk.value = chunk.value.substring(0, posChunk) + chunk.value.substring(posChunk + 1);
chunk.isCompleted = chunk.value.length >= chunk.min;
if (!chunk.isCompleted) {
let prev = chunk;
for (let i = chunk.index + 2; i < this.chunks.length; i += 2) {
const next = this.chunks[i];
if (!next.value.length || !next.isTouched || !prev.test(next.value, 0)) {
this.resetChunk(next);
break;
}
prev.value += next.value[0];
next.value = next.value.substring(1);
prev = next;
}
if (prev !== chunk) {
chunk.isCompleted = true;
prev.isCompleted = prev.value.length >= prev.min;
!prev.isCompleted && this.resetChunk(this.chunks[prev.index + 1]);
}
else {
const next = this.chunks[chunk.index + 1];
next && delete next.isTouched;
}
}
}
this.updateState();
return pos;
}
deleteAfter(position) {
return this.deleteChar(position, false);
}
deleteBefore(position) {
return this.deleteChar(position, true);
}
}