UNPKG

web-ui-pack

Version:
415 lines (414 loc) 15.5 kB
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); } }