hyper-undo 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -19,8 +19,33 @@
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.7.0] - 2026-10-30
23
+
24
+ ### Added
25
+ - Undo and redo can run as explicit commands: a command records only the writes it makes itself, also while a sync apply is paused, keeps an outer pause depth and returns a receipt.
26
+ - `isProtected` option, which refuses an undo or redo that would erase protected content.
27
+ - `ignoreNode` option, which in automatic mode drops a change record when every node it adds and removes is one you ignore.
28
+ - `future` lists the redo stack in history's shape, next first, so a Redo control can name what it replays.
29
+ - Discarding an entry emits a `discard` event.
30
+ - `describe(entryId)`, which returns an entry, its refusal and every step as plain data, so a refused step can be kept as recovery.
31
+
32
+ ### Changed
33
+ - Undo checks a whole entry before changing either stack. Each entry carries a certificate of what it touched, and a step whose target has changed since is refused, with both stacks kept.
34
+ - A refused top entry can be discarded, and a failed operation rolls back the ones before it.
35
+ - Cmd+Z on a checkbox, radio or button now reaches the page stack. Only fields that take typed text keep the browser's undo.
36
+ - A commit's or command's own head writes belong to its entry, and a commit captures the title it changes as one value step.
37
+ - A refused step names the nearest ancestor with a `data-id`, so a text step inside an entry is described as that entry.
38
+
22
39
  ### Fixed
23
- - Cmd+Z inside an OverType editor is left to the textarea, instead of a page-level undo that only restored its `data-value`
40
+ - Cmd+Z inside an OverType editor is left to the textarea, instead of a page-level undo that only restored its `data-value`.
41
+ - The undo guard ignores the attributes the scope ignores on every projected node.
42
+ - A body scope fed by a document-wide record source drops records whose target is outside it, so a head write such as `document.title` text never joins an entry, and an explicit `recordValue` title stays the one title step.
43
+ - The `ignoreAttribute` predicate receives the host a removed subtree belongs under, so a projection that reads ancestors matches while detached and after restore.
44
+ - Redo of a deleted list is no longer refused as content-changed.
45
+ - Redo of a multi-node add keeps the nodes in order.
46
+ - Redo of an add refuses a reused durable id, descendants included, and the id check skips nodes the replay itself removes.
47
+ - Rollback returns a moved node where it was, and scalar steps are judged against the replay's planned attachment.
48
+ - A removed node is described by the element that held it, and ignored children stay out of projections.
24
49
 
25
50
  ## [0.6.2] - 2026-10-03
26
51
 
package/README.md CHANGED
@@ -65,12 +65,17 @@ the box.
65
65
  | `undo.commitCaptured(label)` | drain `observer.takeRecords()` and push as one commit (pause-before / commit-on-success pattern) |
66
66
  | `undo.discardCaptured()` | drain and throw away the captured records (failure path companion) |
67
67
  | `undo.flush()` | force-close the current idle batch as its own commit |
