// 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);