UNPKG

web-ui-pack

Version:
330 lines (329 loc) 11.7 kB
var InputTypes; (function (InputTypes) { InputTypes[InputTypes["append"] = 0] = "append"; InputTypes[InputTypes["insert"] = 1] = "insert"; InputTypes[InputTypes["deleteAfter"] = 2] = "deleteAfter"; InputTypes[InputTypes["deleteBefore"] = 3] = "deleteBefore"; InputTypes[InputTypes["replace"] = 4] = "replace"; })(InputTypes || (InputTypes = {})); export default class TextHistory { refInput; static historyToSnapshot(s, pos) { return String.fromCharCode(pos) + s; } static historyFromSnapshot(h) { return { pos: h.charCodeAt(0), v: h.substring(1) }; } static findDiff(prev, next) { const inserted = { v: "", pos: 0 }; const removed = { v: "", pos: 0 }; const len = Math.max(prev.length, next.length); for (let i = 0; i < len; ++i) { if (prev[i] !== next[i]) { for (let iPrev = prev.length - 1, iNext = next.length - 1;; --iPrev, --iNext) { if (prev[iPrev] !== next[iNext]) { if (iNext > -1) { inserted.pos = i; inserted.v = next.substring(i, iNext + 1); } if (iPrev > -1) { removed.pos = i; removed.v = prev.substring(i, iPrev + 1); } break; } } break; } } return { inserted: inserted.v ? inserted : null, removed: removed.v ? removed : null }; } static delimiters = " .,!?-/\\|_"; constructor(refInput) { this.refInput = refInput; } snapshotEncode(snap) { return `${String.fromCharCode(snap.action, snap.pos1, snap.pos2, snap.posIns) + (snap.removed ?? "")}\0${snap.inserted ?? ""}`; } snapshotDecode(snap) { const i = snap.indexOf("\0", 4); return { action: snap.charCodeAt(0), pos1: snap.charCodeAt(1), pos2: snap.charCodeAt(2), posIns: snap.charCodeAt(3), removed: snap.substring(4, i), inserted: snap.substring(i + 1), }; } handleKeyDown(e) { if (e.altKey) { return false; } let isUndo = (e.ctrlKey || e.metaKey) && e.code === "KeyZ"; const isRedo = (isUndo && e.shiftKey) || (e.ctrlKey && e.code === "KeyY" && !e.metaKey); isUndo &&= !isRedo; if (isUndo || isRedo) { e.preventDefault(); if (!this.canUndoRedo(isRedo)) { return false; } setTimeout(() => { this.refInput.dispatchEvent(new InputEvent("beforeinput", { cancelable: true, bubbles: true, inputType: isUndo ? "historyUndo" : "historyRedo", })); }); return true; } return false; } _stateBeforeInput; get inputState() { const pos1 = this.refInput.selectionStart || 0; const pos2 = this.refInput.selectionEnd || 0; const v = this.refInput.value; return { pos1, pos2, action: 4, inserted: null, value: v, }; } handleBeforeInput(e) { this.#inpDebounce?.call(this); this._histTimeout && clearTimeout(this._histTimeout); this.#inpBubble?.call(this); delete this._histTimeout; this._stateBeforeInput = undefined; let isUndoRedo = true; let isUndoRedoSuccess = false; let isRedo = false; switch (e.inputType) { case "historyRedo": isRedo = true; case "historyUndo": isUndoRedoSuccess = this.undoRedo(isRedo); break; default: this._stateBeforeInput = this.inputState; if (e.inputType.startsWith("insert")) { this._stateBeforeInput.action = e.inputType === "insertText" ? 0 : 1; this._stateBeforeInput.inserted = e.data; } else if (e.inputType.startsWith("delete")) { this._stateBeforeInput.action = e.inputType === "deleteContentBackward" ? 3 : 2; } isUndoRedo = false; break; } if (isUndoRedo) { e.preventDefault(); if (isUndoRedoSuccess) { this._stateBeforeInput = false; this.#inpBubble = () => { clearTimeout(tid); this.#inpBubble = undefined; this.refInput.dispatchEvent(new InputEvent("input", { cancelable: false, bubbles: true, inputType: e.inputType })); }; const tid = setTimeout(this.#inpBubble); } } return isUndoRedo; } #inpBubble; #inpDebounce; handleInput(e) { const isBrowserAutofill = e.isTrusted && e.inputType == null; if (isBrowserAutofill) { this._stateBeforeInput = { action: 4, pos1: 0, pos2: 0, inserted: this.refInput.value, value: "", }; } if (this._stateBeforeInput) { const was = this._stateBeforeInput.value; this.save(this._stateBeforeInput); const v = this.refInput.value; this.#inpDebounce = () => { clearTimeout(t); this.#inpDebounce = undefined; const next = this.refInput.value; const isChanged = next !== v; isChanged && this.save(was, next); }; const t = setTimeout(this.#inpDebounce); } else if (this._stateBeforeInput !== false) { console.error("WUP.History error. Event [input] fired without [beforeinput] event. Undo-history is cleared"); this._hist = []; this._histPos = null; } else { this._stateBeforeInput = undefined; } } _hist = []; _histTimeout; save(arg1, arg2) { if (this._histPos != null) { this._hist.splice(this._histPos + 1); this._histPos = null; } let stateBefore; if (typeof arg1 === "string") { if (this._stateBeforeInput) { stateBefore = this._stateBeforeInput; } else { stateBefore = this.inputState; stateBefore.value = arg1; } stateBefore.action = 4; } else { stateBefore = arg1; } this._stateBeforeInput = stateBefore; const { pos1, pos2, action, value: prev } = stateBefore; let snap; let isUpdate = false; if (this._histTimeout) { isUpdate = true; const ls = this._hist[this._hist.length - 1]; snap = this.snapshotDecode(ls); if (snap.action === 0 && snap.inserted.length > 1) { this._hist[this._hist.length - 1] = ls.substring(0, ls.length - 1); isUpdate = false; snap.pos1 = pos1; snap.pos2 = pos2; snap.removed = ""; } snap.action = 4; } else { snap = { action, pos1, pos2, posIns: pos1, removed: prev.substring(pos1, pos2) }; } switch (snap.action) { case 0: if (!snap.removed && this._hist.length) { const sl = this.snapshotDecode(this._hist[this._hist.length - 1]); if (sl.action === 0 && sl.posIns + sl.inserted.length === snap.posIns && (!TextHistory.delimiters.includes(stateBefore.inserted) || TextHistory.delimiters.includes(sl.inserted[sl.inserted.length - 1]))) { isUpdate = true; sl.inserted += stateBefore.inserted; snap = sl; break; } } snap.inserted = stateBefore.inserted; break; case 1: snap.inserted = stateBefore.inserted; break; case 2: if (pos1 === pos2) { snap.removed = prev[pos1]; } break; case 3: if (pos1 === pos2) { snap.removed = prev[pos1 - 1]; snap.posIns = pos1 - 1; } break; default: { const diff = TextHistory.findDiff(prev, arg2 ?? this.refInput.value); if (diff.inserted) { snap.inserted = diff.inserted.v; snap.posIns = diff.inserted.pos; } if (diff.removed) { snap.removed = diff.removed.v; snap.posIns = diff.removed.pos; } } break; } const sn = this.snapshotEncode(snap); if (isUpdate) { this._histTimeout && clearTimeout(this._histTimeout); this._hist[this._hist.length - 1] = sn; } else { this._hist.push(sn); } this._histTimeout = setTimeout(() => { this._histTimeout = null; this._stateBeforeInput = undefined; }, 1); } get lastIndex() { return this._histPos ?? this._hist.length - 1; } removeLast() { const li = this.lastIndex; if (li >= 0) { const snap = this.snapshotDecode(this._hist[li]); if (snap.action === 0 && snap.inserted.length > 1) { snap.inserted = snap.inserted.substring(0, snap.inserted.length - 1); this._hist[li] = this.snapshotEncode(snap); } else { this._hist.splice(li); if (this._histPos != null) { --this._histPos; } } } } canUndoRedo(isRedo) { if (!this._hist.length) { return false; } if (this._histPos == null) { return !isRedo; } if (!isRedo) { return this._histPos >= 0; } return this._histPos < this._hist.length - 1; } _histPos; undoRedo(isRedo) { if (!this.canUndoRedo(isRedo)) { return false; } let v = this.refInput.value; if (this._histPos == null) { this._histPos = this._hist.length - 1; } if (isRedo) { ++this._histPos; } const snap = this.snapshotDecode(this._hist[this._histPos]); let { pos1, pos2, posIns, inserted, removed } = snap; if (isRedo) { v = v.substring(0, posIns) + inserted + v.substring(posIns + removed.length); this.refInput.value = v; const posSel = posIns + inserted.length; this.refInput.setSelectionRange(posSel, posSel); } else { v = v.substring(0, posIns) + removed + v.substring(posIns + inserted.length); this.refInput.value = v; this.refInput.setSelectionRange(pos1, pos2); --this._histPos; } return true; } }