68
- | `undo.undo()` / `undo.redo()` | navigate history |
68
+ | `undo.command(label, fn)` | run a synchronous `fn` and push only its own writes as one labelled entry; returns `{ id, label }`, or `null` when it wrote nothing |
69
+ | `undo.setRecording(mode)` | `'automatic'` (default) or `'commands'`; see [Recording modes](#recording-modes) |
70
+ | `undo.undo()` / `undo.redo()` | navigate history; each returns a receipt, `{ ok: true, entryId, label, direction }` or `{ ok: false, code, entryId, label, direction }`. `undo({ entryId })` and `redo({ entryId })` replay only while that entry is the next one, and refuse `stale-entry` otherwise |
71
+ | `undo.discard(entryId)` | drop the refused top entry, and only that one: `{ ok: true, entryId, direction }`, or `{ ok: false, code: 'not-refused' }` |
69
72
  | `undo.clear()` | clear both stacks |
70
73
  | `undo.pause()` / `undo.resume()` | recorder skips while paused |
71
- | `undo.on('undo'\|'redo'\|'commit'\|'clear', fn)` / `undo.off(...)` | subscribe to lifecycle events; `undo`/`redo` fire after a navigate, `commit` after a new commit, `clear` after a reset. (No generic `change` event.) |
74
+ | `undo.on('undo'\|'redo'\|'commit'\|'refused'\|'discard'\|'clear', fn)` / `undo.off(...)` | subscribe to lifecycle events; `undo`/`redo` fire after a navigate with the receipt, `refused` with `{ entryId, label, direction, code, targets }`, `commit` after a new commit, `discard` with `{ entryId, label, direction }`, `clear` after a reset. (No generic `change` event.) |
72
75
  | `undo.canUndo` / `undo.canRedo` | booleans (getters) |
73
- | `undo.history` | `[{ label, timestamp }, ...]`, oldest first; `timestamp` is `Date.now()` millis |
76
+ | `undo.history` | `[{ id, label, timestamp }, ...]`, oldest first; `timestamp` is `Date.now()` millis. The entry that was just refused also carries `refused: { direction, code, targets }` |
77
+ | `undo.future` | the redo stack in the same shape, next redo first |
78
+ | `undo.describe(entryId)` | JSON-safe description of what an entry changes, built from its certificate (`null` for an unknown id or an unreplayable entry); see [Refusals](#refusals) |
74
79
  | `undo.isPaused` | boolean (getter) |
75
80
  | `undo.defaults` | `{ shadowKeydownIn: [...] }` |
76
81
  | `undo.create(opts)` | a separate scope for advanced multi-scope use |
@@ -85,7 +90,9 @@ the box.
85
90
  | `idleLabel` | `'Edit'` | label for auto-closed batches |
86
91
  | `bindKeys` | `true` (singleton), `false` (`create`) | install the global Cmd+Z handler |
87
92
  | `shadowKeydownIn` | code-editor selectors (see below) | when `event.target.closest(selector)` matches, the global handler bails without `preventDefault` |
88
- | `ignoreAttribute` | `null` | predicate `(attrName, element) => boolean`; return true to skip recording that attribute mutation |
93
+ | `ignoreAttribute` | `null` | predicate `(attrName, element) => boolean`; return true to skip recording that attribute mutation, and the certificate's projection skips it too, so such a change never refuses a later undo or redo. |
94
+ | `isProtected` | `null` | predicate `(node) => boolean`; return true for content that no undo or redo may erase, and an operation that would remove or replace a subtree containing such a node is refused with code `"protected"`. |
95
+ | `ignoreNode` | `null` | predicate `(node) => boolean`; in automatic mode a `childList` record whose every added and removed node it accepts is not recorded, so a gesture's own repeated moves leave no generic entry beside the one the consumer records with `command`, and such a node is left out of a bracketed entry's added and removed lists too. |
89
96
  | `debug` | `false` | console.log internal state transitions |
90
97
 
91
98
  ## Batching
@@ -99,6 +106,57 @@ Raw mutation records are too fine-grained (typing "hello" is five
99
106
  - **Idle auto-batch** — mutations made outside an explicit commit collect until
100
107
  the scope is idle for `idleWindowMs`, then close into one `Edit` commit.
101
108
 
109
+ ## Recording modes
110
+
111
+ `automatic`, the default, records every observed mutation and closes an idle batch into one `Edit` entry. `commands` records nothing on its own: only the writes a `command(label, fn)` (or `commit`) brackets become entries, so a merge, a derived repaint, a local command's own bookkeeping, or a callback that writes after its `await` never appears in the history. The document still changes for all of them, and a later undo sees those changes.
112
+
113
+ ```js
114
+ undo.setRecording('commands')
115
+ const entry = undo.command('Add card', () => list.append(card)) // { id, label } | null
116
+ undo.setRecording('automatic')
117
+ ```
118
+
119
+ `setRecording` accepts only `'automatic'` and `'commands'` and throws for anything else; it also throws while a command is running. The switch is a commit boundary, never a merge: work already recorded keeps its own entry, and nothing recorded after the switch joins it. A `command` is synchronous, never runs inside another command, and refuses a callback declared `async` or one that returns a promise.
120
+
121
+ ## Refusals
122
+
123
+ Undo and redo preflight the whole entry against the live DOM without writing, and apply it only when every step still matches, so a refusal changes neither the document nor either stack:
124
+
125
+ ```js
126
+ undo.undo() // { ok: true, entryId, label, direction }
127
+ // { ok: false, code, entryId, label, direction }
128
+ ```
129
+
130
+ | Code | What changed since the entry was recorded |
131
+ |---|---|
132
+ | `content-changed` | an attribute, a text run, or the saved content of a subtree the step would erase is not what the step recorded |
133
+ | `structure-changed` | the children of a list the step edits are not in the order it recorded, or a node it would place is still attached elsewhere |
134
+ | `target-detached` | the element or list the step addresses is no longer in the scope |
135
+ | `duplicate-id` | restoring a removed subtree would bring back a `data-id` a live node already carries |
136
+ | `protected` | the step would erase a subtree containing a node `isProtected` accepts |
137
+ | `unsupported` | the step holds a shape replay cannot address or verify |
138
+ | `stale-entry` | `undo({ entryId })` named an entry that is not on top of its stack |
139
+ | `replay-error` | a native write failed mid-apply; the entry is rolled back and `replay-error` is the code |
140
+ | `empty` | nothing to replay on that stack |
141
+
142
+ A refused entry keeps its place on top, and `undo.history` (or `undo.future`) shows `refused: { direction, code, targets }` on it, so nothing is lost and nothing is silently skipped. `undo.discard(entryId)` drops exactly that entry and leaves the document and every other entry as they are.
143
+
144
+ `targets` is one `{ id, kind }` per node the refused step addresses. `id` is the nearest `[data-id]` ancestor, so a text step with no id of its own names the record it was typed in, and `null` when nothing in the ancestor chain carries one; `kind` says what the target was to the step (`added`, `removed`, `move`, `protected`, or the attribute or property name of a value step). The receipt's own `targets` are on the `refused` event and the history entry, so a consumer can name the record a refusal is about without asking the engine again.
145
+
146
+ ## Describing an entry
147
+
148
+ `undo.describe(entryId)` is the plain, serializable description of one entry, built from its certificate: nothing in it is a live node, so a caller can store it. It answers `null` for an unknown id or an entry whose certificate could not be compiled, and otherwise:
149
+
150
+ ```js
151
+ {
152
+ id, label, timestamp,
153
+ refused: { direction, code, targets } | null, // only for the entry just refused
154
+ steps: [ /* one per step, in recorded order */ ],
155
+ }
156
+ ```
157
+
158
+ A `move` step is `{ kind: 'move', node: { id }, from: { id }, to: { id } }`; an added or removed subtree is `{ kind: 'add' | 'remove', parent: { id }, nodes: [{ id, html }] }`, where `html` is the recorded markup; a value, text or attribute step is `{ kind: 'value' | 'text' | 'attribute', target: { id }, prop?, name?, before, after }`. Every nested `id` follows the same nearest-`[data-id]` rule as a refusal target.
159
+
102
160
  ## Keyboard shortcuts
103
161
 
104
162
  `bindKeys: true` installs a `window` keydown capture-phase listener:
@@ -126,11 +184,19 @@ Extend it for your own editor:
126
184
  undo.start({ shadowKeydownIn: [...undo.defaults.shadowKeydownIn, '.my-editor'] })
127
185
  ```
128
186
 
129
- Outside the shadow list, the global Cmd+Z intercepts even inside plain
130
- `<input>`/`<textarea>`, so native char-level input-undo no longer fires there.
131
- That's intentional: page-state undo is what users expect for Cmd+Z on a
132
- self-editing page. Pass `bindKeys: false` to opt out entirely and bind your own
133
- handler.
187
+ ### Plain form controls own their undo
188
+
189
+ The handler also returns without `preventDefault` when the key event's composed
190
+ path holds an `input`, a `textarea`, a `select`, or a `contenteditable` subtree
191
+ (declared or resolved, so jsdom works), when the event is an IME composition or
192
+ carries `keyCode` 229, and when another handler already called
193
+ `preventDefault`. Both Cmd+Z and Cmd+Shift+Z follow that rule.
194
+
195
+ So a plain field keeps the browser's own character-level undo, and the page
196
+ stack never undoes inside it. Typing that is not written into an attribute
197
+ records nothing: the page's Cmd+Z history starts at the write a commit or a
198
+ `command` makes of that text. Pass `bindKeys: false` to opt out entirely and
199
+ bind your own handler.
134
200
 
135
201
  ## Filter attributes
136
202
 
@@ -1 +1 @@
1
- var hyperundo=(()=>{var T=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var le=Object.prototype.hasOwnProperty;var de=(e,t)=>{for(var r in t)T(e,r,{get:t[r],enumerable:!0})},ce=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ue(t))!le.call(e,o)&&o!==r&&T(e,o,{get:()=>t[o],enumerable:!(n=se(t,o))||n.enumerable});return e};var fe=e=>ce(T({},"__esModule",{value:!0}),e);var xe={};de(xe,{default:()=>ke,undo:()=>I});function pe(e,t=null){let r=[];if(e.type==="attributes"){let n=e.target,o=e.attributeName,a=e.oldValue,u=n.getAttribute(o);return a==null&&u!=null?r.push({kind:"attr-add",target:n,name:o,newValue:u}):a!=null&&u==null?r.push({kind:"attr-remove",target:n,name:o,oldValue:a}):a!==u&&r.push({kind:"attr-set",target:n,name:o,oldValue:a,newValue:u}),r}if(e.type==="characterData")return r.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),r;if(e.type==="childList"){let n=e.target,o=e.nextSibling,a=e.nextSibling;for(;a&&t?.(a);)a=a.nextSibling;let u=e.previousSibling;for(;u&&t?.(u);)u=u.previousSibling;let l=t?Array.from(e.addedNodes).filter(p=>!t(p)):Array.from(e.addedNodes),d=t?Array.from(e.removedNodes).filter(p=>!t(p)):Array.from(e.removedNodes);return l.length>0&&r.push({kind:"add",parent:n,nodes:l,before:o,afterAuthored:a,beforeAuthored:u}),d.length>0&&r.push({kind:"remove",parent:n,nodes:d,before:o,afterAuthored:a,beforeAuthored:u}),r}return r}function D(e,t=null){let r=[],n=new Map;for(let o of e){if(o.type==="attributes"){let a=o.target,u=o.attributeName,l=n.get(a);l||(l=new Map,n.set(a,l)),l.has(u)||(l.set(u,{idx:r.length,oldValue:o.oldValue}),r.push(null));continue}for(let a of pe(o,t))r.push(a)}for(let[o,a]of n)for(let[u,l]of a){let d=l.oldValue,p=o.getAttribute(u),m=null;d===p?m=null:d==null?m={kind:"attr-add",target:o,name:u,newValue:p}:p==null?m={kind:"attr-remove",target:o,name:u,oldValue:d}:m={kind:"attr-set",target:o,name:u,oldValue:d,newValue:p},r[l.idx]=m}return r.filter(o=>o!=null)}function j(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.write?e.write(e.target,e.newValue):e.target[e.prop]=e.newValue;return;case"add":M(e);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function U(e){switch(e.kind){case"attr-set":if(e.target.getAttribute(e.name)!==e.newValue)return;e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":if(e.target.getAttribute(e.name)!==e.newValue)return;e.target.removeAttribute(e.name);return;case"attr-remove":if(e.target.hasAttribute(e.name))return;e.target.setAttribute(e.name,e.oldValue);return;case"text":if(e.target.data!==e.newValue)return;e.target.data=e.oldValue;return;case"value":if((e.read?e.read(e.target):e.target[e.prop])!==e.newValue)return;e.write?e.write(e.target,e.oldValue):e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":M(e);return}}function M(e){if(e.afterAuthored?.parentNode===e.parent){for(let t of e.nodes)e.parent.insertBefore(t,e.afterAuthored);return}if(e.beforeAuthored?.parentNode===e.parent){let t=e.beforeAuthored;for(let r of e.nodes)t.after(r),t=r;return}for(let t of e.nodes)e.before?.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t)}var me=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],he=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],X=me.join(","),B=new RegExp("^(?:"+he.join("|")+")(?:-|$)");var Se=Object.freeze({data:Object.freeze({tokens:["no-data"],bundles:["editor-ui"]}),save:Object.freeze({tokens:["no-save"],bundles:["editor-ui"]}),snapshot:Object.freeze({tokens:["no-snapshot"],bundles:["editor-ui"]}),watch:Object.freeze({tokens:["no-watch"],bundles:["editor-ui"]}),undo:Object.freeze({tokens:["no-undo"],bundles:["editor-ui"]}),history:Object.freeze({tokens:[],bundles:["editor-ui"]})}),ye=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),ge=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function E(e,t){if(!e||e.nodeType!==1)return!1;let r=e.getAttribute?.("clay");return!!(r&&r.split(/\s+/).includes(t)||e.hasAttribute?.(t))}function N(e,t){if(E(e,t))return!0;for(let[r,n]of Object.entries(ye))if(n.includes(t)&&E(e,r))return!0;return!1}function q(e){return e==="clay"||ge.includes(e)}var we=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function be(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function S(e){let t=be();if(t)try{if(!t(e).undoable)return!0}catch{}let r=e&&e.nodeType!==1?e.parentElement:e;if(r&&r.closest&&r.closest(X))return!0;for(;r&&r.nodeType===1;){if(N(r,"no-undo")||N(r,"no-watch"))return!0;for(let n of we)if(E(r,n))return!0;r=r.parentElement}return!1}function Ee(e){let t=e.target,r=t.cloneNode(!1),n=r;for(;t.parentElement;){t=t.parentElement;let o=t.cloneNode(!1);o.appendChild(r),r=o}return e.oldValue==null?n.removeAttribute(e.attributeName):n.setAttribute(e.attributeName,e.oldValue),S(n)}function R(e,t,r){if(r?.type==="attributes"&&q(r.attributeName)){if(S(e)&&Ee(r))return!0}else if(S(e))return!0;if(r&&r.type==="attributes"){if(r.attributeName&&B.test(r.attributeName.toLowerCase()))return!0;if(t)try{if(t(r.attributeName,r.target))return!0}catch{}}return!1}function F(){let e=new Map;function t(o,a){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(a),()=>r(o,a)}function r(o,a){let u=e.get(o);u&&u.delete(a)}function n(o,a){let u=e.get(o);if(u)for(let l of Array.from(u))try{l(a)}catch(d){console.error("[hyper-undo] listener threw",d)}}return{on:t,off:r,emit:n}}var ve={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function O(e){let t={...ve,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let r=F(),n=[],o=[],a=[],u=null,l=null,d=0,p=!1;function m(...s){t.debug&&console.log("[hyper-undo]",...s)}function Y(){p||(p=!0,l=Z(g),l.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),m("started"))}function Z(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return c&&f&&typeof f.createObserver=="function"?(m("sourcing records from the platform Mutation hub (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function v(){p&&(p=!1,l&&(l.disconnect(),l=null),y(),n.length=0,o.length=0,a=[],d=0,m("stopped"))}function P(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(d>0)return;let c=_(s);if(c.length>0){for(let f of c)a.push(f);K()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return D(c,f=>R(f,t.ignoreAttribute))}function K(){y(),u=setTimeout(L,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function L(){if(u=null,a.length===0)return;let s=a;a=[],k(t.idleLabel,s)}function k(s,c){for(n.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;n.length>t.maxHistory;)n.shift();m("commit",s,"primitives:",c.length),r.emit("commit")}function J(s,c){if(!p)throw new Error("hyper-undo: scope not started");g(l.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(l.takeRecords()),a.length===0){r.emit("commit");return}y();let b=a;a=[],k(s,b)}function Q(s){if(!p)throw new Error("hyper-undo: scope not started");if(!l)return;if(d>1){l.takeRecords();return}w();let c=_(l.takeRecords());c.length!==0&&(y(),k(s,c))}function ee(){l&&l.takeRecords()}function z(s){!p||d>0||!s||(a.push(s),K())}function te(s,{prop:c="value",oldValue:f,newValue:b,read:ae,write:ie}={}){!s||f===b||z({kind:"value",target:s,prop:c,oldValue:f,newValue:b,read:ae,write:ie})}function w(){d===0&&l&&g(l.takeRecords()),a.length!==0&&(y(),L())}function x(){d===0&&l&&g(l.takeRecords()),d++,m("paused",d)}function A(){d!==0&&(d--,d===0&&l&&l.takeRecords(),m("resumed",d))}function re(){if(P()){v();return}w();let s=n.pop();if(s){x();try{for(let c=s.primitives.length-1;c>=0;c--)try{U(s.primitives[c])}catch(f){m("undo primitive failed (continuing)",f)}}finally{A()}o.push(s),m("undo",s.label),r.emit("undo")}}function ne(){if(P()){v();return}w();let s=o.pop();if(s){x();try{for(let c of s.primitives)try{j(c)}catch(f){m("redo primitive failed (continuing)",f)}}finally{A()}n.push(s),m("redo",s.label),r.emit("redo")}}function oe(){l&&l.takeRecords(),n.length=0,o.length=0,a=[],y(),m("cleared"),r.emit("clear")}return{_config:t,start:Y,stop:v,commit:J,commitCaptured:Q,discardCaptured:ee,flush:w,record:z,recordValue:te,undo:re,redo:ne,clear:oe,pause:x,resume:A,on:r.on,off:r.off,get canUndo(){return n.length>0},get canRedo(){return o.length>0},get isPaused(){return d>0},get history(){return n.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function W(e,t){let r=n=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let a=n.target,u=e._config.shadowKeydownIn;if(a&&a.closest&&Array.isArray(u))for(let p of u)try{if(a.closest(p))return}catch{}if(!(n.metaKey||n.ctrlKey))return;let d=(n.key||"").toLowerCase();if(d==="z"&&!n.shiftKey){n.preventDefault(),n.stopPropagation(),e.undo();return}if(d==="z"&&n.shiftKey){n.preventDefault(),n.stopPropagation(),e.redo();return}if(d==="y"&&!n.shiftKey){n.preventDefault(),n.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",r,!0),()=>window.removeEventListener("keydown",r,!0)}var C=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]"],i=null,V=null,$=null,h=null;function H(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let r=W(e,t);return h={inst:e,cleanup:r},r}function G(e){h&&h.inst===e&&(h.cleanup(),h=null)}var I={defaults:{shadowKeydownIn:C},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(i){if(V===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),i;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let r={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||C};return i=O(r),i.start(),V=t,e.bindKeys!==!1&&($=H(i,()=>I.stop())),i},stop(){i&&(G(i),$=null,i.stop(),i=null,V=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let r={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||C},n=O(r),o=null,a={_config:n._config,commit:n.commit,commitCaptured:n.commitCaptured,discardCaptured:n.discardCaptured,flush:n.flush,record:n.record,recordValue:n.recordValue,undo:n.undo,redo:n.redo,clear:n.clear,pause:n.pause,resume:n.resume,on:n.on,off:n.off,get canUndo(){return n.canUndo},get canRedo(){return n.canRedo},get isPaused(){return n.isPaused},get history(){return n.history},get scope(){return n.scope},start(){e.bindKeys&&(o=H(n,()=>a.stop())),n.start()},stop(){G(n),o=null,n.stop()}};return a},commit(e,t){return i?i.commit(e,t):void 0},commitCaptured(e){return i?i.commitCaptured(e):void 0},discardCaptured(){return i?i.discardCaptured():void 0},record(e){return i?i.record(e):void 0},recordValue(e,t){return i?i.recordValue(e,t):void 0},undo(){return i?i.undo():void 0},redo(){return i?i.redo():void 0},clear(){return i?i.clear():void 0},pause(){return i?i.pause():void 0},resume(){return i?i.resume():void 0},flush(){return i?i.flush():void 0},on(e,t){return i?i.on(e,t):void 0},off(e,t){return i?i.off(e,t):void 0},get canUndo(){return i?i.canUndo:!1},get canRedo(){return i?i.canRedo:!1},get history(){return i?i.history:[]},get isPaused(){return i?i.isPaused:!1}};var ke=I;return fe(xe);})();
1
+ var hyperundo=(()=>{var J=Object.defineProperty;var ct=Object.getOwnPropertyDescriptor;var ft=Object.getOwnPropertyNames;var mt=Object.prototype.hasOwnProperty;var pt=(e,n)=>{for(var t in n)J(e,t,{get:n[t],enumerable:!0})},gt=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of ft(n))!mt.call(e,i)&&i!==t&&J(e,i,{get:()=>n[i],enumerable:!(r=ct(n,i))||r.enumerable});return e};var ht=e=>gt(J({},"__esModule",{value:!0}),e);var Yt={};pt(Yt,{default:()=>Gt,undo:()=>ne});function yt(e,n=null){let t=[];if(e.type==="attributes"){let r=e.target,i=e.attributeName,a=e.oldValue,l=r.getAttribute(i);return a==null&&l!=null?t.push({kind:"attr-add",target:r,name:i,newValue:l}):a!=null&&l==null?t.push({kind:"attr-remove",target:r,name:i,oldValue:a}):a!==l&&t.push({kind:"attr-set",target:r,name:i,oldValue:a,newValue:l}),t}if(e.type==="characterData")return t.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),t;if(e.type==="childList"){let r=e.target,i=e.nextSibling,a=e.nextSibling;for(;a&&n?.(a);)a=a.nextSibling;let l=e.previousSibling;for(;l&&n?.(l);)l=l.previousSibling;let s=n?Array.from(e.addedNodes).filter(g=>!n(g)):Array.from(e.addedNodes),d=n?Array.from(e.removedNodes).filter(g=>!n(g)):Array.from(e.removedNodes);return s.length>0&&t.push({kind:"add",parent:r,nodes:s,before:i,afterAuthored:a,beforeAuthored:l,record:e}),d.length>0&&t.push({kind:"remove",parent:r,nodes:d,before:i,afterAuthored:a,beforeAuthored:l,record:e}),t}return t}function ve(e,n=null){let t=[],r=new Map;for(let i of e){if(i.type==="attributes"){let a=i.target,l=i.attributeName,s=r.get(a);s||(s=new Map,r.set(a,s)),s.has(l)||(s.set(l,{idx:t.length,oldValue:i.oldValue}),t.push(null));continue}for(let a of yt(i,n))t.push(a)}for(let[i,a]of r)for(let[l,s]of a){let d=s.oldValue,g=i.getAttribute(l),p=null;d===g?p=null:d==null?p={kind:"attr-add",target:i,name:l,newValue:g}:g==null?p={kind:"attr-remove",target:i,name:l,oldValue:d}:p={kind:"attr-set",target:i,name:l,oldValue:d,newValue:g},t[s.idx]=p}return t.filter(i=>i!=null)}function Ae(e){let n=[],t=new Map;for(let i of e){let a=we(i);if(a==null){n.push(i);continue}let l=t.get(i.target);l||(l=new Map,t.set(i.target,l));let s=l.get(a);if(s==null){let d=ke(i);if(d.oldValue===d.newValue)continue;l.set(a,n.length),n.push({...i,...d})}else n[s]={...n[s],newValue:ke(i).newValue}}let r=[];for(let i of n){if(we(i)==null){r.push(i);continue}i.oldValue!==i.newValue&&r.push(wt(i))}return r}var bt=new Set(["attr-set","attr-add","attr-remove","text","value"]);function we(e){return bt.has(e.kind)?e.kind==="text"?"#text":e.kind==="value"?`#${e.prop||"value"}`:e.name==null?null:`@${e.name}`:null}function ke(e){return e.kind==="attr-add"?{oldValue:null,newValue:e.newValue}:e.kind==="attr-remove"?{oldValue:e.oldValue,newValue:null}:{oldValue:e.oldValue,newValue:e.newValue}}function wt(e){return e.kind!=="attr-add"&&e.kind!=="attr-remove"?e:e.oldValue==null&&e.newValue!=null?{...e,kind:"attr-add"}:e.oldValue!=null&&e.newValue==null?{...e,kind:"attr-remove"}:{...e,kind:"attr-set"}}var Ne=new WeakMap,kt=0;function T(e){let n=Ne.get(e);return n||(n=`n${++kt}`,Ne.set(e,n)),n}function v(e){return!!e&&typeof e=="object"&&typeof e.nodeType=="number"}function vt(e){return e.kind==="add"||e.kind==="remove"}function Se(e,n){let t=[],r=e.childNodes;if(!r)return t;for(let i of r)n&&n(i)||t.push(T(i));return t}function z(e,n,t=null,r=null){if(!v(e))return null;if(e.nodeType===3)return`#text:${JSON.stringify(e.data)}`;if(e.nodeType===8)return`#comment:${JSON.stringify(e.data)}`;if(e.nodeType!==1)return`#node:${e.nodeType}`;let i=[];if(e.attributes){for(let s of e.attributes)At(s.name,e,n,t)||i.push(`${s.name}=${JSON.stringify(s.value)}`);i.sort()}let a=[];if(e.childNodes)for(let s of e.childNodes)Nt(s,r)||a.push(z(s,n,t,r));return`<${String(e.tagName||"").toLowerCase()}${i.length?` ${i.join(" ")}`:""}>${a.join("")}</>`}function G(e,n=[]){if(!v(e))return n;if(e.nodeType===1){let t=e.getAttribute&&e.getAttribute("data-id");t&&n.push(t)}if(e.childNodes)for(let t of e.childNodes)G(t,n);return n}function At(e,n,t,r=null){if(typeof t!="function")return!1;try{return!!t(e,n,r)}catch{return!1}}function Nt(e,n){if(typeof n!="function")return!1;try{return!!n(e)}catch{return!1}}function Te(e,n){if(typeof n!="function")return[];let t=[],r=i=>{if(!v(i))return;let a=!1;try{a=!!n(i)}catch{a=!1}if(a&&t.push(E(i,"protected")),i.childNodes)for(let l of i.childNodes)r(l)};for(let i of e)r(i);return t}function E(e,n,t=null){let r=e;r&&r.nodeType!==1&&(r=r.parentElement);let i=r&&typeof r.closest=="function"?r.closest("[data-id]"):null;return!i&&t&&typeof t.closest=="function"&&(i=t.closest("[data-id]")),{id:(i?i.getAttribute("data-id"):null)||null,kind:n||null}}function Et(e,n,t=null){return(e||[]).filter(v).map(r=>E(r,n,t))}function q(e){return{replayable:!1,reason:e,steps:[]}}function D(e,n){let t=[[n.afterAuthored,i=>i],[n.beforeAuthored,i=>i+1],[n.before,i=>i]],r=!1;for(let[i,a]of t){if(!v(i))continue;r=!0;let l=e.indexOf(T(i));if(l>=0)return a(l)}return r?null:e.length}function Ve(e,n={}){let t=n.isIgnored||null,r=n.ignoreAttribute||null,i=Ae(e),a=[];for(let p of i){if(!vt(p)){if(!v(p.target))return q("non-dom-target");continue}if(!v(p.parent))return q("non-dom-parent");let h=Array.from(p.nodes||[]).filter(v);if(h.length!==0){if(h.length!==Array.from(p.nodes||[]).length)return q("non-dom-node");a.push({primitive:p,nodes:h,parent:p.parent})}}if(St(a))return q("in-record-move");let l=Tt(a,t);if(!l)return q("ambiguous-structure");let s=Vt(a,l),d=new Map(a.map(p=>[p.primitive,p])),g=[];for(let p of i){let h=d.get(p);if(!h){g.push(Pt(p));continue}let w=s.get(h);if(w){w.at===h&&g.push(w.step);continue}g.push(It(h,l.get(h),r,t))}return{replayable:!0,reason:null,steps:g}}function St(e){let n=new Map;for(let t of e){let r=t.primitive.record;if(!r)continue;let i=n.get(r);i||(i=new Map,n.set(r,i));for(let a of t.nodes){let l=i.get(a);l||(l=new Set,i.set(a,l)),l.add(t.primitive.kind)}}for(let t of n.values())for(let r of t.values())if(r.size>1)return!0;return!1}function Tt(e,n){let t=new Map;for(let i of e)t.has(i.parent)||t.set(i.parent,Se(i.parent,n));let r=new Map;for(let i=e.length-1;i>=0;i--){let a=e[i],l=t.get(a.parent);if(!l)return null;let s=l.slice(),d;if(a.primitive.kind==="add"){let g=a.nodes.map(T);if(!g.every(p=>l.includes(p)))return null;d=l.filter(p=>!g.includes(p))}else{let g=D(s,a.primitive);if(g==null)return null;d=s.slice(),d.splice(g,0,...a.nodes.map(T))}r.set(a,{before:d,after:s}),t.set(a.parent,d)}return r}function Vt(e,n){let t=new Map,r=new Map;e.forEach((a,l)=>{for(let s of a.nodes){let d=T(s);a.primitive.kind==="remove"?t.set(d,{item:a,index:l,node:s}):r.set(d,{item:a,index:l,node:s})}});let i=new Map;for(let[a,l]of t){let s=r.get(a);if(!s||s.index<l.index||l.item.nodes.length!==1||s.item.nodes.length!==1)continue;let d={type:"move",node:l.node,sourceParent:l.item.parent,destParent:s.item.parent,source:n.get(l.item),dest:n.get(s.item),sourceSlot:l.item.primitive,destSlot:s.item.primitive,targets:[E(l.node,"move")]};i.set(l.item,{step:d,at:s.item}),i.set(s.item,{step:d,at:s.item})}return i}function It(e,n,t,r){let i=e.primitive.kind;return{type:i==="add"?"add":"restore",parent:e.parent,nodes:e.nodes,seqBefore:n.before,seqAfter:n.after,slot:e.primitive,content:e.nodes.map(a=>z(a,t,e.parent,r)),ids:i==="remove"?e.nodes.flatMap(a=>G(a)):[],targets:Et(e.nodes,i==="add"?"added":"removed",e.parent)}}function Pt(e){return{type:e.kind,primitive:e,targets:[E(e.target,e.name||e.prop||"text")]}}function Ie(e,n,t={}){if(!e||!e.replayable)return{ok:!1,code:"unsupported",targets:[]};let r={isIgnored:t.isIgnored||null,ignoreAttribute:t.ignoreAttribute||null,isProtected:t.isProtected||null,inScope:t.inScope||(()=>!0),liveNodesWithIds:t.liveNodesWithIds||(()=>[]),sim:new Map,planned:new Map,operations:[]};for(let i=0;i<e.steps.length;i++){let a=e.steps[n==="undo"?e.steps.length-1-i:i],l=Rt(a,n,r);if(l)return{ok:!1,code:l.code,targets:l.targets}}return{ok:!0,operations:r.operations}}function M(e,n){return e.sim.has(n)||e.sim.set(n,Se(n,e.isIgnored)),e.sim.get(n)}function Pe(e,n){for(let t=e;t;t=t.parentNode)if(n.planned.has(t))return n.planned.get(t);return n.inScope(e)}function Ce(e,n){return n.liveNodesWithIds(e).filter(t=>Pe(t,n)).length>0}function b(e,n){return{code:e,targets:n||[]}}function Ct(e){return e.kind==="text"?e.target.data:e.kind==="value"?typeof e.read=="function"?e.read(e.target):e.target[e.prop||"value"]:e.target.getAttribute(e.name)}function Rt(e,n,t){let r=n==="undo";return e.type==="move"?Lt(e,r,t):e.type==="add"?Ot(e,r,t):e.type==="restore"?_t(e,r,t):xt(e,r,t)}function xt(e,n,t){let r=e.primitive,i=n?r.newValue:r.oldValue,a=n?r.oldValue:r.newValue;return v(r.target)?Pe(r.target,t)?r.kind==="value"&&typeof r.write=="function"&&typeof r.read!="function"?b("unsupported",e.targets):Ct(r)!==i?b("content-changed",e.targets):(r.kind==="value"?t.operations.push({type:"value",target:r.target,prop:r.prop||"value",value:a,read:r.read||null,write:r.write||null}):r.kind==="text"?t.operations.push({type:"text",target:r.target,data:a}):t.operations.push({type:"attr",target:r.target,name:r.name,value:a??null}),null):b("target-detached",e.targets):b("unsupported",e.targets)}function Ot(e,n,t){if(!v(e.parent)||!t.inScope(e.parent))return b("target-detached",e.targets);if(n){if(!L(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);let s=Te(e.nodes,t.isProtected);if(s.length>0)return b("protected",s);for(let d=0;d<e.nodes.length;d++)if(z(e.nodes[d],t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[d])return b("content-changed",e.targets);for(let d of e.nodes)t.operations.push({type:"remove",node:d}),t.planned.set(d,!1);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!L(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);for(let s of e.nodes)if(s.parentNode)return b("structure-changed",e.targets);let r=e.nodes.flatMap(s=>G(s));if(r.length>0&&Ce(r,t))return b("duplicate-id",e.targets);let i=D(e.seqBefore,e.slot);if(i==null)return b("unsupported",e.targets);let a=e.seqBefore.slice(),l=i;for(let s of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:s,beforeId:a[l]??null}),a=a.slice(0,l).concat(T(s),a.slice(l)),l+=1,t.planned.set(s,!0);return t.sim.set(e.parent,e.seqAfter.slice()),null}function _t(e,n,t){if(!v(e.parent)||!t.inScope(e.parent))return b("target-detached",e.targets);if(n){if(!L(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);for(let s=0;s<e.nodes.length;s++){let d=e.nodes[s];if(d.parentNode||z(d,t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[s])return b("content-changed",e.targets)}if(e.ids.length>0&&Ce(e.ids,t))return b("duplicate-id",e.targets);let i=D(e.seqAfter,e.slot);if(i==null)return b("unsupported",e.targets);let a=e.seqAfter.slice(),l=i;for(let s of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:s,beforeId:a[l]??null}),a=a.slice(0,l).concat(T(s),a.slice(l)),l+=1,t.planned.set(s,!0);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!L(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);let r=Te(e.nodes,t.isProtected);if(r.length>0)return b("protected",r);for(let i=0;i<e.nodes.length;i++){let a=e.nodes[i];if(a.parentNode!==e.parent||z(a,t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[i])return b("content-changed",e.targets)}for(let i of e.nodes)t.operations.push({type:"remove",node:i}),t.planned.set(i,!1);return t.sim.set(e.parent,e.seqAfter.slice()),null}function Mt(e,n){let t=e.indexOf(n);return t<0?null:e.slice(0,t).concat(e.slice(t+1))}function Lt(e,n,t){let r=T(e.node),i=n?e.destParent:e.sourceParent,a=n?e.sourceParent:e.destParent,l=M(t,i),s=M(t,a),d=n?e.dest.after:e.source.before,g=n?e.source.after:e.dest.before,p=n?e.sourceSlot:e.destSlot;if(!v(i)||!v(a)||!t.inScope(i)||!t.inScope(a))return b("target-detached",e.targets);if(!L(l,d))return b("structure-changed",e.targets);let h=i===a?Mt(s,r):s;if(!L(h,g))return b("structure-changed",e.targets);let w=D(g,p);return w==null?b("unsupported",e.targets):(t.operations.push({type:"insert",parent:a,node:e.node,beforeId:g[w]??null}),t.planned.set(e.node,!0),n?(t.sim.set(a,e.source.before.slice()),i!==a&&t.sim.set(i,e.dest.before.slice())):(t.sim.set(a,e.dest.after.slice()),i!==a&&t.sim.set(i,e.source.after.slice())),null)}function L(e,n){if(!e||!n||e.length!==n.length)return!1;for(let t=0;t<e.length;t++)if(e[t]!==n[t])return!1;return!0}function qt(e,n){for(let t of e.childNodes||[])if(T(t)===n)return t;return null}function Ee(e,n){typeof e.write=="function"?e.write(e.target,n):e.target[e.prop]=n}function Re(e){let n=[];try{for(let t of e)zt(t,n);return{ok:!0}}catch(t){for(let r=n.length-1;r>=0;r--)try{n[r]()}catch{}return{ok:!1,error:t}}}function zt(e,n){if(e.type==="attr"){let t=e.target.getAttribute(e.name);e.value==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,e.value),n.push(()=>{t==null?e.target.removeAttribute(e.name):e.target.setAttribute(e.name,t)});return}if(e.type==="text"){let t=e.target.data;e.target.data=e.data,n.push(()=>{e.target.data=t});return}if(e.type==="value"){let t=typeof e.read=="function"?e.read(e.target):e.target[e.prop];Ee(e,e.value),n.push(()=>Ee(e,t));return}if(e.type==="insert"){let t=e.beforeId==null?null:qt(e.parent,e.beforeId);if(e.beforeId!=null&&!t)throw new Error("hyper-undo: replay anchor vanished");let r=e.node.parentNode,i=e.node.nextSibling;e.parent.insertBefore(e.node,t),n.push(()=>{r?r.insertBefore(e.node,i&&i.parentNode===r?i:null):e.node.parentNode===e.parent&&e.parent.removeChild(e.node)});return}if(e.type==="remove"){let t=e.node.parentNode;if(!t)throw new Error("hyper-undo: replay target is no longer attached");let r=e.node.nextSibling;t.removeChild(e.node),n.push(()=>t.insertBefore(e.node,r))}}var Kt=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],$t=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],xe=Kt.join(","),Oe=new RegExp("^(?:"+$t.join("|")+")(?:-|$)");var rn=Object.freeze({data:Object.freeze({tokens:["no-data"],bundles:["editor-ui"]}),save:Object.freeze({tokens:["no-save"],bundles:["editor-ui"]}),snapshot:Object.freeze({tokens:["no-snapshot"],bundles:["editor-ui"]}),watch:Object.freeze({tokens:["no-watch"],bundles:["editor-ui"]}),undo:Object.freeze({tokens:["no-undo"],bundles:["editor-ui"]}),history:Object.freeze({tokens:[],bundles:["editor-ui"]})}),Bt=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),Dt=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function U(e,n){if(!e||e.nodeType!==1)return!1;let t=e.getAttribute?.("clay");return!!(t&&t.split(/\s+/).includes(n)||e.hasAttribute?.(n))}function Y(e,n){if(U(e,n))return!0;for(let[t,r]of Object.entries(Bt))if(r.includes(n)&&U(e,t))return!0;return!1}function _e(e){return e==="clay"||Dt.includes(e)}var Ut=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function jt(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function Z(e){let n=jt();if(n)try{if(!n(e).undoable)return!0}catch{}let t=e&&e.nodeType!==1?e.parentElement:e;if(t&&t.closest&&t.closest(xe))return!0;for(;t&&t.nodeType===1;){if(Y(t,"no-undo")||Y(t,"no-watch"))return!0;for(let r of Ut)if(U(t,r))return!0;t=t.parentElement}return!1}function Wt(e){let n=e.target,t=n.cloneNode(!1),r=t;for(;n.parentElement;){n=n.parentElement;let i=n.cloneNode(!1);i.appendChild(t),t=i}return e.oldValue==null?r.removeAttribute(e.attributeName):r.setAttribute(e.attributeName,e.oldValue),Z(r)}function j(e,n,t){if(t?.type==="attributes"&&_e(t.attributeName)){if(Z(e)&&Wt(t))return!0}else if(Z(e))return!0;if(t&&t.type==="attributes"){if(t.attributeName&&Oe.test(t.attributeName.toLowerCase()))return!0;if(n)try{if(n(t.attributeName,t.target))return!0}catch{}}return!1}function Me(){let e=new Map;function n(i,a){let l=e.get(i);return l||(l=new Set,e.set(i,l)),l.add(a),()=>t(i,a)}function t(i,a){let l=e.get(i);l&&l.delete(a)}function r(i,a){let l=e.get(i);if(l)for(let s of Array.from(l))try{s(a)}catch(d){console.error("[hyper-undo] listener threw",d)}}return{on:n,off:t,emit:r}}var Ft={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",recording:"automatic",ignoreAttribute:null,isProtected:null,ignoreNode:null,debug:!1};function Q(e){let n={...Ft,...e};if(!n.scope)throw new Error("hyper-undo: scope is required");let t=Me(),r=[],i=[],a=`u${Math.random().toString(36).slice(2,8)}`,l=0,s=null,d=[],g=null,p=null,h=0,w=!1,P=n.recording==="commands"?"commands":"automatic",A=null,C=!1;function k(...o){n.debug&&console.log("[hyper-undo]",...o)}function $e(){w||(w=!0,p=Be(R),p.observe(n.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),k("started"))}function Be(o){let u=typeof document<"u"&&n.scope===document.body,c=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return u&&c&&typeof c.createObserver=="function"?(k("sourcing records from the platform Mutation hub (shared observer)"),c.createObserver(o)):new MutationObserver(o)}function re(){w&&(w=!1,p&&(p.disconnect(),p=null),x(),r.length=0,i.length=0,d=[],s=null,A=null,C=!1,h=0,k("stopped"))}function De(){let o=n.scope;return!!(o&&o.nodeType===1&&o.isConnected===!1)}function R(o){oe(o,!1)}function oe(o,u){if(A){for(let m of o)A.records.push(m);return}if(P==="commands"||h>0)return;let c=W(Ue(o),u);if(c.length>0){for(let m of c)d.push(m);ie()}}function Ue(o){return typeof n.ignoreNode!="function"?o:o.filter(u=>{if(u.type!=="childList")return!0;let c=[...u.addedNodes,...u.removedNodes];return c.length===0?!0:!c.every(m=>je(m))})}function je(o){try{return!!n.ignoreNode(o)}catch{return!1}}function W(o,u=!1){let c=o.filter(m=>(ut(m.target)||u&&ye(m.target))&&!j(m.target,n.ignoreAttribute,m));return ve(c,m=>j(m,n.ignoreAttribute))}function ie(){x(),g=setTimeout(ae,n.idleWindowMs)}function x(){g!=null&&(clearTimeout(g),g=null)}function ae(){if(g=null,d.length===0)return;let o=d;d=[],K(n.idleLabel,o)}function K(o,u){let c=Ve(u,{isIgnored:F,ignoreAttribute:ue});if(c.steps.length===0)return k("commit",o,"with nothing to record"),t.emit("commit"),null;let m={id:`${a}-${++l}`,label:o,timestamp:Date.now(),primitives:u,certificate:c};for(r.push(m),i.length=0,s=null;r.length>n.maxHistory;)r.shift();return k("commit",o,"primitives:",u.length),t.emit("commit"),m}function F(o){return j(o,n.ignoreAttribute)}function ue(o,u,c=null){if(typeof n.ignoreAttribute!="function")return!1;try{return!!n.ignoreAttribute(o,u,c)}catch{return!1}}function We(o){if(typeof n.isProtected!="function")return!1;try{return!!n.isProtected(o)}catch{return!1}}function Fe(o,u){if(!w)throw new Error("hyper-undo: scope not started");if(P==="commands")return de(o,u,{honorPause:!0,receipt:!1});R(p.takeRecords()),O();let c=h===0?se():null,m=u();if(m&&typeof m.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(oe(p.takeRecords(),!0),c!=null&&le(c),d.length===0){t.emit("commit");return}x();let y=d;d=[],K(o,y)}function Xe(o){if(!w)throw new Error("hyper-undo: scope not started");if(!p)return;if(h>1){p.takeRecords();return}O();let u=W(p.takeRecords(),!0);u.length!==0&&(x(),K(o,u))}function He(){p&&p.takeRecords()}function X(){let o=n.scope;return o&&o.ownerDocument||(typeof document<"u"?document:null)}function $(){let o=X();return!o||!o.head||typeof o.head.querySelector!="function"?null:o.head.querySelector("title")}function se(){let o=$();return o?o.textContent:null}function le(o){let u=$();if(!u||o==null)return;let c=u.textContent;c!==o&&fe({kind:"value",target:u,prop:"textContent",oldValue:o,newValue:c})}function Je(o){if(!o)return null;let u=X();if(!u||o.target!==u||o.prop!=="title")return o;let c=$();return c?{kind:"value",target:c,prop:"textContent",oldValue:o.oldValue,newValue:o.newValue}:null}function Ge(){if(typeof window>"u")return!1;let o=window.clay?.effect||window.hyperclay?.effect;return!!(o&&o.running)}function Ye(){p&&R(p.takeRecords()),O(),A={records:[],primitives:[]}}function Ze(){let o=A;if(A=null,!o)return[];let u=W(o.records,!0);for(let c of o.primitives)u.push(c);return u}function de(o,u,{honorPause:c,receipt:m}){if(typeof u!="function")throw new Error("hyper-undo: command() needs a synchronous function");if(A)throw new Error("hyper-undo: command() cannot nest inside another command");if(C)throw new Error("hyper-undo: command() cannot run during undo or redo");if(Ge())throw new Error("hyper-undo: command() cannot run inside a derived-effect context");if(u.constructor&&u.constructor.name==="AsyncFunction")throw new Error("hyper-undo: command() fn must be synchronous; it was declared async");let y=c&&h>0,N=y?null:se();Ye();let S=[],V=null,_;try{try{_=u()}catch(be){V=be}if(p&&R(p.takeRecords()),_&&typeof _.then=="function"){try{_.then(void 0,()=>{})}catch{}V||(V=new Error("hyper-undo: command() fn must be synchronous; returned a Promise"))}y||le(N)}finally{S=Ze()}let B=y||S.length===0?null:K(o,S);if(V)throw V;return m?B?{id:B.id,label:B.label}:null:B}function Qe(o,u){if(!w)throw new Error("hyper-undo: scope not started");return de(o,u,{honorPause:!1,receipt:!0})}function et(o){if(o!=="automatic"&&o!=="commands")throw new Error("hyper-undo: setRecording() takes 'automatic' or 'commands'");if(A)throw new Error("hyper-undo: setRecording() cannot change while a command is running");return P===o||(O(),P=o,k("recording",o)),P}function ce(o){if(!w||!o||!A&&(h>0||P==="commands"))return;let u=Je(o);u&&fe(u)}function fe(o){if(A){A.primitives.push(o);return}d.push(o),ie()}function tt(o,{prop:u="value",oldValue:c,newValue:m,read:y,write:N}={}){!o||c===m||ce({kind:"value",target:o,prop:u,oldValue:c,newValue:m,read:y,write:N})}function O(){h===0&&p&&R(p.takeRecords()),d.length!==0&&(x(),ae())}function me(){h===0&&p&&R(p.takeRecords()),h++,k("paused",h)}function pe(){h!==0&&(h--,h===0&&p&&p.takeRecords(),k("resumed",h))}function ge(o,u){if(De())return re(),{ok:!1,code:"disconnected"};O();let c=o==="undo"?r:i,m=o==="undo"?i:r,y=c.at(-1);if(!y)return{ok:!1,code:"empty"};if(u&&u!==y.id){let _=he(u)||y;return H(_,o,"stale-entry")}let N;C=!0;try{N=Ie(y.certificate,o,{isIgnored:F,ignoreAttribute:ue,isProtected:We,inScope:st,liveNodesWithIds:lt})}finally{C=!1}if(!N.ok)return H(y,o,N.code,N.targets);me();let S;C=!0;try{S=Re(N.operations)}finally{C=!1,pe()}if(!S.ok)return k(`${o} failed and was rolled back`,S.error),H(y,o,"replay-error");c.pop(),m.push(y),s=null;let V={ok:!0,entryId:y.id,label:y.label,direction:o};return k(o,y.label),t.emit(o,V),V}function nt(o){return ge("undo",o&&o.entryId)}function rt(o){return ge("redo",o&&o.entryId)}function he(o){return r.find(u=>u.id===o)||i.find(u=>u.id===o)||null}function H(o,u,c,m=[]){return s={entryId:o.id,direction:u,code:c,targets:m},k("refused",u,c),t.emit("refused",{entryId:o.id,label:o.label,direction:u,code:c,targets:m}),{ok:!1,code:c,entryId:o.id,label:o.label,direction:u}}function ot(o){if(!s||s.entryId!==o)return{ok:!1,code:"not-refused",entryId:o};let u=s.direction,c=u==="undo"?r:i,m=c.at(-1);return!m||m.id!==o?{ok:!1,code:"stale-entry",entryId:o}:(c.pop(),s=null,k("discarded",m.label),t.emit("discard",{entryId:o,label:m.label,direction:u}),{ok:!0,entryId:o,direction:u})}function it(o){let u=he(o);if(!u||!u.certificate||!u.certificate.replayable)return null;let c=s&&s.entryId===u.id?{direction:s.direction,code:s.code,targets:(s.targets||[]).map(m=>({id:m.id??null,kind:m.kind??null}))}:null;return{id:u.id,label:u.label,timestamp:u.timestamp,refused:c,steps:u.certificate.steps.map(at)}}function at(o){if(o.type==="move")return{kind:"move",node:{id:E(o.node).id},from:{id:E(o.sourceParent).id},to:{id:E(o.destParent).id}};if(o.type==="add"||o.type==="restore")return{kind:o.type==="add"?"add":"remove",parent:{id:E(o.parent).id},nodes:o.nodes.map((N,S)=>({id:E(N).id,html:o.content[S]??null}))};let u=o.primitive,c={id:E(u.target).id},m=u.oldValue??null,y=u.newValue??null;return o.type==="value"?{kind:"value",target:c,prop:u.prop??"value",before:m,after:y}:o.type==="text"?{kind:"text",target:c,before:m,after:y}:o.type==="attr-set"||o.type==="attr-add"||o.type==="attr-remove"?{kind:"attribute",target:c,name:u.name??null,before:m,after:y}:{kind:o.type,target:c,before:m,after:y}}function ut(o){let u=n.scope;return!u||!o?!1:o===u||typeof u.contains=="function"&&u.contains(o)}function ye(o){let u=X(),c=u&&u.head;if(!c||!o)return!1;let m=o.nodeType===1?o:o.parentElement;for(;m&&m!==c;m=m.parentElement)if(m.localName==="title")return!1;return m===c}function st(o){let u=n.scope;return!o||!u?!1:o===u||$()===o||ye(o)?!0:typeof u.contains=="function"?u.contains(o):!1}function lt(o){let u=n.scope;if(!o||o.length===0||typeof u?.querySelectorAll!="function")return[];let c=new Set(o),m=[];for(let y of u.querySelectorAll("[data-id]"))c.has(y.getAttribute("data-id"))&&(F(y)||m.push(y));return m}function dt(){p&&p.takeRecords(),r.length=0,i.length=0,d=[],s=null,x(),k("cleared"),t.emit("clear")}return{_config:n,start:$e,stop:re,commit:Fe,commitCaptured:Xe,discardCaptured:He,flush:O,command:Qe,setRecording:et,record:ce,recordValue:tt,undo:nt,redo:rt,clear:dt,discard:ot,describe:it,pause:me,resume:pe,on:t.on,off:t.off,get canUndo(){return r.length>0},get canRedo(){return i.length>0},get isPaused(){return h>0},get history(){return r.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return s&&s.entryId===o.id&&(u.refused={direction:s.direction,code:s.code,targets:s.targets}),u})},get future(){return i.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return s&&s.entryId===o.id&&(u.refused={direction:s.direction,code:s.code,targets:s.targets}),u}).reverse()},get scope(){return n.scope}}}var Xt=new Set(["checkbox","radio","button","submit","reset","image","file","range","color"]);function Ht(e){let n=typeof e.composedPath=="function"?e.composedPath():[],t=n.length>0?n:Jt(e.target);for(let r of t){if(!r||r.nodeType!==1)continue;let i=String(r.tagName||"").toUpperCase();if(i==="TEXTAREA"||i==="SELECT"||i==="INPUT"&&!Xt.has(String(r.type||r.getAttribute?.("type")||"text").toLowerCase())||r.isContentEditable===!0)return!0;let a=r.getAttribute&&r.getAttribute("contenteditable");if(a!=null&&String(a).toLowerCase()!=="false")return!0}return!1}function Jt(e){let n=[];for(let t=e;t;t=t.parentNode||(t.host??null))n.push(t);return n}function Le(e,n){let t=r=>{let i=e.scope;if(i&&i.nodeType===1&&i.isConnected===!1){n&&n();return}if(r.defaultPrevented||r.isComposing||r.keyCode===229||Ht(r))return;let a=r.target,l=e._config.shadowKeydownIn;if(a&&a.closest&&Array.isArray(l))for(let g of l)try{if(a.closest(g))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let d=(r.key||"").toLowerCase();if(d==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(d==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(d==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",t,!0),()=>window.removeEventListener("keydown",t,!0)}var ee=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]"],f=null,te=null,qe=null,I=null;function ze(e,n){if(I){if(I.inst===e)return I.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let t=Le(e,n);return I={inst:e,cleanup:t},t}function Ke(e){I&&I.inst===e&&(I.cleanup(),I=null)}var ne={defaults:{shadowKeydownIn:ee},start(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(f){if(te===n)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),f;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||ee};return f=Q(t),f.start(),te=n,e.bindKeys!==!1&&(qe=ze(f,()=>ne.stop())),f},stop(){f&&(Ke(f),qe=null,f.stop(),f=null,te=null)},create(e={}){let n=e.scope||(typeof document<"u"?document.body:null);if(!n)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let t={...e,scope:n,shadowKeydownIn:e.shadowKeydownIn||ee},r=Q(t),i=null,a={_config:r._config,commit:r.commit,command:r.command,setRecording:r.setRecording,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,discard:r.discard,describe:r.describe,pause:r.pause,resume:r.resume,on:r.on,off:r.off,get canUndo(){return r.canUndo},get canRedo(){return r.canRedo},get isPaused(){return r.isPaused},get history(){return r.history},get future(){return r.future},get scope(){return r.scope},start(){e.bindKeys&&(i=ze(r,()=>a.stop())),r.start()},stop(){Ke(r),i=null,r.stop()}};return a},commit(e,n){return f?f.commit(e,n):void 0},command(e,n){return f?f.command(e,n):void 0},setRecording(e){return f?f.setRecording(e):void 0},commitCaptured(e){return f?f.commitCaptured(e):void 0},discardCaptured(){return f?f.discardCaptured():void 0},record(e){return f?f.record(e):void 0},recordValue(e,n){return f?f.recordValue(e,n):void 0},undo(e){return f?f.undo(e):void 0},redo(e){return f?f.redo(e):void 0},clear(){return f?f.clear():void 0},discard(e){return f?f.discard(e):void 0},describe(e){return f?f.describe(e):void 0},pause(){return f?f.pause():void 0},resume(){return f?f.resume():void 0},flush(){return f?f.flush():void 0},on(e,n){return f?f.on(e,n):void 0},off(e,n){return f?f.off(e,n):void 0},get canUndo(){return f?f.canUndo:!1},get canRedo(){return f?f.canRedo:!1},get history(){return f?f.history:[]},get future(){return f?f.future:[]},get isPaused(){return f?f.isPaused:!1}};var Gt=ne;return ht(Yt);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-undo",
3
- "version": "0.6.4",
3
+ "version": "0.7.0",
4
4
  "description": "DOM-state undo/redo via MutationObserver inverse-op replay. Standalone or paired with hyperclayjs.",
5
5
  "keywords": [
6
6
  "undo",
package/src/index.js CHANGED
@@ -106,6 +106,8 @@ const undo = {
106
106
  const wrapper = {
107
107
  _config: inst._config,
108
108
  commit: inst.commit,
109
+ command: inst.command,
110
+ setRecording: inst.setRecording,
109
111
  commitCaptured: inst.commitCaptured,
110
112
  discardCaptured: inst.discardCaptured,
111
113
  flush: inst.flush,
@@ -114,6 +116,8 @@ const undo = {
114
116
  undo: inst.undo,
115
117
  redo: inst.redo,
116
118
  clear: inst.clear,
119
+ discard: inst.discard,
120
+ describe: inst.describe,
117
121
  pause: inst.pause,
118
122
  resume: inst.resume,
119
123
  on: inst.on,
@@ -122,6 +126,7 @@ const undo = {
122
126
  get canRedo() { return inst.canRedo },
123
127
  get isPaused() { return inst.isPaused },
124
128
  get history() { return inst.history },
129
+ get future() { return inst.future },
125
130
  get scope() { return inst.scope },
126
131
  start() {
127
132
  // Claim the global key binding first so a conflict throws before the
@@ -141,13 +146,17 @@ const undo = {
141
146
 
142
147
  // Singleton delegators
143
148
  commit(label, fn) { return singleton ? singleton.commit(label, fn) : undefined },
149
+ command(label, fn) { return singleton ? singleton.command(label, fn) : undefined },
150
+ setRecording(mode) { return singleton ? singleton.setRecording(mode) : undefined },
144
151
  commitCaptured(label) { return singleton ? singleton.commitCaptured(label) : undefined },
145
152
  discardCaptured() { return singleton ? singleton.discardCaptured() : undefined },
146
153
  record(p) { return singleton ? singleton.record(p) : undefined },
147
154
  recordValue(t, o) { return singleton ? singleton.recordValue(t, o) : undefined },
148
- undo() { return singleton ? singleton.undo() : undefined },
149
- redo() { return singleton ? singleton.redo() : undefined },
155
+ undo(opts) { return singleton ? singleton.undo(opts) : undefined },
156
+ redo(opts) { return singleton ? singleton.redo(opts) : undefined },
150
157
  clear() { return singleton ? singleton.clear() : undefined },
158
+ discard(entryId) { return singleton ? singleton.discard(entryId) : undefined },
159
+ describe(entryId) { return singleton ? singleton.describe(entryId) : undefined },
151
160
  pause() { return singleton ? singleton.pause() : undefined },
152
161
  resume() { return singleton ? singleton.resume() : undefined },
153
162
  flush() { return singleton ? singleton.flush() : undefined },
@@ -156,6 +165,7 @@ const undo = {
156
165
  get canUndo() { return singleton ? singleton.canUndo : false },
157
166
  get canRedo() { return singleton ? singleton.canRedo : false },
158
167
  get history() { return singleton ? singleton.history : [] },
168
+ get future() { return singleton ? singleton.future : [] },
159
169
  get isPaused() { return singleton ? singleton.isPaused : false },
160
170
  }
161
171
 
package/src/keys.js CHANGED
@@ -1,3 +1,32 @@
1
+ // Is the key being pressed inside a native editable control (or an editable
2
+ // subtree)? The browser's own undo belongs there, not the page stack's.
3
+ const NO_TEXT_INPUTS = new Set(['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'range', 'color'])
4
+
5
+ function ownsNativeUndo(event) {
6
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : []
7
+ const nodes = path.length > 0 ? path : chainFromTarget(event.target)
8
+ for (const node of nodes) {
9
+ if (!node || node.nodeType !== 1) continue
10
+ const tag = String(node.tagName || '').toUpperCase()
11
+ if (tag === 'TEXTAREA' || tag === 'SELECT') return true
12
+ // An input that takes no typed text has no native undo of its own: a checkbox,
13
+ // a radio or a button leaves the key to the page stack.
14
+ if (tag === 'INPUT' && !NO_TEXT_INPUTS.has(String(node.type || node.getAttribute?.('type') || 'text').toLowerCase())) return true
15
+ if (node.isContentEditable === true) return true
16
+ // jsdom (and older engines) do not resolve isContentEditable, so read the
17
+ // declaration itself; an unset or explicit "false" is not editable.
18
+ const declared = node.getAttribute && node.getAttribute('contenteditable')
19
+ if (declared != null && String(declared).toLowerCase() !== 'false') return true
20
+ }
21
+ return false
22
+ }
23
+
24
+ function chainFromTarget(target) {
25
+ const out = []
26
+ for (let node = target; node; node = node.parentNode || (node.host ?? null)) out.push(node)
27
+ return out
28
+ }
29
+
1
30
  // Installs a single window-level keydown listener in capture phase.
2
31
  // Returns a cleanup function.
3
32
 
@@ -11,6 +40,14 @@ export function installKeys(scope, onDetached) {
11
40
  return
12
41
  }
13
42
 
43
+ // A key the page already handled belongs to whoever handled it.
44
+ if (event.defaultPrevented) return
45
+ // The IME owns keys while it composes.
46
+ if (event.isComposing || event.keyCode === 229) return
47
+ // A plain form control keeps the browser's own text undo. Only the unsent
48
+ // control changes; Save/Enter later commits that text as a page command.
49
+ if (ownsNativeUndo(event)) return
50
+
14
51
  // Bypass for in-page editors (CodeMirror, Monaco, etc.)
15
52
  const target = event.target
16
53
  const shadow = scope._config.shadowKeydownIn
package/src/primitives.js CHANGED
@@ -53,10 +53,10 @@ export function recordToPrimitives(record, ignoreNode = null) {
53
53
  ? Array.from(record.removedNodes).filter((n) => !ignoreNode(n))
54
54
  : Array.from(record.removedNodes)
55
55
  if (added.length > 0) {
56
- out.push({ kind: 'add', parent, nodes: added, before, afterAuthored, beforeAuthored })
56
+ out.push({ kind: 'add', parent, nodes: added, before, afterAuthored, beforeAuthored, record })
57
57
  }
58
58
  if (removed.length > 0) {
59
- out.push({ kind: 'remove', parent, nodes: removed, before, afterAuthored, beforeAuthored })
59
+ out.push({ kind: 'remove', parent, nodes: removed, before, afterAuthored, beforeAuthored, record })
60
60
  }
61
61
  return out
62
62
  }
@@ -119,6 +119,62 @@ export function recordsToPrimitives(records, ignoreNode = null) {
119
119
  return out.filter((p) => p != null)
120
120
  }
121
121
 
122
+ // Entry-wide scalar normalization. An entry spans several observer deliveries
123
+ // (a command's records, then its idle neighbours), so the same field can appear
124
+ // more than once. Keep ONE primitive per (target, field) carrying the first old
125
+ // value and the final new value, and drop the ones whose endpoints are equal:
126
+ // undo must walk back to the value before the entry, not to an intermediate one.
127
+ export function normalizePrimitives(primitives) {
128
+ const out = []
129
+ const slots = new Map() // target → Map(field → index in out)
130
+
131
+ for (const p of primitives) {
132
+ const field = scalarField(p)
133
+ if (field == null) { out.push(p); continue }
134
+ let byField = slots.get(p.target)
135
+ if (!byField) { byField = new Map(); slots.set(p.target, byField) }
136
+ const at = byField.get(field)
137
+ if (at == null) {
138
+ const endpoints = scalarEndpoints(p)
139
+ if (endpoints.oldValue === endpoints.newValue) continue
140
+ byField.set(field, out.length)
141
+ out.push({ ...p, ...endpoints })
142
+ } else {
143
+ out[at] = { ...out[at], newValue: scalarEndpoints(p).newValue }
144
+ }
145
+ }
146
+
147
+ const normalized = []
148
+ for (const p of out) {
149
+ if (scalarField(p) == null) { normalized.push(p); continue }
150
+ if (p.oldValue === p.newValue) continue
151
+ normalized.push(reclassifyScalar(p))
152
+ }
153
+ return normalized
154
+ }
155
+
156
+ const SCALAR_KINDS = new Set(['attr-set', 'attr-add', 'attr-remove', 'text', 'value'])
157
+
158
+ function scalarField(p) {
159
+ if (!SCALAR_KINDS.has(p.kind)) return null
160
+ if (p.kind === 'text') return '#text'
161
+ if (p.kind === 'value') return `#${p.prop || 'value'}`
162
+ return p.name == null ? null : `@${p.name}`
163
+ }
164
+
165
+ function scalarEndpoints(p) {
166
+ if (p.kind === 'attr-add') return { oldValue: null, newValue: p.newValue }
167
+ if (p.kind === 'attr-remove') return { oldValue: p.oldValue, newValue: null }
168
+ return { oldValue: p.oldValue, newValue: p.newValue }
169
+ }
170
+
171
+ function reclassifyScalar(p) {
172
+ if (p.kind !== 'attr-add' && p.kind !== 'attr-remove') return p
173
+ if (p.oldValue == null && p.newValue != null) return { ...p, kind: 'attr-add' }
174
+ if (p.oldValue != null && p.newValue == null) return { ...p, kind: 'attr-remove' }
175
+ return { ...p, kind: 'attr-set' }
176
+ }
177
+
122
178
  // Forward replay. Used by redo().
123
179
  export function replayForward(p) {
124
180
  switch (p.kind) {