115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
// undo.js — Phase 5 of editable-cell mode.
|
|
//
|
|
// Linear command stack, depth 50, session-local. Every successful
|
|
// per-cell edit and every bulk operation (paste, fill, delete) push
|
|
// a Command onto the stack. Ctrl/Cmd+Z pops the most recent and
|
|
// replays the inverse — sets each affected cell's draft buffer
|
|
// back to its `oldValue` (or clears the draft when oldValue was
|
|
// the row's stored value), then triggers a re-paint and the
|
|
// row-blur save flow picks the change up like any other edit.
|
|
//
|
|
// Why local-only: shared undo across multiple users is conceptually
|
|
// broken under last-writer-wins (undoing my edit might revert
|
|
// someone else's intervening edit). Every production grid keeps
|
|
// undo per-tab; we follow.
|
|
//
|
|
// Why no redo: minimum viable. Adding redo is a parallel forward
|
|
// stack cleared on any new edit. Cheap to add later if users miss
|
|
// it.
|
|
//
|
|
// Command shape:
|
|
// { cells: [ {rowId, field, oldValue, newValue}, ... ] }
|
|
//
|
|
// One-cell edits push a single-cell Command. Bulk operations push
|
|
// one Command with N cells so a single Ctrl+Z reverts the whole
|
|
// group.
|
|
(function (app) {
|
|
'use strict';
|
|
|
|
const STACK_MAX = 50;
|
|
const _stack = [];
|
|
|
|
function push(cmd) {
|
|
if (!cmd || !cmd.cells || cmd.cells.length === 0) return;
|
|
_stack.push(cmd);
|
|
if (_stack.length > STACK_MAX) {
|
|
_stack.shift();
|
|
}
|
|
}
|
|
|
|
function depth() { return _stack.length; }
|
|
|
|
function clear() { _stack.length = 0; }
|
|
|
|
function undo() {
|
|
const cmd = _stack.pop();
|
|
if (!cmd || !cmd.cells || cmd.cells.length === 0) return null;
|
|
|
|
const editor = app.modules.editor;
|
|
if (!editor) return null;
|
|
|
|
for (let i = 0; i < cmd.cells.length; i++) {
|
|
const c = cmd.cells[i];
|
|
// Compare oldValue to the row's stored data — if they
|
|
// match, clear the draft (the user's edit is being
|
|
// reversed back to baseline). Otherwise set draft = old.
|
|
const row = findRow(c.rowId);
|
|
if (!row) continue;
|
|
const stored = app.modules.util.resolveField(row.data, c.field);
|
|
if (sameValue(stored, c.oldValue)) {
|
|
editor.clearDraftField(c.rowId, c.field);
|
|
} else {
|
|
editor.setDraft(c.rowId, c.field, c.oldValue);
|
|
}
|
|
}
|
|
|
|
if (typeof app.repaint === 'function') app.repaint();
|
|
return cmd;
|
|
}
|
|
|
|
function findRow(rowId) {
|
|
const editor = app.modules.editor;
|
|
const all = (app.state && app.state.rows) || [];
|
|
for (let i = 0; i < all.length; i++) {
|
|
if (editor.rowKey(all[i]) === rowId) return all[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function sameValue(a, b) {
|
|
if (a === b) return true;
|
|
if (a == null && b == null) return true;
|
|
if (a == null || b == null) return false;
|
|
if (typeof a === 'object' || typeof b === 'object') {
|
|
try { return JSON.stringify(a) === JSON.stringify(b); }
|
|
catch (_) { return false; }
|
|
}
|
|
return String(a) === String(b);
|
|
}
|
|
|
|
// Hotkey: Ctrl+Z (Cmd+Z on macOS). Bound at the document level
|
|
// so the user can undo from anywhere on the page, not just from
|
|
// within a focused cell.
|
|
function onKey(ev) {
|
|
const isMod = ev.ctrlKey || ev.metaKey;
|
|
if (!isMod) return;
|
|
if (ev.key === 'z' || ev.key === 'Z') {
|
|
// Skip when the active element is a text-input-like; we
|
|
// don't want to override the browser's intra-input undo.
|
|
const ae = document.activeElement;
|
|
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) {
|
|
return;
|
|
}
|
|
ev.preventDefault();
|
|
undo();
|
|
}
|
|
}
|
|
document.addEventListener('keydown', onKey);
|
|
|
|
app.modules.undo = {
|
|
push: push,
|
|
undo: undo,
|
|
depth: depth,
|
|
clear: clear,
|
|
};
|
|
})(window.tablesApp);
|