web-ui-pack
Version:
Web package with UI elements
330 lines (329 loc) • 11.7 kB
JavaScript
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;
}
}