hyper-undo 0.6.3 → 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 +36 -1
- package/README.md +75 -9
- package/dist/hyper-undo.min.js +1 -1
- package/package.json +1 -1
- package/src/extension-noise.js +11 -4
- package/src/index.js +12 -2
- package/src/keys.js +37 -0
- package/src/lib/content-dom.js +58 -28
- package/src/lib/region-capabilities.js +1 -0
- package/src/primitives.js +58 -2
- package/src/replay-plan.js +564 -0
- package/src/scope.js +441 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# hyper-undo changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.4] - 2026-10-15
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Synced content-dom.js and region-capabilities.js with ClayJS, and updated extension-noise.js
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- A select whose selected option is excluded from saving now keeps its selection in the content view (ported from ClayJS)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
3
13
|
## [0.6.3] - 2026-10-10
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -9,8 +19,33 @@
|
|
|
9
19
|
|
|
10
20
|
## [Unreleased]
|
|
11
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
|
+
|
|
12
39
|
### Fixed
|
|
13
|
-
- 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.
|
|
14
49
|
|
|
15
50
|
## [0.6.2] - 2026-10-03
|
|
16
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.
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
package/dist/hyper-undo.min.js
CHANGED
|
@@ -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"]);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
package/src/extension-noise.js
CHANGED
|
@@ -55,12 +55,19 @@ export function stripExtensionNoise(root) {
|
|
|
55
55
|
// querySelectorAll('*') never returns the root itself, so an extension stamp on
|
|
56
56
|
// <html> used to ride into every saved file. The root is the one element an
|
|
57
57
|
// extension can mark on a page with no other match.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// The attribute list is only copied when a marker is found on it.
|
|
59
|
+
const strip = (el) => {
|
|
60
|
+
const attrs = el.attributes
|
|
61
|
+
if (!attrs) return
|
|
62
|
+
let found = null
|
|
63
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
64
|
+
const name = attrs[i].name
|
|
65
|
+
if (EXTENSION_ATTR_PATTERN.test(name.toLowerCase())) (found || (found = [])).push(name)
|
|
62
66
|
}
|
|
67
|
+
if (found) for (const name of found) el.removeAttribute(name)
|
|
63
68
|
}
|
|
69
|
+
strip(root)
|
|
70
|
+
for (const el of root.querySelectorAll('*')) strip(el)
|
|
64
71
|
} catch (e) {
|
|
65
72
|
if (typeof console !== 'undefined') console.warn('[hyperclay] stripExtensionNoise skipped:', e)
|
|
66
73
|
}
|
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()
|
|
149
|
-
redo()
|
|
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/lib/content-dom.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GENERATED from clayjs/src/lib/content-dom.js. Edit ClayJS and run `npm run propagate:foundation`.
|
|
2
2
|
|
|
3
|
-
import { capabilitySelector, hasCapability } from './region-capabilities.js'
|
|
3
|
+
import { capabilitySelector, hasCapability, CAPABILITIES, expandsTo, hasPolicyToken } from './region-capabilities.js'
|
|
4
4
|
|
|
5
5
|
const UNSUPPORTED_SELECTOR = /:(?:focus(?:-within|-visible)?|hover|active|visited|defined)\b/i
|
|
6
6
|
|
|
@@ -9,10 +9,9 @@ function copyControlState(source, copy) {
|
|
|
9
9
|
const type = (source.getAttribute?.('type') || '').toLowerCase()
|
|
10
10
|
if ('value' in source && 'value' in copy && source.tagName !== 'OPTION' && type !== 'checkbox' && type !== 'radio' && type !== 'file') copy.value = source.value
|
|
11
11
|
if (source.tagName === 'INPUT' && (type === 'checkbox' || type === 'radio')) copy.checked = source.checked
|
|
12
|
+
// Each surviving option carries its own selection, so a <select> whose excluded
|
|
13
|
+
// options were removed from the copy keeps the rest in step with the page.
|
|
12
14
|
if (source.tagName === 'OPTION') copy.selected = source.selected
|
|
13
|
-
if (source.tagName === 'SELECT') {
|
|
14
|
-
for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected
|
|
15
|
-
}
|
|
16
15
|
if ('indeterminate' in source && 'indeterminate' in copy) copy.indeterminate = source.indeterminate
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -27,46 +26,77 @@ function matchesWithin(node, selector, boundary, inherit) {
|
|
|
27
26
|
return false
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
// One native deep clone, then a lockstep walk of the two isomorphic trees that
|
|
30
|
+
// records provenance, collects controls, and marks excluded subtrees for removal.
|
|
31
|
+
// The browser's clone is far cheaper than importing node by node. The root answers
|
|
32
|
+
// for its ancestors once; below it the walk is top-down, so with inheritance each
|
|
33
|
+
// element answers only for its own markers: an excluded ancestor is never entered.
|
|
34
|
+
function excluded(source, capability, capabilityMatch, exclude, boundary, inherit) {
|
|
35
|
+
return (inherit
|
|
36
|
+
? hasCapability(source, capability)
|
|
37
|
+
: matchesWithin(source, capabilityMatch, boundary, false)) ||
|
|
38
|
+
!!(exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The test closestWithCapability makes at each level, for this element alone.
|
|
42
|
+
function hasOwnCapability(element, capability) {
|
|
43
|
+
const definition = CAPABILITIES[capability]
|
|
44
|
+
return definition.tokens.some(token => expandsTo(element, token)) ||
|
|
45
|
+
definition.bundles.some(bundle => hasPolicyToken(element, bundle))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CONTROL_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT', 'OPTION'])
|
|
49
|
+
|
|
50
|
+
function pairTree(source, copy, maps, drop) {
|
|
51
|
+
if (source.nodeType === 1 && drop(source)) { maps.dropped.push(copy); return }
|
|
38
52
|
maps.cloneToLive.set(copy, source)
|
|
39
53
|
maps.liveToClone.set(source, copy)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
maps.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
copyChildren.
|
|
50
|
-
|
|
54
|
+
let sourceChildren = source
|
|
55
|
+
let copyChildren = copy
|
|
56
|
+
if (source.nodeType === 1) {
|
|
57
|
+
if (CONTROL_TAGS.has(source.tagName)) maps.controls.push(source, copy)
|
|
58
|
+
if (source.tagName === 'TEMPLATE') {
|
|
59
|
+
// The deep clone also copied the template's own child nodes, which are not its
|
|
60
|
+
// content and were never part of the view.
|
|
61
|
+
while (copy.firstChild) copy.removeChild(copy.firstChild)
|
|
62
|
+
sourceChildren = source.content
|
|
63
|
+
copyChildren = copy.content
|
|
64
|
+
maps.cloneToLive.set(copyChildren, sourceChildren)
|
|
65
|
+
maps.liveToClone.set(sourceChildren, copyChildren)
|
|
51
66
|
}
|
|
67
|
+
} else if (source.nodeType !== 11) return
|
|
68
|
+
let c = copyChildren.firstChild
|
|
69
|
+
for (let s = sourceChildren.firstChild; s && c; s = s.nextSibling) {
|
|
70
|
+
pairTree(s, c, maps, drop)
|
|
71
|
+
c = c.nextSibling
|
|
52
72
|
}
|
|
53
|
-
if (source.nodeType === 1) copyControlState(source, copy)
|
|
54
|
-
return copy
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
export function createContentView(context, { capability = 'data', exclude = null, inherit = true } = {}) {
|
|
75
|
+
export function createContentView(context, { capability = 'data', exclude = null, inherit = true, provenance = null } = {}) {
|
|
58
76
|
if (!context) throw new TypeError('createContentView requires a DOM context')
|
|
59
77
|
const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument
|
|
60
78
|
if (!sourceDocument?.implementation?.createHTMLDocument) {
|
|
61
79
|
throw new TypeError('createContentView requires an HTML DOM implementation')
|
|
62
80
|
}
|
|
63
81
|
const inertDocument = sourceDocument.implementation.createHTMLDocument('')
|
|
64
|
-
const cloneToLive = new WeakMap()
|
|
82
|
+
const cloneToLive = provenance || new WeakMap()
|
|
65
83
|
const liveToClone = new WeakMap()
|
|
66
84
|
const sourceRoot = context.nodeType === 9 ? context.documentElement : context
|
|
67
85
|
if (exclude) inertDocument.documentElement.matches(exclude)
|
|
68
86
|
const capabilityMatch = capabilitySelector(capability)
|
|
69
|
-
const
|
|
87
|
+
const drop = inherit
|
|
88
|
+
? (el) => hasOwnCapability(el, capability) || !!(exclude && el.matches(exclude))
|
|
89
|
+
: (el) => excluded(el, capability, capabilityMatch, exclude, sourceRoot, false)
|
|
90
|
+
let root = null
|
|
91
|
+
if (sourceRoot.nodeType !== 1 || !excluded(sourceRoot, capability, capabilityMatch, exclude, sourceRoot, inherit)) {
|
|
92
|
+
root = inertDocument.importNode(sourceRoot, true)
|
|
93
|
+
const maps = { cloneToLive, liveToClone, dropped: [], controls: [] }
|
|
94
|
+
pairTree(sourceRoot, root, maps, drop)
|
|
95
|
+
for (const node of maps.dropped) node.remove()
|
|
96
|
+
// Control state is copied once the copy holds only what the view keeps, so a
|
|
97
|
+
// <select> sees exactly the options the old node-by-node import gave it.
|
|
98
|
+
for (let i = 0; i < maps.controls.length; i += 2) copyControlState(maps.controls[i], maps.controls[i + 1])
|
|
99
|
+
}
|
|
70
100
|
if (context.nodeType === 9 && root) {
|
|
71
101
|
inertDocument.replaceChild(root, inertDocument.documentElement)
|
|
72
102
|
liveToClone.set(context, inertDocument)
|
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) {
|