hyper-undo 0.7.1 → 0.8.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 +18 -0
- package/README.md +68 -2
- package/dist/hyper-undo.min.js +1 -1
- package/package.json +1 -1
- package/src/index.js +12 -1
- package/src/keys.js +55 -17
- package/src/primitives.js +2 -1
- package/src/replay-plan.js +58 -17
- package/src/scope.js +204 -47
- package/src/typing.js +142 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# hyper-undo changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `refused: 'skip'` scope option: one `undo()` or `redo()` press removes each entry that preflight or apply refuses (every code), emits a `skipped` event carrying the entry's plain-data description, and goes on to the next entry until one applies or the stack is empty. The receipt gains a `skipped` list, and the default (`null`) keeps a refused entry on top as before.
|
|
7
|
+
- `skipped` event: fired once per entry a `refused: 'skip'` press removes.
|
|
8
|
+
- `trackContent(root, { read, write, prop })`: a field's content is recorded as one guarded value step, its saved HTML before and after, sampled from the changes inside the root instead of the raw text and node steps those changes would otherwise leave. Returns a release function, and `stop()` forgets every tracked root.
|
|
9
|
+
- `durableForm` scope option: a host gives a node a stable saved form, used instead of its live markup when certifying and replaying steps
|
|
10
|
+
- `editableKeys: 'page'` scope option: in editable content, Cmd+Z, Cmd+Shift+Z, Ctrl+Y and the browser's own `beforeinput` `historyUndo`/`historyRedo` go to the page history instead of the browser's per-field undo. Form controls, IME compositions, the `shadowKeydownIn` editors and regions the scope does not record keep their own undo, and the default leaves every key with the browser.
|
|
11
|
+
- `ownKeysIn` option: selectors whose history keys belong to the page even inside a `shadowKeydownIn` editor.
|
|
12
|
+
- `holdReplay` option: a consumer can hold undo and redo, which then return `{ ok: false, code: 'held' }` without refusing the entry, touching a stack or changing the document.
|
|
13
|
+
- `typingBreak: 'sentence'` scope option: an undo press undoes one sentence of typing. A step closes after the space or line break that follows `.`, `?` or `!`, at a field switch, at a switch between typing and deleting, after a paste, a drop, a cut or a format command, when a selection is replaced, and at a caret jump made with the mouse or the navigation keys. An open IME composition never lets the idle timer close a step, on every scope.
|
|
14
|
+
- `endsSentence(textBefore)`: the sentence rule as a predicate, for a host that groups typing the same way.
|
|
15
|
+
- A successful `undo()` or `redo()` receipt gains `caret`, where the applied entry changed text: `{ node, offset }` for a text node or an added or removed one, and `{ root, textOffset }` for a tracked content root. The key is present only when the applied entry changed text; a refused press carries no `caret`. A press that skips entries and then applies one reports that entry's `caret`. The engine never touches the selection.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Changes inside a subtree removed from a `no-undo` region no longer leave a history entry
|
|
19
|
+
- An entry description shows a node's durable form as text, not as an internal token
|
|
20
|
+
|
|
3
21
|
## [0.6.4] - 2026-10-15
|
|
4
22
|
|
|
5
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -66,12 +66,13 @@ the box.
|
|
|
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
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.trackContent(root, { read, write, prop })` | record `root`'s content as one guarded value step: `read(root)` is sampled when a mutation lands inside the root and `write(root, html)` replays a step, so the inside of a tracked root is never recorded as raw text or node steps. Returns a release function; `stop()` forgets every tracked root. `prop` defaults to `'content'` |
|
|
69
70
|
| `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,
|
|
71
|
+
| `undo.undo()` / `undo.redo()` | navigate history; each returns a receipt, `{ ok: true, entryId, label, direction, skipped, caret }` or `{ ok: false, code, entryId, label, direction }`. `skipped` is the list of skip records from this call and is present only when something was skipped, and a press that only skipped returns `{ ok: false, code: 'empty', direction, skipped }`. `caret` says where the applied entry changed text (see [Where the text changed](#where-the-text-changed)); it is present only when the applied entry changed text. `undo({ entryId })` and `redo({ entryId })` replay only while that entry is the next one, refuse `stale-entry` otherwise, and never skip |
|
|
71
72
|
| `undo.discard(entryId)` | drop the refused top entry, and only that one: `{ ok: true, entryId, direction }`, or `{ ok: false, code: 'not-refused' }` |
|
|
72
73
|
| `undo.clear()` | clear both stacks |
|
|
73
74
|
| `undo.pause()` / `undo.resume()` | recorder skips while paused |
|
|
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.) |
|
|
75
|
+
| `undo.on('undo'\|'redo'\|'commit'\|'refused'\|'skipped'\|'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 }`, `skipped` with `{ entryId, label, direction, code, targets, description }`, `commit` after a new commit, `discard` with `{ entryId, label, direction }`, `clear` after a reset. (No generic `change` event.) |
|
|
75
76
|
| `undo.canUndo` / `undo.canRedo` | booleans (getters) |
|
|
76
77
|
| `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
78
|
| `undo.future` | the redo stack in the same shape, next redo first |
|
|
@@ -90,9 +91,15 @@ the box.
|
|
|
90
91
|
| `idleLabel` | `'Edit'` | label for auto-closed batches |
|
|
91
92
|
| `bindKeys` | `true` (singleton), `false` (`create`) | install the global Cmd+Z handler |
|
|
92
93
|
| `shadowKeydownIn` | code-editor selectors (see below) | when `event.target.closest(selector)` matches, the global handler bails without `preventDefault` |
|
|
94
|
+
| `editableKeys` | `'native'` | how the history keys and the browser's own history input event are routed inside editable content. `'native'` leaves every `contenteditable` (and the `shadowKeydownIn` list) its own undo. `'page'` sends Cmd+Z, Cmd+Shift+Z, Ctrl+Y and a `beforeinput` `historyUndo`/`historyRedo` to the page history when the target sits inside a tracked content root or matches `ownKeysIn`; a form control, the `shadowKeydownIn` list and an editing host the scope does not record keep their own undo. Only then is `beforeinput` listened to at all |
|
|
95
|
+
| `ownKeysIn` | `null` | selectors whose history keys and history input events belong to the page stack under `editableKeys: 'page'`, even when the element also matches `shadowKeydownIn` (a block editor sharing its element with `.ProseMirror`) |
|
|
96
|
+
| `typingBreak` | `null` | `'sentence'` closes the open typing step after the space or line break that follows `.`, `?` or `!` (the delimiter belongs to the finished sentence), at a switch to another field, at a switch between typing and deleting, after a paste, a drop, a cut or a format command, when a selection is replaced, and at a caret jump made with the mouse or a navigation key. So one undo press undoes one sentence of typing. On every scope an open IME composition holds the idle step open |
|
|
97
|
+
| `holdReplay` | `null` | function `() => boolean`; while it answers true, `undo()` and `redo()` return `{ ok: false, code: 'held' }` without flushing, moving either stack, changing the document or emitting `refused` |
|
|
98
|
+
| `refused` | `null` | `'skip'` lets one `undo()` or `redo()` press get past a step it can no longer replay: the refused entry is removed from its stack (it goes to no other stack), announced with a `skipped` event, and the same call goes on to the next entry until one applies or the stack is empty. Every refusal code is skipped, `replay-error` included, but `undo({ entryId })` never skips. The default keeps a refused entry on top. See [Refusals](#refusals) |
|
|
93
99
|
| `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
100
|
| `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
101
|
| `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. |
|
|
102
|
+
| `durableForm` | `null` | function `(node, host) => string \| undefined`; return a string for a node whose live markup is not its saved markup (a mounted rich text editor), and the certificate uses that string as the node's whole form instead of looking inside it, at record time and at every replay check. Return `undefined` to keep the default form. A throw or a non-string also keeps the default. |
|
|
96
103
|
| `debug` | `false` | console.log internal state transitions |
|
|
97
104
|
|
|
98
105
|
## Batching
|
|
@@ -141,6 +148,39 @@ undo.undo() // { ok: true, entryId, label, direction }
|
|
|
141
148
|
|
|
142
149
|
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
150
|
|
|
151
|
+
### Skipping what can no longer be replayed
|
|
152
|
+
|
|
153
|
+
By default a refused entry keeps its place on top, so every later press refuses it again until it is dismissed. With `refused: 'skip'` one press removes it instead and goes on:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
const undo = hyperundo.create({ refused: 'skip' })
|
|
157
|
+
undo.on('skipped', ({ entryId, label, direction, code, targets, description }) => keepCopy(description))
|
|
158
|
+
|
|
159
|
+
undo.undo()
|
|
160
|
+
// { ok: true, entryId: 'A', label: 'Edit A', direction: 'undo',
|
|
161
|
+
// skipped: [{ entryId: 'C', label: 'Edit C', direction: 'undo', code: 'content-changed',
|
|
162
|
+
// targets: [...], description: { id: 'C', label: 'Edit C', ... } }] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The press pops each refused entry, emits one `skipped` event for it and continues to the next entry in the same call, until one applies or the stack is empty (`{ ok: false, code: 'empty', direction, skipped }`). A skipped entry goes to no other stack and emits no `refused` event. Its `description` is the plain-data `describe(entryId)` of the entry, built before it is removed, so a consumer can keep a recovery copy after the fact. `undo({ entryId })` never skips: a named entry is refused as it is today.
|
|
166
|
+
|
|
167
|
+
### Where the text changed
|
|
168
|
+
|
|
169
|
+
A successful receipt carries `caret`, taken from the last operation of the replay that changed text, so a host can put the caret where the removed or restored text was. The key is present only when the applied entry changed text:
|
|
170
|
+
|
|
171
|
+
| The entry changed | `caret` |
|
|
172
|
+
|---|---|
|
|
173
|
+
| a text node | `{ node, offset }`, at the end of the changed run of its data |
|
|
174
|
+
| a tracked content root (`trackContent`) | `{ root, textOffset }`, at the end of the changed run of `root.textContent` |
|
|
175
|
+
| added or removed a text node | `{ node, offset }`: the parent at the node's index, or the node at its length |
|
|
176
|
+
| a value step with a `write` and no `content` flag, an attribute, a node that is not text | no `caret` key |
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
undo.undo() // { ok: true, entryId, label, direction, skipped, caret: { node, offset: 5 } }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The engine never reads or writes the selection; placing the caret is the host's job. A refused or skipped press reports no `caret`, and when one press skips entries and then applies one, the `caret` is the applied entry's.
|
|
183
|
+
|
|
144
184
|
`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
185
|
|
|
146
186
|
## Describing an entry
|
|
@@ -198,6 +238,32 @@ records nothing: the page's Cmd+Z history starts at the write a commit or a
|
|
|
198
238
|
`command` makes of that text. Pass `bindKeys: false` to opt out entirely and
|
|
199
239
|
bind your own handler.
|
|
200
240
|
|
|
241
|
+
### One undo press, one sentence of typing
|
|
242
|
+
|
|
243
|
+
`typingBreak: 'sentence'` groups typing into sentences instead of into half
|
|
244
|
+
second batches. A step closes after the space or line break that follows `.`,
|
|
245
|
+
`?` or `!`, so the space belongs to the finished sentence; the rule is simple on
|
|
246
|
+
purpose, so `e.g. ` ends a step early and `3.5` does not. It also closes at a
|
|
247
|
+
switch to another field, at a switch between typing and deleting, after a paste,
|
|
248
|
+
a drop, a cut or a format command (each is a step of its own), when a selection
|
|
249
|
+
is replaced, and at a caret jump made with the mouse or with the navigation
|
|
250
|
+
keys. An open IME composition never lets the idle timer close a step, with or
|
|
251
|
+
without the option.
|
|
252
|
+
|
|
253
|
+
`endsSentence(textBefore)` is exported for a host that needs the same rule.
|
|
254
|
+
|
|
255
|
+
### Editable content can share the page history
|
|
256
|
+
|
|
257
|
+
`editableKeys: 'page'` gives `contenteditable` fields the page's timeline. A key
|
|
258
|
+
or the browser's own Undo (`beforeinput` `historyUndo`) inside a plain field, a
|
|
259
|
+
tracked rich field, or an element named by `ownKeysIn` calls `undo()` or
|
|
260
|
+
`redo()`; the event is prevented, so the browser's per-field undo never runs and
|
|
261
|
+
the two histories cannot drift. An `input`, a `textarea`, a `select`, an open
|
|
262
|
+
IME composition, the `shadowKeydownIn` list, and an editing host inside a
|
|
263
|
+
region the scope does not record (an authored `no-undo` field) keep their own
|
|
264
|
+
undo. An empty page history still prevents the key: the field's own undo would
|
|
265
|
+
change text the page history does not know about.
|
|
266
|
+
|
|
201
267
|
## Filter attributes
|
|
202
268
|
|
|
203
269
|
A mutation is excluded from recording when its target's ancestor chain contains
|
package/dist/hyper-undo.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var hyperundo=(()=>{var Y=Object.defineProperty;var mt=Object.getOwnPropertyDescriptor;var pt=Object.getOwnPropertyNames;var gt=Object.prototype.hasOwnProperty;var ht=(e,n)=>{for(var t in n)Y(e,t,{get:n[t],enumerable:!0})},yt=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of pt(n))!gt.call(e,i)&&i!==t&&Y(e,i,{get:()=>n[i],enumerable:!(r=mt(n,i))||r.enumerable});return e};var bt=e=>yt(Y({},"__esModule",{value:!0}),e);var Qt={};ht(Qt,{default:()=>Zt,undo:()=>oe});function wt(e,n=null){let t=[];if(e.type==="attributes"){let r=e.target,i=e.attributeName,a=e.oldValue,s=r.getAttribute(i);return a==null&&s!=null?t.push({kind:"attr-add",target:r,name:i,newValue:s}):a!=null&&s==null?t.push({kind:"attr-remove",target:r,name:i,oldValue:a}):a!==s&&t.push({kind:"attr-set",target:r,name:i,oldValue:a,newValue:s}),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 s=e.previousSibling;for(;s&&n?.(s);)s=s.previousSibling;let l=n?Array.from(e.addedNodes).filter(h=>!n(h)):Array.from(e.addedNodes),c=n?Array.from(e.removedNodes).filter(h=>!n(h)):Array.from(e.removedNodes);return l.length>0&&t.push({kind:"add",parent:r,nodes:l,before:i,afterAuthored:a,beforeAuthored:s,record:e}),c.length>0&&t.push({kind:"remove",parent:r,nodes:c,before:i,afterAuthored:a,beforeAuthored:s,record:e}),t}return t}function Ee(e,n=null){let t=[],r=new Map;for(let i of e){if(i.type==="attributes"){let a=i.target,s=i.attributeName,l=r.get(a);l||(l=new Map,r.set(a,l)),l.has(s)||(l.set(s,{idx:t.length,oldValue:i.oldValue}),t.push(null));continue}for(let a of wt(i,n))t.push(a)}for(let[i,a]of r)for(let[s,l]of a){let c=l.oldValue,h=i.getAttribute(s),p=null;c===h?p=null:c==null?p={kind:"attr-add",target:i,name:s,newValue:h}:h==null?p={kind:"attr-remove",target:i,name:s,oldValue:c}:p={kind:"attr-set",target:i,name:s,oldValue:c,newValue:h},t[l.idx]=p}return t.filter(i=>i!=null)}function Se(e){let n=[],t=new Map;for(let i of e){let a=Ae(i);if(a==null){n.push(i);continue}let s=t.get(i.target);s||(s=new Map,t.set(i.target,s));let l=s.get(a);if(l==null){let c=Ne(i);if(c.oldValue===c.newValue)continue;s.set(a,n.length),n.push({...i,...c})}else n[l]={...n[l],newValue:Ne(i).newValue}}let r=[];for(let i of n){if(Ae(i)==null){r.push(i);continue}i.oldValue!==i.newValue&&r.push(vt(i))}return r}var kt=new Set(["attr-set","attr-add","attr-remove","text","value"]);function Ae(e){return kt.has(e.kind)?e.kind==="text"?"#text":e.kind==="value"?`#${e.prop||"value"}`:e.name==null?null:`@${e.name}`:null}function Ne(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 vt(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 Te=new WeakMap,At=0;function T(e){let n=Te.get(e);return n||(n=`n${++At}`,Te.set(e,n)),n}function v(e){return!!e&&typeof e=="object"&&typeof e.nodeType=="number"}function Nt(e){return e.kind==="add"||e.kind==="remove"}function Ie(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 K(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 l of e.attributes)Et(l.name,e,n,t)||i.push(`${l.name}=${JSON.stringify(l.value)}`);i.sort()}let a=[];if(e.childNodes)for(let l of e.childNodes)St(l,r)||a.push(K(l,n,t,r));return`<${String(e.tagName||"").toLowerCase()}${i.length?` ${i.join(" ")}`:""}>${a.join("")}</>`}function Z(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)Z(t,n);return n}function Et(e,n,t,r=null){if(typeof t!="function")return!1;try{return!!t(e,n,r)}catch{return!1}}function St(e,n){if(typeof n!="function")return!1;try{return!!n(e)}catch{return!1}}function Pe(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 s of i.childNodes)r(s)};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 Tt(e,n,t=null){return(e||[]).filter(v).map(r=>E(r,n,t))}function z(e){return{replayable:!1,reason:e,steps:[]}}function U(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 s=e.indexOf(T(i));if(s>=0)return a(s)}return r?null:e.length}function Ce(e,n={}){let t=n.isIgnored||null,r=n.ignoreAttribute||null,i=Se(e),a=[];for(let p of i){if(!Nt(p)){if(!v(p.target))return z("non-dom-target");continue}if(!v(p.parent))return z("non-dom-parent");let y=Array.from(p.nodes||[]).filter(v);if(y.length!==0){if(y.length!==Array.from(p.nodes||[]).length)return z("non-dom-node");a.push({primitive:p,nodes:y,parent:p.parent})}}if(Vt(a))return z("in-record-move");let s=It(a,t);if(!s)return z("ambiguous-structure");let l=Pt(a,s),c=new Map(a.map(p=>[p.primitive,p])),h=[];for(let p of i){let y=c.get(p);if(!y){h.push(Rt(p));continue}let w=l.get(y);if(w){w.at===y&&h.push(w.step);continue}h.push(Ct(y,s.get(y),r,t))}return{replayable:!0,reason:null,steps:h}}function Vt(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 s=i.get(a);s||(s=new Set,i.set(a,s)),s.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 It(e,n){let t=new Map;for(let i of e)t.has(i.parent)||t.set(i.parent,Ie(i.parent,n));let r=new Map;for(let i=e.length-1;i>=0;i--){let a=e[i],s=t.get(a.parent);if(!s)return null;let l=s.slice(),c;if(a.primitive.kind==="add"){let h=a.nodes.map(T);if(!h.every(p=>s.includes(p)))return null;c=s.filter(p=>!h.includes(p))}else{let h=U(l,a.primitive);if(h==null)return null;c=l.slice(),c.splice(h,0,...a.nodes.map(T))}r.set(a,{before:c,after:l}),t.set(a.parent,c)}return r}function Pt(e,n){let t=new Map,r=new Map;e.forEach((a,s)=>{for(let l of a.nodes){let c=T(l);a.primitive.kind==="remove"?t.set(c,{item:a,index:s,node:l}):r.set(c,{item:a,index:s,node:l})}});let i=new Map;for(let[a,s]of t){let l=r.get(a);if(!l||l.index<s.index||s.item.nodes.length!==1||l.item.nodes.length!==1)continue;let c={type:"move",node:s.node,sourceParent:s.item.parent,destParent:l.item.parent,source:n.get(s.item),dest:n.get(l.item),sourceSlot:s.item.primitive,destSlot:l.item.primitive,targets:[E(s.node,"move")]};i.set(s.item,{step:c,at:l.item}),i.set(l.item,{step:c,at:l.item})}return i}function Ct(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=>K(a,t,e.parent,r)),ids:i==="remove"?e.nodes.flatMap(a=>Z(a)):[],targets:Tt(e.nodes,i==="add"?"added":"removed",e.parent)}}function Rt(e){return{type:e.kind,primitive:e,targets:[E(e.target,e.name||e.prop||"text")]}}function Re(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],s=xt(a,n,r);if(s)return{ok:!1,code:s.code,targets:s.targets}}return{ok:!0,operations:r.operations}}function M(e,n){return e.sim.has(n)||e.sim.set(n,Ie(n,e.isIgnored)),e.sim.get(n)}function L(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 Oe(e,n){return n.liveNodesWithIds(e).filter(t=>L(t,n)).length>0}function b(e,n){return{code:e,targets:n||[]}}function Ot(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 xt(e,n,t){let r=n==="undo";return e.type==="move"?zt(e,r,t):e.type==="add"?Mt(e,r,t):e.type==="restore"?Lt(e,r,t):_t(e,r,t)}function _t(e,n,t){let r=e.primitive,i=n?r.newValue:r.oldValue,a=n?r.oldValue:r.newValue;return v(r.target)?L(r.target,t)?r.kind==="value"&&typeof r.write=="function"&&typeof r.read!="function"?b("unsupported",e.targets):Ot(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 Mt(e,n,t){if(!v(e.parent)||!L(e.parent,t))return b("target-detached",e.targets);if(n){if(!q(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);let l=Pe(e.nodes,t.isProtected);if(l.length>0)return b("protected",l);for(let c=0;c<e.nodes.length;c++)if(K(e.nodes[c],t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[c])return b("content-changed",e.targets);for(let c of e.nodes)t.operations.push({type:"remove",node:c}),t.planned.set(c,!1);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!q(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);for(let l of e.nodes)if(l.parentNode)return b("structure-changed",e.targets);let r=e.nodes.flatMap(l=>Z(l));if(r.length>0&&Oe(r,t))return b("duplicate-id",e.targets);let i=U(e.seqBefore,e.slot);if(i==null)return b("unsupported",e.targets);let a=e.seqBefore.slice(),s=i;for(let l of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:l,beforeId:a[s]??null}),a=a.slice(0,s).concat(T(l),a.slice(s)),s+=1,t.planned.set(l,!0);return t.sim.set(e.parent,e.seqAfter.slice()),null}function Lt(e,n,t){if(!v(e.parent)||!L(e.parent,t))return b("target-detached",e.targets);if(n){if(!q(M(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);for(let c=0;c<e.nodes.length;c++){let h=e.nodes[c];if(h.parentNode||K(h,t.ignoreAttribute,e.parent,t.isIgnored)!==e.content[c])return b("content-changed",e.targets)}if(e.ids.length>0&&Oe(e.ids,t))return b("duplicate-id",e.targets);let a=U(e.seqAfter,e.slot);if(a==null)return b("unsupported",e.targets);let s=e.seqAfter.slice(),l=a;for(let c of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:c,beforeId:s[l]??null}),s=s.slice(0,l).concat(T(c),s.slice(l)),l+=1,t.planned.set(c,!0);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!q(M(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);let r=Pe(e.nodes,t.isProtected);if(r.length>0)return b("protected",r);let i=a=>t.planned.get(a)===!0||typeof t.isIgnored=="function"&&t.isIgnored(a);for(let a=0;a<e.nodes.length;a++){let s=e.nodes[a];if(s.parentNode!==e.parent||K(s,t.ignoreAttribute,e.parent,i)!==e.content[a])return b("content-changed",e.targets)}for(let a of e.nodes)t.operations.push({type:"remove",node:a}),t.planned.set(a,!1);return t.sim.set(e.parent,e.seqAfter.slice()),null}function qt(e,n){let t=e.indexOf(n);return t<0?null:e.slice(0,t).concat(e.slice(t+1))}function zt(e,n,t){let r=T(e.node),i=n?e.destParent:e.sourceParent,a=n?e.sourceParent:e.destParent,s=M(t,i),l=M(t,a),c=n?e.dest.after:e.source.before,h=n?e.source.after:e.dest.before,p=n?e.sourceSlot:e.destSlot;if(!v(i)||!v(a)||!L(i,t)||!L(a,t))return b("target-detached",e.targets);if(!q(s,c))return b("structure-changed",e.targets);let y=i===a?qt(l,r):l;if(!q(y,h))return b("structure-changed",e.targets);let w=U(h,p);return w==null?b("unsupported",e.targets):(t.operations.push({type:"insert",parent:a,node:e.node,beforeId:h[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 q(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 Kt(e,n){for(let t of e.childNodes||[])if(T(t)===n)return t;return null}function Ve(e,n){typeof e.write=="function"?e.write(e.target,n):e.target[e.prop]=n}function xe(e){let n=[];try{for(let t of e)$t(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 $t(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];Ve(e,e.value),n.push(()=>Ve(e,t));return}if(e.type==="insert"){let t=e.beforeId==null?null:Kt(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 Bt=['[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]"],Dt=["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"],_e=Bt.join(","),Me=new RegExp("^(?:"+Dt.join("|")+")(?:-|$)");var an=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"]})}),Ut=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),jt=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function j(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 Q(e,n){if(j(e,n))return!0;for(let[t,r]of Object.entries(Ut))if(r.includes(n)&&j(e,t))return!0;return!1}function Le(e){return e==="clay"||jt.includes(e)}var Ft=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function Wt(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function ee(e){let n=Wt();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(_e))return!0;for(;t&&t.nodeType===1;){if(Q(t,"no-undo")||Q(t,"no-watch"))return!0;for(let r of Ft)if(j(t,r))return!0;t=t.parentElement}return!1}function Xt(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),ee(r)}function F(e,n,t){if(t?.type==="attributes"&&Le(t.attributeName)){if(ee(e)&&Xt(t))return!0}else if(ee(e))return!0;if(t&&t.type==="attributes"){if(t.attributeName&&Me.test(t.attributeName.toLowerCase()))return!0;if(n)try{if(n(t.attributeName,t.target))return!0}catch{}}return!1}function qe(){let e=new Map;function n(i,a){let s=e.get(i);return s||(s=new Set,e.set(i,s)),s.add(a),()=>t(i,a)}function t(i,a){let s=e.get(i);s&&s.delete(a)}function r(i,a){let s=e.get(i);if(s)for(let l of Array.from(s))try{l(a)}catch(c){console.error("[hyper-undo] listener threw",c)}}return{on:n,off:t,emit:r}}var Ht={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",recording:"automatic",ignoreAttribute:null,isProtected:null,ignoreNode:null,debug:!1};function te(e){let n={...Ht,...e};if(!n.scope)throw new Error("hyper-undo: scope is required");let t=qe(),r=[],i=[],a=`u${Math.random().toString(36).slice(2,8)}`,s=0,l=null,c=[],h=null,p=null,y=0,w=!1,P=n.recording==="commands"?"commands":"automatic",A=null,C=!1;function k(...o){n.debug&&console.log("[hyper-undo]",...o)}function De(){w||(w=!0,p=Ue(R),p.observe(n.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),k("started"))}function Ue(o){let u=typeof document<"u"&&n.scope===document.body,d=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return u&&d&&typeof d.createObserver=="function"?(k("sourcing records from the platform Mutation hub (shared observer)"),d.createObserver(o)):new MutationObserver(o)}function ie(){w&&(w=!1,p&&(p.disconnect(),p=null),O(),r.length=0,i.length=0,c=[],l=null,A=null,C=!1,y=0,k("stopped"))}function je(){let o=n.scope;return!!(o&&o.nodeType===1&&o.isConnected===!1)}function R(o){ae(o,!1)}function ae(o,u){if(A){for(let f of o)A.records.push(f);return}if(P==="commands"||y>0)return;let d=W(Fe(o),u);if(d.length>0){for(let f of d)c.push(f);ue()}}function Fe(o){return typeof n.ignoreNode!="function"?o:o.filter(u=>{if(u.type!=="childList")return!0;let d=[...u.addedNodes,...u.removedNodes];return d.length===0?!0:!d.every(f=>We(f))})}function We(o){try{return!!n.ignoreNode(o)}catch{return!1}}function W(o,u=!1){let d=lt(o),f=o.filter(g=>(G(g.target)||d(g.target)||u&&ke(g.target))&&!F(g.target,n.ignoreAttribute,g));return Ee(f,g=>F(g,n.ignoreAttribute))}function ue(){O(),h=setTimeout(se,n.idleWindowMs)}function O(){h!=null&&(clearTimeout(h),h=null)}function se(){if(h=null,c.length===0)return;let o=c;c=[],$(n.idleLabel,o)}function $(o,u){let d=Ce(u,{isIgnored:X,ignoreAttribute:le});if(d.steps.length===0)return k("commit",o,"with nothing to record"),t.emit("commit"),null;let f={id:`${a}-${++s}`,label:o,timestamp:Date.now(),primitives:u,certificate:d};for(r.push(f),i.length=0,l=null;r.length>n.maxHistory;)r.shift();return k("commit",o,"primitives:",u.length),t.emit("commit"),f}function X(o){return F(o,n.ignoreAttribute)}function le(o,u,d=null){if(typeof n.ignoreAttribute!="function")return!1;try{return!!n.ignoreAttribute(o,u,d)}catch{return!1}}function Xe(o){if(typeof n.isProtected!="function")return!1;try{return!!n.isProtected(o)}catch{return!1}}function He(o,u){if(!w)throw new Error("hyper-undo: scope not started");if(P==="commands")return fe(o,u,{honorPause:!0,receipt:!1});R(p.takeRecords()),x();let d=y===0?de():null,f=u();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(ae(p.takeRecords(),!0),d!=null&&ce(d),c.length===0){t.emit("commit");return}O();let g=c;c=[],$(o,g)}function Je(o){if(!w)throw new Error("hyper-undo: scope not started");if(!p)return;if(y>1){p.takeRecords();return}x();let u=W(p.takeRecords(),!0);u.length!==0&&(O(),$(o,u))}function Ge(){p&&p.takeRecords()}function H(){let o=n.scope;return o&&o.ownerDocument||(typeof document<"u"?document:null)}function B(){let o=H();return!o||!o.head||typeof o.head.querySelector!="function"?null:o.head.querySelector("title")}function de(){let o=B();return o?o.textContent:null}function ce(o){let u=B();if(!u||o==null)return;let d=u.textContent;d!==o&&pe({kind:"value",target:u,prop:"textContent",oldValue:o,newValue:d})}function Ye(o){if(!o)return null;let u=H();if(!u||o.target!==u||o.prop!=="title")return o;let d=B();return d?{kind:"value",target:d,prop:"textContent",oldValue:o.oldValue,newValue:o.newValue}:null}function Ze(){if(typeof window>"u")return!1;let o=window.clay?.effect||window.hyperclay?.effect;return!!(o&&o.running)}function Qe(){p&&R(p.takeRecords()),x(),A={records:[],primitives:[]}}function et(){let o=A;if(A=null,!o)return[];let u=W(o.records,!0);for(let d of o.primitives)u.push(d);return u}function fe(o,u,{honorPause:d,receipt:f}){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(Ze())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 g=d&&y>0,N=g?null:de();Qe();let S=[],V=null,_;try{try{_=u()}catch(ve){V=ve}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"))}g||ce(N)}finally{S=et()}let D=g||S.length===0?null:$(o,S);if(V)throw V;return f?D?{id:D.id,label:D.label}:null:D}function tt(o,u){if(!w)throw new Error("hyper-undo: scope not started");return fe(o,u,{honorPause:!1,receipt:!0})}function nt(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||(x(),P=o,k("recording",o)),P}function me(o){if(!w||!o||!A&&(y>0||P==="commands"))return;let u=Ye(o);u&&pe(u)}function pe(o){if(A){A.primitives.push(o);return}c.push(o),ue()}function rt(o,{prop:u="value",oldValue:d,newValue:f,read:g,write:N}={}){!o||d===f||me({kind:"value",target:o,prop:u,oldValue:d,newValue:f,read:g,write:N})}function x(){y===0&&p&&R(p.takeRecords()),c.length!==0&&(O(),se())}function ge(){y===0&&p&&R(p.takeRecords()),y++,k("paused",y)}function he(){y!==0&&(y--,y===0&&p&&p.takeRecords(),k("resumed",y))}function ye(o,u){if(je())return ie(),{ok:!1,code:"disconnected"};x();let d=o==="undo"?r:i,f=o==="undo"?i:r,g=d.at(-1);if(!g)return{ok:!1,code:"empty"};if(u&&u!==g.id){let _=be(u)||g;return J(_,o,"stale-entry")}let N;C=!0;try{N=Re(g.certificate,o,{isIgnored:X,ignoreAttribute:le,isProtected:Xe,inScope:dt,liveNodesWithIds:ct})}finally{C=!1}if(!N.ok)return J(g,o,N.code,N.targets);ge();let S;C=!0;try{S=xe(N.operations)}finally{C=!1,he()}if(!S.ok)return k(`${o} failed and was rolled back`,S.error),J(g,o,"replay-error");d.pop(),f.push(g),l=null;let V={ok:!0,entryId:g.id,label:g.label,direction:o};return k(o,g.label),t.emit(o,V),V}function ot(o){return ye("undo",o&&o.entryId)}function it(o){return ye("redo",o&&o.entryId)}function be(o){return r.find(u=>u.id===o)||i.find(u=>u.id===o)||null}function J(o,u,d,f=[]){return l={entryId:o.id,direction:u,code:d,targets:f},k("refused",u,d),t.emit("refused",{entryId:o.id,label:o.label,direction:u,code:d,targets:f}),{ok:!1,code:d,entryId:o.id,label:o.label,direction:u}}function at(o){if(!l||l.entryId!==o)return{ok:!1,code:"not-refused",entryId:o};let u=l.direction,d=u==="undo"?r:i,f=d.at(-1);return!f||f.id!==o?{ok:!1,code:"stale-entry",entryId:o}:(d.pop(),l=null,k("discarded",f.label),t.emit("discard",{entryId:o,label:f.label,direction:u}),{ok:!0,entryId:o,direction:u})}function ut(o){let u=be(o);if(!u||!u.certificate||!u.certificate.replayable)return null;let d=l&&l.entryId===u.id?{direction:l.direction,code:l.code,targets:(l.targets||[]).map(f=>({id:f.id??null,kind:f.kind??null}))}:null;return{id:u.id,label:u.label,timestamp:u.timestamp,refused:d,steps:u.certificate.steps.map(st)}}function st(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,d={id:E(u.target).id},f=u.oldValue??null,g=u.newValue??null;return o.type==="value"?{kind:"value",target:d,prop:u.prop??"value",before:f,after:g}:o.type==="text"?{kind:"text",target:d,before:f,after:g}:o.type==="attr-set"||o.type==="attr-add"||o.type==="attr-remove"?{kind:"attribute",target:d,name:u.name??null,before:f,after:g}:{kind:o.type,target:d,before:f,after:g}}function lt(o){let u=new Set,d=!0;for(;d;){d=!1;for(let f of o)if(!(f.type!=="childList"||!f.removedNodes?.length)&&!(!G(f.target)&&!we(f.target,u)))for(let g of f.removedNodes)u.has(g)||(u.add(g),d=!0)}return f=>!G(f)&&we(f,u)}function we(o,u){for(let d=o;d;d=d.parentNode)if(u.has(d))return!0;return!1}function G(o){let u=n.scope;return!u||!o?!1:o===u||typeof u.contains=="function"&&u.contains(o)}function ke(o){let u=H(),d=u&&u.head;if(!d||!o)return!1;let f=o.nodeType===1?o:o.parentElement;for(;f&&f!==d;f=f.parentElement)if(f.localName==="title")return!1;return f===d}function dt(o){let u=n.scope;return!o||!u?!1:o===u||B()===o||ke(o)?!0:typeof u.contains=="function"?u.contains(o):!1}function ct(o){let u=n.scope;if(!o||o.length===0||typeof u?.querySelectorAll!="function")return[];let d=new Set(o),f=[];for(let g of u.querySelectorAll("[data-id]"))d.has(g.getAttribute("data-id"))&&(X(g)||f.push(g));return f}function ft(){p&&p.takeRecords(),r.length=0,i.length=0,c=[],l=null,O(),k("cleared"),t.emit("clear")}return{_config:n,start:De,stop:ie,commit:He,commitCaptured:Je,discardCaptured:Ge,flush:x,command:tt,setRecording:nt,record:me,recordValue:rt,undo:ot,redo:it,clear:ft,discard:at,describe:ut,pause:ge,resume:he,on:t.on,off:t.off,get canUndo(){return r.length>0},get canRedo(){return i.length>0},get isPaused(){return y>0},get history(){return r.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return l&&l.entryId===o.id&&(u.refused={direction:l.direction,code:l.code,targets:l.targets}),u})},get future(){return i.map(o=>{let u={id:o.id,label:o.label,timestamp:o.timestamp};return l&&l.entryId===o.id&&(u.refused={direction:l.direction,code:l.code,targets:l.targets}),u}).reverse()},get scope(){return n.scope}}}var Jt=new Set(["checkbox","radio","button","submit","reset","image","file","range","color"]);function Gt(e){let n=typeof e.composedPath=="function"?e.composedPath():[],t=n.length>0?n:Yt(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"&&!Jt.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 Yt(e){let n=[];for(let t=e;t;t=t.parentNode||(t.host??null))n.push(t);return n}function ze(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||Gt(r))return;let a=r.target,s=e._config.shadowKeydownIn;if(a&&a.closest&&Array.isArray(s))for(let h of s)try{if(a.closest(h))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let c=(r.key||"").toLowerCase();if(c==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(c==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(c==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",t,!0),()=>window.removeEventListener("keydown",t,!0)}var ne=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]"],m=null,re=null,Ke=null,I=null;function $e(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=ze(e,n);return I={inst:e,cleanup:t},t}function Be(e){I&&I.inst===e&&(I.cleanup(),I=null)}var oe={defaults:{shadowKeydownIn:ne},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(m){if(re===n)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),m;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||ne};return m=te(t),m.start(),re=n,e.bindKeys!==!1&&(Ke=$e(m,()=>oe.stop())),m},stop(){m&&(Be(m),Ke=null,m.stop(),m=null,re=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||ne},r=te(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=$e(r,()=>a.stop())),r.start()},stop(){Be(r),i=null,r.stop()}};return a},commit(e,n){return m?m.commit(e,n):void 0},command(e,n){return m?m.command(e,n):void 0},setRecording(e){return m?m.setRecording(e):void 0},commitCaptured(e){return m?m.commitCaptured(e):void 0},discardCaptured(){return m?m.discardCaptured():void 0},record(e){return m?m.record(e):void 0},recordValue(e,n){return m?m.recordValue(e,n):void 0},undo(e){return m?m.undo(e):void 0},redo(e){return m?m.redo(e):void 0},clear(){return m?m.clear():void 0},discard(e){return m?m.discard(e):void 0},describe(e){return m?m.describe(e):void 0},pause(){return m?m.pause():void 0},resume(){return m?m.resume():void 0},flush(){return m?m.flush():void 0},on(e,n){return m?m.on(e,n):void 0},off(e,n){return m?m.off(e,n):void 0},get canUndo(){return m?m.canUndo:!1},get canRedo(){return m?m.canRedo:!1},get history(){return m?m.history:[]},get future(){return m?m.future:[]},get isPaused(){return m?m.isPaused:!1}};var Zt=oe;return bt(Qt);})();
|
|
1
|
+
var hyperundo=(()=>{var he=Object.defineProperty;var Kt=Object.getOwnPropertyDescriptor;var Bt=Object.getOwnPropertyNames;var Ft=Object.prototype.hasOwnProperty;var qt=(e,n)=>{for(var t in n)he(e,t,{get:n[t],enumerable:!0})},zt=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let a of Bt(n))!Ft.call(e,a)&&a!==t&&he(e,a,{get:()=>n[a],enumerable:!(o=Kt(n,a))||o.enumerable});return e};var $t=e=>zt(he({},"__esModule",{value:!0}),e);var Rn={};qt(Rn,{default:()=>Pn,endsSentence:()=>ke,undo:()=>Ne});function Ut(e,n=null){let t=[];if(e.type==="attributes"){let o=e.target,a=e.attributeName,i=e.oldValue,s=o.getAttribute(a);return i==null&&s!=null?t.push({kind:"attr-add",target:o,name:a,newValue:s}):i!=null&&s==null?t.push({kind:"attr-remove",target:o,name:a,oldValue:i}):i!==s&&t.push({kind:"attr-set",target:o,name:a,oldValue:i,newValue:s}),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 o=e.target,a=e.nextSibling,i=e.nextSibling;for(;i&&n?.(i);)i=i.nextSibling;let s=e.previousSibling;for(;s&&n?.(s);)s=s.previousSibling;let d=n?Array.from(e.addedNodes).filter(y=>!n(y)):Array.from(e.addedNodes),f=n?Array.from(e.removedNodes).filter(y=>!n(y)):Array.from(e.removedNodes);return d.length>0&&t.push({kind:"add",parent:o,nodes:d,before:a,afterAuthored:i,beforeAuthored:s,record:e}),f.length>0&&t.push({kind:"remove",parent:o,nodes:f,before:a,afterAuthored:i,beforeAuthored:s,record:e}),t}return t}function We(e,n=null){let t=[],o=new Map;for(let a of e){if(a.type==="attributes"){let i=a.target,s=a.attributeName,d=o.get(i);d||(d=new Map,o.set(i,d)),d.has(s)||(d.set(s,{idx:t.length,oldValue:a.oldValue}),t.push(null));continue}for(let i of Ut(a,n))t.push(i)}for(let[a,i]of o)for(let[s,d]of i){let f=d.oldValue,y=a.getAttribute(s),p=null;f===y?p=null:f==null?p={kind:"attr-add",target:a,name:s,newValue:y}:y==null?p={kind:"attr-remove",target:a,name:s,oldValue:f}:p={kind:"attr-set",target:a,name:s,oldValue:f,newValue:y},t[d.idx]=p}return t.filter(a=>a!=null)}function je(e,{sameContent:n=(t,o)=>t===o}={}){let t=[],o=new Map;for(let i of e){let s=$e(i);if(s==null){t.push(i);continue}let d=o.get(i.target);d||(d=new Map,o.set(i.target,d));let f=d.get(s);if(f==null){let y=Ue(i);if(y.oldValue===y.newValue)continue;d.set(s,t.length),t.push({...i,...y})}else t[f]={...t[f],newValue:Ue(i).newValue}}let a=[];for(let i of t){if($e(i)==null){a.push(i);continue}i.oldValue!==i.newValue&&(i.kind==="value"&&i.content===!0&&n(i.oldValue,i.newValue)||a.push(jt(i)))}return a}var Wt=new Set(["attr-set","attr-add","attr-remove","text","value"]);function $e(e){return Wt.has(e.kind)?e.kind==="text"?"#text":e.kind==="value"?`#${e.prop||"value"}`:e.name==null?null:`@${e.name}`:null}function Ue(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 jt(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 Xe=new WeakMap,Xt=0;function _(e){let n=Xe.get(e);return n||(n=`n${++Xt}`,Xe.set(e,n)),n}function N(e){return!!e&&typeof e=="object"&&typeof e.nodeType=="number"}function Ht(e){return e.kind==="add"||e.kind==="remove"}function Ge(e,n){let t=[],o=e.childNodes;if(!o)return t;for(let a of o)n&&n(a)||t.push(_(a));return t}function He(e,n,t){if(typeof n!="function"||!N(e)||e.nodeType!==1)return null;let o=null;try{o=n(e,t)}catch{o=null}return typeof o=="string"?`#form:${JSON.stringify(o)}`:null}function Q(e,n,t=null,o=null,a=null,i=null){if(!N(e))return null;let s=He(e,a,t);if(s!=null)return s;if(e.nodeType===3)return`#text:${JSON.stringify(i&&i.has(e)?i.get(e):e.data)}`;if(e.nodeType===8)return`#comment:${JSON.stringify(e.data)}`;if(e.nodeType!==1)return`#node:${e.nodeType}`;let d=[];if(e.attributes){for(let p of e.attributes)Jt(p.name,e,n,t)||d.push(`${p.name}=${JSON.stringify(p.value)}`);d.sort()}let f=[];if(e.childNodes)for(let p of e.childNodes){let w=He(p,a,t);if(w!=null){f.push(w);continue}Yt(p,o)||f.push(Q(p,n,t,o,a,i))}return`<${String(e.tagName||"").toLowerCase()}${d.length?` ${d.join(" ")}`:""}>${f.join("")}</>`}function ye(e,n=[]){if(!N(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)ye(t,n);return n}function Jt(e,n,t,o=null){if(typeof t!="function")return!1;try{return!!t(e,n,o)}catch{return!1}}function Yt(e,n){if(typeof n!="function")return!1;try{return!!n(e)}catch{return!1}}function Ze(e,n){if(typeof n!="function")return[];let t=[],o=a=>{if(!N(a))return;let i=!1;try{i=!!n(a)}catch{i=!1}if(i&&t.push(I(a,"protected")),a.childNodes)for(let s of a.childNodes)o(s)};for(let a of e)o(a);return t}function I(e,n,t=null){let o=e;o&&o.nodeType!==1&&(o=o.parentElement);let a=o&&typeof o.closest=="function"?o.closest("[data-id]"):null;return!a&&t&&typeof t.closest=="function"&&(a=t.closest("[data-id]")),{id:(a?a.getAttribute("data-id"):null)||null,kind:n||null}}function Gt(e,n,t=null){return(e||[]).filter(N).map(o=>I(o,n,t))}function Z(e){return{replayable:!1,reason:e,steps:[]}}function ie(e,n){let t=[[n.afterAuthored,a=>a],[n.beforeAuthored,a=>a+1],[n.before,a=>a]],o=!1;for(let[a,i]of t){if(!N(a))continue;o=!0;let s=e.indexOf(_(a));if(s>=0)return i(s)}return o?null:e.length}function Qe(e,n={}){let t=n.isIgnored||null,o=n.ignoreAttribute||null,a=n.durableForm||null,i=je(e,{sameContent:ae}),s=[];for(let w of i){if(!Ht(w)){if(!N(w.target))return Z("non-dom-target");continue}if(!N(w.parent))return Z("non-dom-parent");let k=Array.from(w.nodes||[]).filter(N);if(k.length!==0){if(k.length!==Array.from(w.nodes||[]).length)return Z("non-dom-node");s.push({primitive:w,nodes:k,parent:w.parent})}}if(Zt(s))return Z("in-record-move");let d=Qt(s,t);if(!d)return Z("ambiguous-structure");let f=en(s,d),y=new Map(s.map(w=>[w.primitive,w])),p=[];for(let w of i){let k=y.get(w);if(!k){p.push(nn(w));continue}let A=f.get(k);if(A){A.at===k&&p.push(A.step);continue}p.push(tn(k,d.get(k),o,t,a))}return{replayable:!0,reason:null,steps:p}}function Zt(e){let n=new Map;for(let t of e){let o=t.primitive.record;if(!o)continue;let a=n.get(o);a||(a=new Map,n.set(o,a));for(let i of t.nodes){let s=a.get(i);s||(s=new Set,a.set(i,s)),s.add(t.primitive.kind)}}for(let t of n.values())for(let o of t.values())if(o.size>1)return!0;return!1}function Qt(e,n){let t=new Map;for(let a of e)t.has(a.parent)||t.set(a.parent,Ge(a.parent,n));let o=new Map;for(let a=e.length-1;a>=0;a--){let i=e[a],s=t.get(i.parent);if(!s)return null;let d=s.slice(),f;if(i.primitive.kind==="add"){let y=i.nodes.map(_);if(!y.every(p=>s.includes(p)))return null;f=s.filter(p=>!y.includes(p))}else{let y=ie(d,i.primitive);if(y==null)return null;f=d.slice(),f.splice(y,0,...i.nodes.map(_))}o.set(i,{before:f,after:d}),t.set(i.parent,f)}return o}function en(e,n){let t=new Map,o=new Map;e.forEach((i,s)=>{for(let d of i.nodes){let f=_(d);i.primitive.kind==="remove"?t.set(f,{item:i,index:s,node:d}):o.set(f,{item:i,index:s,node:d})}});let a=new Map;for(let[i,s]of t){let d=o.get(i);if(!d||d.index<s.index||s.item.nodes.length!==1||d.item.nodes.length!==1)continue;let f={type:"move",node:s.node,sourceParent:s.item.parent,destParent:d.item.parent,source:n.get(s.item),dest:n.get(d.item),sourceSlot:s.item.primitive,destSlot:d.item.primitive,targets:[I(s.node,"move")]};a.set(s.item,{step:f,at:d.item}),a.set(d.item,{step:f,at:d.item})}return a}function tn(e,n,t,o,a=null){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(s=>Q(s,t,e.parent,o,a)),ids:i==="remove"?e.nodes.flatMap(s=>ye(s)):[],targets:Gt(e.nodes,i==="add"?"added":"removed",e.parent)}}function nn(e){return{type:e.kind,primitive:e,targets:[I(e.target,e.name||e.prop||"text")]}}function et(e,n,t={}){if(!e||!e.replayable)return{ok:!1,code:"unsupported",targets:[]};let o={isIgnored:t.isIgnored||null,ignoreAttribute:t.ignoreAttribute||null,durableForm:t.durableForm||null,isProtected:t.isProtected||null,inScope:t.inScope||(()=>!0),liveNodesWithIds:t.liveNodesWithIds||(()=>[]),sim:new Map,planned:new Map,texts:new Map,operations:[]};for(let a=0;a<e.steps.length;a++){let i=e.steps[n==="undo"?e.steps.length-1-a:a],s=on(i,n,o);if(s)return{ok:!1,code:s.code,targets:s.targets}}return{ok:!0,operations:o.operations}}function X(e,n){return e.sim.has(n)||e.sim.set(n,Ge(n,e.isIgnored)),e.sim.get(n)}function H(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 tt(e,n){return n.liveNodesWithIds(e).filter(t=>H(t,n)).length>0}function b(e,n){return{code:e,targets:n||[]}}function ae(e,n){return e===n?!0:typeof e!="string"||typeof n!="string"?!1:e.replace(/\u00A0| /g," ")===n.replace(/\u00A0| /g," ")}function rn(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 on(e,n,t){let o=n==="undo";return e.type==="move"?dn(e,o,t):e.type==="add"?un(e,o,t):e.type==="restore"?sn(e,o,t):an(e,o,t)}function an(e,n,t){let o=e.primitive,a=n?o.newValue:o.oldValue,i=n?o.oldValue:o.newValue;if(!N(o.target))return b("unsupported",e.targets);if(!H(o.target,t))return b("target-detached",e.targets);if(o.kind==="value"&&typeof o.write=="function"&&typeof o.read!="function")return b("unsupported",e.targets);let s=rn(o);return(o.content===!0?!ae(s,a):s!==a)?b("content-changed",e.targets):(o.kind==="value"?t.operations.push({type:"value",target:o.target,prop:o.prop||"value",value:i,read:o.read||null,write:o.write||null,content:o.content===!0}):o.kind==="text"?(t.operations.push({type:"text",target:o.target,data:i}),t.texts.set(o.target,i)):t.operations.push({type:"attr",target:o.target,name:o.name,value:i??null}),null)}function un(e,n,t){if(!N(e.parent)||!H(e.parent,t))return b("target-detached",e.targets);if(n){if(!J(X(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);let d=Ze(e.nodes,t.isProtected);if(d.length>0)return b("protected",d);for(let f=0;f<e.nodes.length;f++)if(Q(e.nodes[f],t.ignoreAttribute,e.parent,t.isIgnored,t.durableForm)!==e.content[f])return b("content-changed",e.targets);for(let f of e.nodes)t.operations.push({type:"remove",node:f}),t.planned.set(f,!1);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!J(X(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);for(let d of e.nodes)if(d.parentNode)return b("structure-changed",e.targets);let o=e.nodes.flatMap(d=>ye(d));if(o.length>0&&tt(o,t))return b("duplicate-id",e.targets);let a=ie(e.seqBefore,e.slot);if(a==null)return b("unsupported",e.targets);let i=e.seqBefore.slice(),s=a;for(let d of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:d,beforeId:i[s]??null}),i=i.slice(0,s).concat(_(d),i.slice(s)),s+=1,t.planned.set(d,!0);return t.sim.set(e.parent,e.seqAfter.slice()),null}function sn(e,n,t){if(!N(e.parent)||!H(e.parent,t))return b("target-detached",e.targets);if(n){if(!J(X(t,e.parent),e.seqAfter))return b("structure-changed",e.targets);for(let f=0;f<e.nodes.length;f++){let y=e.nodes[f];if(y.parentNode||Q(y,t.ignoreAttribute,e.parent,t.isIgnored,t.durableForm)!==e.content[f])return b("content-changed",e.targets)}if(e.ids.length>0&&tt(e.ids,t))return b("duplicate-id",e.targets);let i=ie(e.seqAfter,e.slot);if(i==null)return b("unsupported",e.targets);let s=e.seqAfter.slice(),d=i;for(let f of e.nodes)t.operations.push({type:"insert",parent:e.parent,node:f,beforeId:s[d]??null}),s=s.slice(0,d).concat(_(f),s.slice(d)),d+=1,t.planned.set(f,!0);return t.sim.set(e.parent,e.seqBefore.slice()),null}if(!J(X(t,e.parent),e.seqBefore))return b("structure-changed",e.targets);let o=Ze(e.nodes,t.isProtected);if(o.length>0)return b("protected",o);let a=i=>t.planned.get(i)===!0||typeof t.isIgnored=="function"&&t.isIgnored(i);for(let i=0;i<e.nodes.length;i++){let s=e.nodes[i];if(s.parentNode!==e.parent||Q(s,t.ignoreAttribute,e.parent,a,t.durableForm,t.texts)!==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 ln(e,n){let t=e.indexOf(n);return t<0?null:e.slice(0,t).concat(e.slice(t+1))}function dn(e,n,t){let o=_(e.node),a=n?e.destParent:e.sourceParent,i=n?e.sourceParent:e.destParent,s=X(t,a),d=X(t,i),f=n?e.dest.after:e.source.before,y=n?e.source.after:e.dest.before,p=n?e.sourceSlot:e.destSlot;if(!N(a)||!N(i)||!H(a,t)||!H(i,t))return b("target-detached",e.targets);if(!J(s,f))return b("structure-changed",e.targets);let w=a===i?ln(d,o):d;if(!J(w,y))return b("structure-changed",e.targets);let k=ie(y,p);return k==null?b("unsupported",e.targets):(t.operations.push({type:"insert",parent:i,node:e.node,beforeId:y[k]??null}),t.planned.set(e.node,!0),n?(t.sim.set(i,e.source.before.slice()),a!==i&&t.sim.set(a,e.dest.before.slice())):(t.sim.set(i,e.dest.after.slice()),a!==i&&t.sim.set(a,e.source.after.slice())),null)}function J(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 cn(e,n){for(let t of e.childNodes||[])if(_(t)===n)return t;return null}function Je(e,n){let t=Math.min(e.length,n.length),o=0;for(;o<t&&e[o]===n[o];)o++;let a=0;for(;a<t-o&&e[e.length-1-a]===n[n.length-1-a];)a++;return n.length-a}function Ye(e,n){typeof e.write=="function"?e.write(e.target,n):e.target[e.prop]=n}function nt(e){let n=[];try{let t=null;for(let o of e)t=fn(o,n)||t;return{ok:!0,caret:t}}catch(t){for(let o=n.length-1;o>=0;o--)try{n[o]()}catch{}return{ok:!1,error:t}}}function fn(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;return e.target.data=e.data,n.push(()=>{e.target.data=t}),{node:e.target,offset:Je(t,e.data)}}if(e.type==="value"){let t=typeof e.read=="function"?e.read(e.target):e.target[e.prop],o=e.content?e.target.textContent:null;if(Ye(e,e.value),n.push(()=>Ye(e,t)),!e.content)return;let a=e.target.textContent;return a===o?void 0:{root:e.target,textOffset:Je(o,a)}}if(e.type==="insert"){let t=e.beforeId==null?null:cn(e.parent,e.beforeId);if(e.beforeId!=null&&!t)throw new Error("hyper-undo: replay anchor vanished");let o=e.node.parentNode,a=e.node.nextSibling;return e.parent.insertBefore(e.node,t),n.push(()=>{o?o.insertBefore(e.node,a&&a.parentNode===o?a:null):e.node.parentNode===e.parent&&e.parent.removeChild(e.node)}),e.node.nodeType!==3?void 0:{node:e.node,offset:e.node.data.length}}if(e.type==="remove"){let t=e.node.parentNode;if(!t)throw new Error("hyper-undo: replay target is no longer attached");let o=e.node.nextSibling,a=Array.prototype.indexOf.call(t.childNodes,e.node);return t.removeChild(e.node),n.push(()=>t.insertBefore(e.node,o)),e.node.nodeType!==3?void 0:{node:t,offset:a}}}var pn=['[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]"],mn=["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"],rt=pn.join(","),ot=new RegExp("^(?:"+mn.join("|")+")(?:-|$)");var Dn=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"]})}),gn=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),hn=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui","live"]);function ue(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 we(e,n){if(ue(e,n))return!0;for(let[t,o]of Object.entries(gn))if(o.includes(n)&&ue(e,t))return!0;return!1}function it(e){return e==="clay"||hn.includes(e)}var yn=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function wn(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function be(e){let n=wn();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(rt))return!0;for(;t&&t.nodeType===1;){if(we(t,"no-undo")||we(t,"no-watch"))return!0;for(let o of yn)if(ue(t,o))return!0;t=t.parentElement}return!1}function bn(e){let n=e.target,t=n.cloneNode(!1),o=t;for(;n.parentElement;){n=n.parentElement;let a=n.cloneNode(!1);a.appendChild(t),t=a}return e.oldValue==null?o.removeAttribute(e.attributeName):o.setAttribute(e.attributeName,e.oldValue),be(o)}function ee(e,n,t){if(t?.type==="attributes"&&it(t.attributeName)){if(be(e)&&bn(t))return!0}else if(be(e))return!0;if(t&&t.type==="attributes"){if(t.attributeName&&ot.test(t.attributeName.toLowerCase()))return!0;if(n)try{if(n(t.attributeName,t.target))return!0}catch{}}return!1}function at(){let e=new Map;function n(a,i){let s=e.get(a);return s||(s=new Set,e.set(a,s)),s.add(i),()=>t(a,i)}function t(a,i){let s=e.get(a);s&&s.delete(i)}function o(a,i){let s=e.get(a);if(s)for(let d of Array.from(s))try{d(i)}catch(f){console.error("[hyper-undo] listener threw",f)}}return{on:n,off:t,emit:o}}var kn=/[.?!]["'\u201D\u2019)\]]*$/,vn=new Set(["insertParagraph","insertLineBreak"]),En=new Set(["insertFromPaste","insertFromDrop","insertFromYank","insertReplacementText","deleteByCut","deleteByDrag"]),An=new Set(["Enter","Backspace","Delete"]),Sn=new Set(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Home","End","PageUp","PageDown"]);function ke(e){return kn.test(e)}function ut(e,{sentence:n,endStep:t,stepOpen:o=()=>!0,composing:a}){let i=e.ownerDocument&&e.ownerDocument.defaultView;if(!i)return()=>{};let s=m=>!!m&&typeof m.nodeType=="number"&&(m===e||e.contains(m)),d=!1,f=!1,y=!1,p={kind:"other",host:null},w=m=>En.has(m)||m.startsWith("format")?"alone":m.startsWith("delete")?"delete":m.startsWith("insert")?"insert":"other",k=m=>{if(!m)return null;let T=m.nodeType===1?m:m.parentElement;return T&&T.closest&&T.closest('[contenteditable]:not([contenteditable="false"])')||m},A=()=>{d&&(d=!1,t())},S=m=>{if(!n||m.isComposing||!s(m.target))return;let T=m.inputType||"",V=w(T),Y=k(m.target),te=i.getSelection&&i.getSelection(),G=!te||te.isCollapsed,se=f;f=!1;let U=y;if(y=!1,(V==="alone"||!U&&V!==p.kind&&V!=="other"&&p.kind!=="other"||!U&&(p.host!==null&&Y!==p.host||!G||se))&&t(),p={kind:V,host:Y},V==="alone"){d=!0,setTimeout(A,0);return}let W=T==="insertText"&&(m.data===" "||m.data==="\xA0");!W&&!vn.has(T)||W&&!o()||ke(Nn(i))&&(d=!0,setTimeout(A,0))},R=m=>{s(m.target)&&A()},C=m=>{s(m.target)&&a(!0)},D=m=>{s(m.target)&&a(!1)},K=m=>{s(m.target)&&(f=!0)},B=m=>{if(!s(m.target))return;if(Sn.has(m.key)){f=!0;return}if(!n||m.isComposing||m.keyCode===229||m.ctrlKey||m.metaKey||!m.key||m.key.length!==1&&!An.has(m.key))return;let T=i.getSelection&&i.getSelection(),V=p.host!==null&&k(m.target)!==p.host;!f&&!V&&(!T||T.isCollapsed)||(t(),f=!1,y=!0,setTimeout(()=>{y=!1},0))};return e.addEventListener("beforeinput",S,!0),e.addEventListener("input",R,!0),e.addEventListener("compositionstart",C,!0),e.addEventListener("compositionend",D,!0),e.addEventListener("pointerdown",K,!0),e.addEventListener("keydown",B,!0),()=>{e.removeEventListener("beforeinput",S,!0),e.removeEventListener("input",R,!0),e.removeEventListener("compositionstart",C,!0),e.removeEventListener("compositionend",D,!0),e.removeEventListener("pointerdown",K,!0),e.removeEventListener("keydown",B,!0)}}function Nn(e){let n=e.getSelection&&e.getSelection();if(!n||!n.isCollapsed||!n.anchorNode)return"";let t=n.anchorNode,o=t.nodeType===1?t:t.parentElement,a=o&&o.closest('[contenteditable]:not([contenteditable="false"])');if(!a)return"";let i=a.ownerDocument.createRange();return i.setStart(a,0),i.setEnd(t,n.anchorOffset),i.toString().replace(/\u200B/g,"").slice(-8)}var Tn={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",recording:"automatic",ignoreAttribute:null,isProtected:null,ignoreNode:null,durableForm:null,typingBreak:null,refused:null,debug:!1};function ve(e){let n={...Tn,...e};if(!n.scope)throw new Error("hyper-undo: scope is required");let t=at(),o=[],a=[],i=`u${Math.random().toString(36).slice(2,8)}`,s=0,d=null,f=[],y=null,p=null,w=0,k=!1,A=n.recording==="commands"?"commands":"automatic",S=null,R=!1,C=new WeakMap,D=!1,K=!1,B=null;function m(...r){n.debug&&console.log("[hyper-undo]",...r)}function T(){k||(k=!0,p=V(F),p.observe(n.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),B=ut(n.scope,{sentence:n.typingBreak==="sentence",stepOpen:()=>(w===0&&p&&F(p.takeRecords()),f.length>0),endStep:z,composing:yt}),m("started"))}function V(r){let u=typeof document<"u"&&n.scope===document.body,l=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return u&&l&&typeof l.createObserver=="function"?(m("sourcing records from the platform Mutation hub (shared observer)"),l.createObserver(r)):new MutationObserver(r)}function Y(){k&&(k=!1,p&&(p.disconnect(),p=null),B&&(B(),B=null),D=!1,q(),o.length=0,a.length=0,f=[],d=null,S=null,R=!1,w=0,C=new WeakMap,m("stopped"))}function te(){let r=n.scope;return!!(r&&r.nodeType===1&&r.isConnected===!1)}function G(r,u){try{let l=u(r);return typeof l=="string"?l:null}catch{return null}}function se(r,{read:u,write:l,prop:c="content"}={}){if(!r||typeof u!="function"||typeof l!="function")return()=>{};let h={prop:c,read:u,write:l,baseline:G(r,u)};return C.set(r,h),()=>{C.get(r)===h&&C.delete(r)}}function U(r){for(let u=r;u;u=u.parentNode)if(C.has(u))return u;return null}function W(r){let u=U(r.target);return!!u&&!(r.type==="attributes"&&r.target===u)}function L(r){let u=new Set;for(let l of r)W(l)&&u.add(U(l.target));return u}function le(r){let u=[];for(let l of r){let c=C.get(l),h=G(l,c.read);if(h!=null){if(ae(h,c.baseline)){c.baseline=h;continue}c.baseline!=null&&u.push({kind:"value",target:l,prop:c.prop,oldValue:c.baseline,newValue:h,read:c.read,write:c.write,content:!0}),c.baseline=h}}return u}function j(r){for(let u of r){let l=C.get(u);l.baseline=G(u,l.read)}}function Te(r){let u=L(r);!K&&f.some(l=>l.kind==="value"?u.has(l.target):r.some(c=>c.target===l.target&&(c.type==="characterData"?l.kind==="text":c.type==="attributes"&&c.attributeName===l.name)))&&(K=!0),j(u)}function F(r){Ce(r,!1)}function Ce(r,u){if(S){for(let c of r)S.records.push(c);return}if(A==="commands"){j(L(r));return}if(w>0){Te(r);return}let l=de(gt(r),u);for(let c of le(L(r)))l.push(c);if(l.length>0){for(let c of l)f.push(c);ce()}}function gt(r){return typeof n.ignoreNode!="function"?r:r.filter(u=>{if(u.type!=="childList")return!0;let l=[...u.addedNodes,...u.removedNodes];return l.length===0?!0:!l.every(c=>ht(c))})}function ht(r){try{return!!n.ignoreNode(r)}catch{return!1}}function de(r,u=!1){let l=_t(r),c=r.filter(h=>(ge(h.target)||l(h.target)||u&&ze(h.target))&&!W(h)&&!ee(h.target,n.ignoreAttribute,h));return We(c,h=>ee(h,n.ignoreAttribute))}function yt(r){D=!!r,!D&&f.length>0&&ce()}function ce(){q(),y=setTimeout(Ie,n.idleWindowMs)}function Ie(){if(y=null,D){y=setTimeout(Ie,n.idleWindowMs);return}fe()}function q(){y!=null&&(clearTimeout(y),y=null)}function fe(){if(y=null,f.length===0)return;let r=f;f=[],ne(n.idleLabel,r)}function ne(r,u){let l=Qe(u,{isIgnored:re,ignoreAttribute:Ve,durableForm:n.durableForm});if(l.steps.length===0)return m("commit",r,"with nothing to record"),t.emit("commit"),null;let c={id:`${i}-${++s}`,label:r,timestamp:Date.now(),primitives:u,certificate:l};for(o.push(c),a.length=0,d=null;o.length>n.maxHistory;)o.shift();return m("commit",r,"primitives:",u.length),t.emit("commit"),c}function re(r){return ee(r,n.ignoreAttribute)}function Ve(r,u,l=null){if(typeof n.ignoreAttribute!="function")return!1;try{return!!n.ignoreAttribute(r,u,l)}catch{return!1}}function wt(r){if(typeof n.isProtected!="function")return!1;try{return!!n.isProtected(r)}catch{return!1}}function bt(r,u){if(!k)throw new Error("hyper-undo: scope not started");if(A==="commands")return xe(r,u,{honorPause:!0,receipt:!1});F(p.takeRecords()),z();let l=w===0?Pe():null,c=u();if(c&&typeof c.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(Ce(p.takeRecords(),!0),l!=null&&Re(l),f.length===0){t.emit("commit");return}q();let h=f;f=[],ne(r,h)}function kt(r){if(!k)throw new Error("hyper-undo: scope not started");if(!p)return;if(w>1){j(L(p.takeRecords()));return}z();let u=p.takeRecords(),l=de(u,!0);for(let c of le(L(u)))l.push(c);l.length!==0&&(q(),ne(r,l))}function vt(){p&&j(L(p.takeRecords()))}function pe(){let r=n.scope;return r&&r.ownerDocument||(typeof document<"u"?document:null)}function oe(){let r=pe();return!r||!r.head||typeof r.head.querySelector!="function"?null:r.head.querySelector("title")}function Pe(){let r=oe();return r?r.textContent:null}function Re(r){let u=oe();if(!u||r==null)return;let l=u.textContent;l!==r&&_e({kind:"value",target:u,prop:"textContent",oldValue:r,newValue:l})}function Et(r){if(!r)return null;let u=pe();if(!u||r.target!==u||r.prop!=="title")return r;let l=oe();return l?{kind:"value",target:l,prop:"textContent",oldValue:r.oldValue,newValue:r.newValue}:null}function At(){if(typeof window>"u")return!1;let r=window.clay?.effect||window.hyperclay?.effect;return!!(r&&r.running)}function St(){p&&F(p.takeRecords()),z(),S={records:[],primitives:[]}}function Nt(){let r=S;if(S=null,!r)return[];let u=de(r.records,!0);for(let l of le(L(r.records)))u.push(l);for(let l of r.primitives)u.push(l);return u}function xe(r,u,{honorPause:l,receipt:c}){if(typeof u!="function")throw new Error("hyper-undo: command() needs a synchronous function");if(S)throw new Error("hyper-undo: command() cannot nest inside another command");if(R)throw new Error("hyper-undo: command() cannot run during undo or redo");if(At())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 h=l&&w>0,v=h?null:Pe();St();let x=[],E=null,P;try{try{P=u()}catch($){E=$}if(p&&F(p.takeRecords()),P&&typeof P.then=="function"){try{P.then(void 0,()=>{})}catch{}E||(E=new Error("hyper-undo: command() fn must be synchronous; returned a Promise"))}h||Re(v)}finally{x=Nt()}let O=h||x.length===0?null:ne(r,x);if(E)throw E;return c?O?{id:O.id,label:O.label}:null:O}function Tt(r,u){if(!k)throw new Error("hyper-undo: scope not started");return xe(r,u,{honorPause:!1,receipt:!0})}function Ct(r){if(r!=="automatic"&&r!=="commands")throw new Error("hyper-undo: setRecording() takes 'automatic' or 'commands'");if(S)throw new Error("hyper-undo: setRecording() cannot change while a command is running");return A===r||(z(),A=r,m("recording",r)),A}function Oe(r){if(!k||!r||!S&&(w>0||A==="commands"))return;let u=Et(r);u&&_e(u)}function _e(r){if(S){S.primitives.push(r);return}f.push(r),ce()}function It(r,{prop:u="value",oldValue:l,newValue:c,read:h,write:v}={}){!r||l===c||Oe({kind:"value",target:r,prop:u,oldValue:l,newValue:c,read:h,write:v})}function z(){w===0&&p&&F(p.takeRecords()),f.length!==0&&(q(),fe())}function Le(){w===0&&p&&F(p.takeRecords()),w++,m("paused",w)}function Me(){w!==0&&(w--,w===0&&p&&(Te(p.takeRecords()),K&&(K=!1,q(),fe())),m("resumed",w))}function De(r,u){if(te())return Y(),{ok:!1,code:"disconnected"};if(typeof n.holdReplay=="function"&&n.holdReplay())return{ok:!1,code:"held"};z();let l=r==="undo"?o:a,c=r==="undo"?a:o,h=[],v=l.at(-1);if(!v)return{ok:!1,code:"empty"};if(u&&u!==v.id){let E=Be(u)||v;return me(E,r,"stale-entry")}let x=n.refused==="skip"&&!u;for(;v;){let E;R=!0;try{E=et(v.certificate,r,{isIgnored:re,ignoreAttribute:Ve,durableForm:n.durableForm,isProtected:wt,inScope:Lt,liveNodesWithIds:Mt})}finally{R=!1}if(!E.ok){if(!x)return me(v,r,E.code,E.targets);Ke(v,r,E.code,E.targets,h),v=l.at(-1);continue}Le();let P;R=!0;try{P=nt(E.operations)}finally{R=!1,Me()}if(!P.ok){if(m(`${r} failed and was rolled back`,P.error),!x)return me(v,r,"replay-error");Ke(v,r,"replay-error",[],h),v=l.at(-1);continue}j(new Set(E.operations.filter($=>$.type==="value"&&C.has($.target)).map($=>$.target))),l.pop(),c.push(v),d=null;let O={ok:!0,entryId:v.id,label:v.label,direction:r};return h.length&&(O.skipped=h),P.caret&&(O.caret=P.caret),m(r,v.label),t.emit(r,O),O}return{ok:!1,code:"empty",direction:r,skipped:h}}function Ke(r,u,l,c,h){d={entryId:r.id,direction:u,code:l,targets:c};let v=Fe(r.id);(u==="undo"?o:a).pop(),d=null;let E={entryId:r.id,label:r.label,direction:u,code:l,targets:c,description:v};m("skipped",u,l),t.emit("skipped",E),h.push(E)}function Vt(r){return De("undo",r&&r.entryId)}function Pt(r){return De("redo",r&&r.entryId)}function Be(r){return o.find(u=>u.id===r)||a.find(u=>u.id===r)||null}function me(r,u,l,c=[]){return d={entryId:r.id,direction:u,code:l,targets:c},m("refused",u,l),t.emit("refused",{entryId:r.id,label:r.label,direction:u,code:l,targets:c}),{ok:!1,code:l,entryId:r.id,label:r.label,direction:u}}function Rt(r){if(!d||d.entryId!==r)return{ok:!1,code:"not-refused",entryId:r};let u=d.direction,l=u==="undo"?o:a,c=l.at(-1);return!c||c.id!==r?{ok:!1,code:"stale-entry",entryId:r}:(l.pop(),d=null,m("discarded",c.label),t.emit("discard",{entryId:r,label:c.label,direction:u}),{ok:!0,entryId:r,direction:u})}function Fe(r){let u=Be(r);if(!u||!u.certificate||!u.certificate.replayable)return null;let l=d&&d.entryId===u.id?{direction:d.direction,code:d.code,targets:(d.targets||[]).map(c=>({id:c.id??null,kind:c.kind??null}))}:null;return{id:u.id,label:u.label,timestamp:u.timestamp,refused:l,steps:u.certificate.steps.map(Ot)}}function xt(r){return typeof r!="string"?null:r.replace(/#form:("(?:[^"\\]|\\.)*")/g,(u,l)=>{try{return JSON.parse(l)}catch{return""}})}function Ot(r){if(r.type==="move")return{kind:"move",node:{id:I(r.node).id},from:{id:I(r.sourceParent).id},to:{id:I(r.destParent).id}};if(r.type==="add"||r.type==="restore")return{kind:r.type==="add"?"add":"remove",parent:{id:I(r.parent).id},nodes:r.nodes.map((v,x)=>({id:I(v).id,html:xt(r.content[x])}))};let u=r.primitive,l={id:I(u.target).id},c=u.oldValue??null,h=u.newValue??null;return r.type==="value"?{kind:"value",target:l,prop:u.prop??"value",before:c,after:h}:r.type==="text"?{kind:"text",target:l,before:c,after:h}:r.type==="attr-set"||r.type==="attr-add"||r.type==="attr-remove"?{kind:"attribute",target:l,name:u.name??null,before:c,after:h}:{kind:r.type,target:l,before:c,after:h}}function _t(r){let u=new Set,l=!0;for(;l;){l=!1;for(let c of r)if(!(c.type!=="childList"||!c.removedNodes?.length)&&!W(c)&&!(!ge(c.target)&&!qe(c.target,u))&&!ee(c.target,n.ignoreAttribute,c))for(let h of c.removedNodes)u.has(h)||(u.add(h),l=!0)}return c=>!ge(c)&&qe(c,u)}function qe(r,u){for(let l=r;l;l=l.parentNode)if(u.has(l))return!0;return!1}function ge(r){let u=n.scope;return!u||!r||typeof r.nodeType!="number"?!1:r===u||typeof u.contains=="function"&&u.contains(r)}function ze(r){let u=pe(),l=u&&u.head;if(!l||!r)return!1;let c=r.nodeType===1?r:r.parentElement;for(;c&&c!==l;c=c.parentElement)if(c.localName==="title")return!1;return c===l}function Lt(r){let u=n.scope;return!r||!u?!1:r===u||oe()===r||ze(r)?!0:typeof u.contains=="function"?u.contains(r):!1}function Mt(r){let u=n.scope;if(!r||r.length===0||typeof u?.querySelectorAll!="function")return[];let l=new Set(r),c=[];for(let h of u.querySelectorAll("[data-id]"))l.has(h.getAttribute("data-id"))&&(re(h)||c.push(h));return c}function Dt(){p&&j(L(p.takeRecords())),o.length=0,a.length=0,f=[],d=null,q(),m("cleared"),t.emit("clear")}return{_config:n,_tracksContentAt:r=>A!=="commands"&&!!U(r),_recordsAt:r=>A!=="commands"&&!re(r),start:T,stop:Y,commit:bt,commitCaptured:kt,discardCaptured:vt,flush:z,command:Tt,setRecording:Ct,record:Oe,recordValue:It,trackContent:se,undo:Vt,redo:Pt,clear:Dt,discard:Rt,describe:Fe,pause:Le,resume:Me,on:t.on,off:t.off,get canUndo(){return o.length>0},get canRedo(){return a.length>0},get isPaused(){return w>0},get history(){return o.map(r=>{let u={id:r.id,label:r.label,timestamp:r.timestamp};return d&&d.entryId===r.id&&(u.refused={direction:d.direction,code:d.code,targets:d.targets}),u})},get future(){return a.map(r=>{let u={id:r.id,label:r.label,timestamp:r.timestamp};return d&&d.entryId===r.id&&(u.refused={direction:d.direction,code:d.code,targets:d.targets}),u}).reverse()},get scope(){return n.scope}}}var Cn=new Set(["checkbox","radio","button","submit","reset","image","file","range","color"]);function dt(e){let n=typeof e.composedPath=="function"?e.composedPath():[];return n.length>0?n:Vn(e.target)}function In(e){for(let n of dt(e)){if(!n||n.nodeType!==1)continue;let t=String(n.tagName||"").toUpperCase();if(t==="TEXTAREA"||t==="SELECT"||t==="INPUT"&&!Cn.has(String(n.type||n.getAttribute?.("type")||"text").toLowerCase()))return!0}return!1}function st(e){for(let n of dt(e)){if(!n||n.nodeType!==1)continue;if(n.isContentEditable===!0)return!0;let t=n.getAttribute&&n.getAttribute("contenteditable");if(t!=null&&String(t).toLowerCase()!=="false")return!0}return!1}function Ee(e,n){if(!e||!e.closest||!Array.isArray(n))return!1;for(let t of n)try{if(e.closest(t))return!0}catch{}return!1}function lt(e,n){if(In(e))return!0;let t=n._config,o=e.target;return t.editableKeys!=="page"?st(e)||Ee(o,t.shadowKeydownIn):Ee(o,t.ownKeysIn)||n._tracksContentAt(o)?!1:Ee(o,t.shadowKeydownIn)?!0:st(e)&&!n._recordsAt(o)}function Vn(e){let n=[];for(let t=e;t;t=t.parentNode||(t.host??null))n.push(t);return n}function ct(e,n){let t=i=>{let s=e.scope;if(s&&s.nodeType===1&&s.isConnected===!1){n&&n();return}if(i.defaultPrevented||i.isComposing||i.keyCode===229||lt(i,e)||!(i.metaKey||i.ctrlKey))return;let f=(i.key||"").toLowerCase();if(f==="z"&&!i.shiftKey){i.preventDefault(),i.stopPropagation(),e.undo();return}if(f==="z"&&i.shiftKey){i.preventDefault(),i.stopPropagation(),e.redo();return}if(f==="y"&&!i.shiftKey){i.preventDefault(),i.stopPropagation(),e.redo();return}},o=i=>{if(i.inputType!=="historyUndo"&&i.inputType!=="historyRedo")return;let s=e.scope;s&&s.nodeType===1&&s.isConnected===!1||i.defaultPrevented||i.isComposing||lt(i,e)||(i.preventDefault(),i.stopPropagation(),i.inputType==="historyRedo"?e.redo():e.undo())},a=e._config.editableKeys==="page";return window.addEventListener("keydown",t,!0),a&&window.addEventListener("beforeinput",o,!0),()=>{window.removeEventListener("keydown",t,!0),a&&window.removeEventListener("beforeinput",o,!0)}}var Ae=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror",".overtype-input","[data-richclay-active]",".ck-editor__editable","[data-lexical-editor]","[data-slate-editor]","trix-editor",".mce-content-body"],g=null,Se=null,ft=null,M=null;function pt(e,n){if(M){if(M.inst===e)return M.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=ct(e,n);return M={inst:e,cleanup:t},t}function mt(e){M&&M.inst===e&&(M.cleanup(),M=null)}var Ne={defaults:{shadowKeydownIn:Ae},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(g){if(Se===n)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),g;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||Ae};return g=ve(t),g.start(),Se=n,e.bindKeys!==!1&&(ft=pt(g,()=>Ne.stop())),g},stop(){g&&(mt(g),ft=null,g.stop(),g=null,Se=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||Ae},o=ve(t),a=null,i={_config:o._config,commit:o.commit,command:o.command,setRecording:o.setRecording,commitCaptured:o.commitCaptured,discardCaptured:o.discardCaptured,flush:o.flush,record:o.record,recordValue:o.recordValue,trackContent:o.trackContent,undo:o.undo,redo:o.redo,clear:o.clear,discard:o.discard,describe:o.describe,pause:o.pause,resume:o.resume,on:o.on,off:o.off,get canUndo(){return o.canUndo},get canRedo(){return o.canRedo},get isPaused(){return o.isPaused},get history(){return o.history},get future(){return o.future},get scope(){return o.scope},start(){e.bindKeys&&(a=pt(o,()=>i.stop())),o.start()},stop(){mt(o),a=null,o.stop()}};return i},commit(e,n){return g?g.commit(e,n):void 0},command(e,n){return g?g.command(e,n):void 0},setRecording(e){return g?g.setRecording(e):void 0},commitCaptured(e){return g?g.commitCaptured(e):void 0},discardCaptured(){return g?g.discardCaptured():void 0},record(e){return g?g.record(e):void 0},recordValue(e,n){return g?g.recordValue(e,n):void 0},trackContent(e,n){return g?g.trackContent(e,n):()=>{}},undo(e){return g?g.undo(e):void 0},redo(e){return g?g.redo(e):void 0},clear(){return g?g.clear():void 0},discard(e){return g?g.discard(e):void 0},describe(e){return g?g.describe(e):void 0},pause(){return g?g.pause():void 0},resume(){return g?g.resume():void 0},flush(){return g?g.flush():void 0},on(e,n){return g?g.on(e,n):void 0},off(e,n){return g?g.off(e,n):void 0},get canUndo(){return g?g.canUndo:!1},get canRedo(){return g?g.canRedo:!1},get history(){return g?g.history:[]},get future(){return g?g.future:[]},get isPaused(){return g?g.isPaused:!1}};var Pn=Ne;return $t(Rn);})();
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createScope } from './scope.js'
|
|
2
2
|
import { installKeys } from './keys.js'
|
|
3
|
+
import { endsSentence } from './typing.js'
|
|
3
4
|
|
|
4
5
|
const DEFAULT_SHADOW_LIST = [
|
|
5
6
|
'.CodeMirror',
|
|
@@ -21,6 +22,14 @@ const DEFAULT_SHADOW_LIST = [
|
|
|
21
22
|
// in richclay/src/richclay.js, which says it marks the region so hyper-undo
|
|
22
23
|
// "defers to Squire instead of fighting it".
|
|
23
24
|
'[data-richclay-active]',
|
|
25
|
+
// Embedded third-party editors keep their own history: the page stack never
|
|
26
|
+
// records what is typed in them, so a page-mode Cmd+Z there would revert some
|
|
27
|
+
// other field instead of the editor's own last change.
|
|
28
|
+
'.ck-editor__editable',
|
|
29
|
+
'[data-lexical-editor]',
|
|
30
|
+
'[data-slate-editor]',
|
|
31
|
+
'trix-editor',
|
|
32
|
+
'.mce-content-body',
|
|
24
33
|
]
|
|
25
34
|
|
|
26
35
|
let singleton = null // the Scope instance returned by createScope
|
|
@@ -113,6 +122,7 @@ const undo = {
|
|
|
113
122
|
flush: inst.flush,
|
|
114
123
|
record: inst.record,
|
|
115
124
|
recordValue: inst.recordValue,
|
|
125
|
+
trackContent: inst.trackContent,
|
|
116
126
|
undo: inst.undo,
|
|
117
127
|
redo: inst.redo,
|
|
118
128
|
clear: inst.clear,
|
|
@@ -152,6 +162,7 @@ const undo = {
|
|
|
152
162
|
discardCaptured() { return singleton ? singleton.discardCaptured() : undefined },
|
|
153
163
|
record(p) { return singleton ? singleton.record(p) : undefined },
|
|
154
164
|
recordValue(t, o) { return singleton ? singleton.recordValue(t, o) : undefined },
|
|
165
|
+
trackContent(root, o) { return singleton ? singleton.trackContent(root, o) : () => {} },
|
|
155
166
|
undo(opts) { return singleton ? singleton.undo(opts) : undefined },
|
|
156
167
|
redo(opts) { return singleton ? singleton.redo(opts) : undefined },
|
|
157
168
|
clear() { return singleton ? singleton.clear() : undefined },
|
|
@@ -169,5 +180,5 @@ const undo = {
|
|
|
169
180
|
get isPaused() { return singleton ? singleton.isPaused : false },
|
|
170
181
|
}
|
|
171
182
|
|
|
172
|
-
export { undo }
|
|
183
|
+
export { undo, endsSentence }
|
|
173
184
|
export default undo
|
package/src/keys.js
CHANGED
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
// subtree)? The browser's own undo belongs there, not the page stack's.
|
|
3
3
|
const NO_TEXT_INPUTS = new Set(['checkbox', 'radio', 'button', 'submit', 'reset', 'image', 'file', 'range', 'color'])
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
function pathOf(event) {
|
|
6
6
|
const path = typeof event.composedPath === 'function' ? event.composedPath() : []
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
return path.length > 0 ? path : chainFromTarget(event.target)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function inFormControl(event) {
|
|
11
|
+
for (const node of pathOf(event)) {
|
|
9
12
|
if (!node || node.nodeType !== 1) continue
|
|
10
13
|
const tag = String(node.tagName || '').toUpperCase()
|
|
11
14
|
if (tag === 'TEXTAREA' || tag === 'SELECT') return true
|
|
12
15
|
// An input that takes no typed text has no native undo of its own: a checkbox,
|
|
13
16
|
// a radio or a button leaves the key to the page stack.
|
|
14
17
|
if (tag === 'INPUT' && !NO_TEXT_INPUTS.has(String(node.type || node.getAttribute?.('type') || 'text').toLowerCase())) return true
|
|
18
|
+
}
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inEditable(event) {
|
|
23
|
+
for (const node of pathOf(event)) {
|
|
24
|
+
if (!node || node.nodeType !== 1) continue
|
|
15
25
|
if (node.isContentEditable === true) return true
|
|
16
26
|
// jsdom (and older engines) do not resolve isContentEditable, so read the
|
|
17
27
|
// declaration itself; an unset or explicit "false" is not editable.
|
|
@@ -21,6 +31,28 @@ function ownsNativeUndo(event) {
|
|
|
21
31
|
return false
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
function inAny(target, selectors) {
|
|
35
|
+
if (!target || !target.closest || !Array.isArray(selectors)) return false
|
|
36
|
+
for (const selector of selectors) {
|
|
37
|
+
try { if (target.closest(selector)) return true }
|
|
38
|
+
catch (_) { /* invalid selector, skip */ }
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Does this history key or history input belong to someone other than the page
|
|
44
|
+
// stack? "page" owns editable content the scope records; a form control, an
|
|
45
|
+
// authored no-undo region or another in-page editor keeps its own undo.
|
|
46
|
+
function standsDown(event, scope) {
|
|
47
|
+
if (inFormControl(event)) return true
|
|
48
|
+
const config = scope._config
|
|
49
|
+
const target = event.target
|
|
50
|
+
if (config.editableKeys !== 'page') return inEditable(event) || inAny(target, config.shadowKeydownIn)
|
|
51
|
+
if (inAny(target, config.ownKeysIn) || scope._tracksContentAt(target)) return false
|
|
52
|
+
if (inAny(target, config.shadowKeydownIn)) return true
|
|
53
|
+
return inEditable(event) && !scope._recordsAt(target)
|
|
54
|
+
}
|
|
55
|
+
|
|
24
56
|
function chainFromTarget(target) {
|
|
25
57
|
const out = []
|
|
26
58
|
for (let node = target; node; node = node.parentNode || (node.host ?? null)) out.push(node)
|
|
@@ -44,19 +76,7 @@ export function installKeys(scope, onDetached) {
|
|
|
44
76
|
if (event.defaultPrevented) return
|
|
45
77
|
// The IME owns keys while it composes.
|
|
46
78
|
if (event.isComposing || event.keyCode === 229) return
|
|
47
|
-
|
|
48
|
-
// control changes; Save/Enter later commits that text as a page command.
|
|
49
|
-
if (ownsNativeUndo(event)) return
|
|
50
|
-
|
|
51
|
-
// Bypass for in-page editors (CodeMirror, Monaco, etc.)
|
|
52
|
-
const target = event.target
|
|
53
|
-
const shadow = scope._config.shadowKeydownIn
|
|
54
|
-
if (target && target.closest && Array.isArray(shadow)) {
|
|
55
|
-
for (const selector of shadow) {
|
|
56
|
-
try { if (target.closest(selector)) return }
|
|
57
|
-
catch (_) { /* invalid selector, skip */ }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
79
|
+
if (standsDown(event, scope)) return
|
|
60
80
|
|
|
61
81
|
const mod = event.metaKey || event.ctrlKey
|
|
62
82
|
if (!mod) return
|
|
@@ -80,6 +100,24 @@ export function installKeys(scope, onDetached) {
|
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
|
|
103
|
+
// Chrome delivers the Edit menu's Undo as a cancellable beforeinput event.
|
|
104
|
+
// Owned by the same rule as the key, it moves the page stack instead.
|
|
105
|
+
const onHistoryInput = (event) => {
|
|
106
|
+
if (event.inputType !== 'historyUndo' && event.inputType !== 'historyRedo') return
|
|
107
|
+
const scopeEl = scope.scope
|
|
108
|
+
if (scopeEl && scopeEl.nodeType === 1 && scopeEl.isConnected === false) return
|
|
109
|
+
if (event.defaultPrevented || event.isComposing) return
|
|
110
|
+
if (standsDown(event, scope)) return
|
|
111
|
+
event.preventDefault(); event.stopPropagation()
|
|
112
|
+
if (event.inputType === 'historyRedo') scope.redo()
|
|
113
|
+
else scope.undo()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ownsInput = scope._config.editableKeys === 'page'
|
|
83
117
|
window.addEventListener('keydown', handler, true)
|
|
84
|
-
|
|
118
|
+
if (ownsInput) window.addEventListener('beforeinput', onHistoryInput, true)
|
|
119
|
+
return () => {
|
|
120
|
+
window.removeEventListener('keydown', handler, true)
|
|
121
|
+
if (ownsInput) window.removeEventListener('beforeinput', onHistoryInput, true)
|
|
122
|
+
}
|
|
85
123
|
}
|
package/src/primitives.js
CHANGED
|
@@ -124,7 +124,7 @@ export function recordsToPrimitives(records, ignoreNode = null) {
|
|
|
124
124
|
// more than once. Keep ONE primitive per (target, field) carrying the first old
|
|
125
125
|
// value and the final new value, and drop the ones whose endpoints are equal:
|
|
126
126
|
// undo must walk back to the value before the entry, not to an intermediate one.
|
|
127
|
-
export function normalizePrimitives(primitives) {
|
|
127
|
+
export function normalizePrimitives(primitives, { sameContent = (a, b) => a === b } = {}) {
|
|
128
128
|
const out = []
|
|
129
129
|
const slots = new Map() // target → Map(field → index in out)
|
|
130
130
|
|
|
@@ -148,6 +148,7 @@ export function normalizePrimitives(primitives) {
|
|
|
148
148
|
for (const p of out) {
|
|
149
149
|
if (scalarField(p) == null) { normalized.push(p); continue }
|
|
150
150
|
if (p.oldValue === p.newValue) continue
|
|
151
|
+
if (p.kind === 'value' && p.content === true && sameContent(p.oldValue, p.newValue)) continue
|
|
151
152
|
normalized.push(reclassifyScalar(p))
|
|
152
153
|
}
|
|
153
154
|
return normalized
|
package/src/replay-plan.js
CHANGED
|
@@ -53,9 +53,18 @@ function childIdentities(parent, isIgnored) {
|
|
|
53
53
|
// `host` is the element a removed subtree belongs under. A detached node has no
|
|
54
54
|
// ancestors left, so a predicate that reads them (is this attribute painted by an
|
|
55
55
|
// app above it?) receives the host and answers as it did while attached.
|
|
56
|
-
|
|
56
|
+
function ownForm(node, form, host) {
|
|
57
|
+
if (typeof form !== 'function' || !isNode(node) || node.nodeType !== 1) return null
|
|
58
|
+
let own = null
|
|
59
|
+
try { own = form(node, host) } catch (_) { own = null }
|
|
60
|
+
return typeof own === 'string' ? `#form:${JSON.stringify(own)}` : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function durableProjection(node, ignoredAttribute, host = null, ignoredNode = null, form = null, texts = null) {
|
|
57
64
|
if (!isNode(node)) return null
|
|
58
|
-
|
|
65
|
+
const own = ownForm(node, form, host)
|
|
66
|
+
if (own != null) return own
|
|
67
|
+
if (node.nodeType === 3) return `#text:${JSON.stringify(texts && texts.has(node) ? texts.get(node) : node.data)}`
|
|
59
68
|
if (node.nodeType === 8) return `#comment:${JSON.stringify(node.data)}`
|
|
60
69
|
if (node.nodeType !== 1) return `#node:${node.nodeType}`
|
|
61
70
|
const attributes = []
|
|
@@ -69,8 +78,10 @@ export function durableProjection(node, ignoredAttribute, host = null, ignoredNo
|
|
|
69
78
|
const children = []
|
|
70
79
|
if (node.childNodes) {
|
|
71
80
|
for (const child of node.childNodes) {
|
|
81
|
+
const ownChild = ownForm(child, form, host)
|
|
82
|
+
if (ownChild != null) { children.push(ownChild); continue }
|
|
72
83
|
if (isIgnoredNode(child, ignoredNode)) continue
|
|
73
|
-
children.push(durableProjection(child, ignoredAttribute, host, ignoredNode))
|
|
84
|
+
children.push(durableProjection(child, ignoredAttribute, host, ignoredNode, form, texts))
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
const tag = String(node.tagName || '').toLowerCase()
|
|
@@ -153,7 +164,8 @@ function slotIndex(list, primitive) {
|
|
|
153
164
|
export function compileCertificate(primitives, env = {}) {
|
|
154
165
|
const isIgnored = env.isIgnored || null
|
|
155
166
|
const ignoredAttribute = env.ignoreAttribute || null
|
|
156
|
-
const
|
|
167
|
+
const durableForm = env.durableForm || null
|
|
168
|
+
const normalized = normalizePrimitives(primitives, { sameContent })
|
|
157
169
|
|
|
158
170
|
const structural = []
|
|
159
171
|
for (const p of normalized) {
|
|
@@ -183,7 +195,7 @@ export function compileCertificate(primitives, env = {}) {
|
|
|
183
195
|
if (move.at === item) steps.push(move.step)
|
|
184
196
|
continue
|
|
185
197
|
}
|
|
186
|
-
steps.push(structuralStep(item, states.get(item), ignoredAttribute, isIgnored))
|
|
198
|
+
steps.push(structuralStep(item, states.get(item), ignoredAttribute, isIgnored, durableForm))
|
|
187
199
|
}
|
|
188
200
|
return { replayable: true, reason: null, steps }
|
|
189
201
|
}
|
|
@@ -276,7 +288,7 @@ function pairMoves(structural, states) {
|
|
|
276
288
|
return moves
|
|
277
289
|
}
|
|
278
290
|
|
|
279
|
-
function structuralStep(item, state, ignoredAttribute, ignoredNode) {
|
|
291
|
+
function structuralStep(item, state, ignoredAttribute, ignoredNode, durableForm = null) {
|
|
280
292
|
const kind = item.primitive.kind
|
|
281
293
|
return {
|
|
282
294
|
type: kind === 'add' ? 'add' : 'restore',
|
|
@@ -285,7 +297,7 @@ function structuralStep(item, state, ignoredAttribute, ignoredNode) {
|
|
|
285
297
|
seqBefore: state.before,
|
|
286
298
|
seqAfter: state.after,
|
|
287
299
|
slot: item.primitive,
|
|
288
|
-
content: item.nodes.map((node) => durableProjection(node, ignoredAttribute, item.parent, ignoredNode)),
|
|
300
|
+
content: item.nodes.map((node) => durableProjection(node, ignoredAttribute, item.parent, ignoredNode, durableForm)),
|
|
289
301
|
ids: kind === 'remove' ? item.nodes.flatMap((node) => durableIdsIn(node)) : [],
|
|
290
302
|
targets: targetsOf(item.nodes, kind === 'add' ? 'added' : 'removed', item.parent),
|
|
291
303
|
}
|
|
@@ -305,11 +317,13 @@ export function preflight(certificate, direction, env = {}) {
|
|
|
305
317
|
const context = {
|
|
306
318
|
isIgnored: env.isIgnored || null,
|
|
307
319
|
ignoreAttribute: env.ignoreAttribute || null,
|
|
320
|
+
durableForm: env.durableForm || null,
|
|
308
321
|
isProtected: env.isProtected || null,
|
|
309
322
|
inScope: env.inScope || (() => true),
|
|
310
323
|
liveNodesWithIds: env.liveNodesWithIds || (() => []),
|
|
311
324
|
sim: new Map(),
|
|
312
325
|
planned: new Map(),
|
|
326
|
+
texts: new Map(),
|
|
313
327
|
operations: [],
|
|
314
328
|
}
|
|
315
329
|
|
|
@@ -344,6 +358,12 @@ function refuseStep(code, targets) {
|
|
|
344
358
|
return { code, targets: targets || [] }
|
|
345
359
|
}
|
|
346
360
|
|
|
361
|
+
export function sameContent(a, b) {
|
|
362
|
+
if (a === b) return true
|
|
363
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false
|
|
364
|
+
return a.replace(/\u00A0| /g, ' ') === b.replace(/\u00A0| /g, ' ')
|
|
365
|
+
}
|
|
366
|
+
|
|
347
367
|
function liveValue(primitive) {
|
|
348
368
|
if (primitive.kind === 'text') return primitive.target.data
|
|
349
369
|
if (primitive.kind === 'value') {
|
|
@@ -369,11 +389,13 @@ function checkScalar(step, undoing, context) {
|
|
|
369
389
|
if (primitive.kind === 'value' && typeof primitive.write === 'function' && typeof primitive.read !== 'function') {
|
|
370
390
|
return refuseStep('unsupported', step.targets)
|
|
371
391
|
}
|
|
372
|
-
|
|
392
|
+
const live = liveValue(primitive)
|
|
393
|
+
if (primitive.content === true ? !sameContent(live, expected) : live !== expected) return refuseStep('content-changed', step.targets)
|
|
373
394
|
if (primitive.kind === 'value') {
|
|
374
|
-
context.operations.push({ type: 'value', target: primitive.target, prop: primitive.prop || 'value', value: next, read: primitive.read || null, write: primitive.write || null })
|
|
395
|
+
context.operations.push({ type: 'value', target: primitive.target, prop: primitive.prop || 'value', value: next, read: primitive.read || null, write: primitive.write || null, content: primitive.content === true })
|
|
375
396
|
} else if (primitive.kind === 'text') {
|
|
376
397
|
context.operations.push({ type: 'text', target: primitive.target, data: next })
|
|
398
|
+
context.texts.set(primitive.target, next)
|
|
377
399
|
} else {
|
|
378
400
|
context.operations.push({ type: 'attr', target: primitive.target, name: primitive.name, value: next == null ? null : next })
|
|
379
401
|
}
|
|
@@ -387,7 +409,7 @@ function checkAdd(step, undoing, context) {
|
|
|
387
409
|
const protectedTargets = protectedWithin(step.nodes, context.isProtected)
|
|
388
410
|
if (protectedTargets.length > 0) return refuseStep('protected', protectedTargets)
|
|
389
411
|
for (let i = 0; i < step.nodes.length; i++) {
|
|
390
|
-
if (durableProjection(step.nodes[i], context.ignoreAttribute, step.parent, context.isIgnored) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
412
|
+
if (durableProjection(step.nodes[i], context.ignoreAttribute, step.parent, context.isIgnored, context.durableForm) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
391
413
|
}
|
|
392
414
|
for (const node of step.nodes) {
|
|
393
415
|
context.operations.push({ type: 'remove', node })
|
|
@@ -422,7 +444,7 @@ function checkRestore(step, undoing, context) {
|
|
|
422
444
|
if (!sameSequence(listFor(context, step.parent), step.seqAfter)) return refuseStep('structure-changed', step.targets)
|
|
423
445
|
for (let i = 0; i < step.nodes.length; i++) {
|
|
424
446
|
const node = step.nodes[i]
|
|
425
|
-
if (node.parentNode || durableProjection(node, context.ignoreAttribute, step.parent, context.isIgnored) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
447
|
+
if (node.parentNode || durableProjection(node, context.ignoreAttribute, step.parent, context.isIgnored, context.durableForm) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
426
448
|
}
|
|
427
449
|
if (step.ids.length > 0 && liveDuplicates(step.ids, context)) return refuseStep('duplicate-id', step.targets)
|
|
428
450
|
const at = slotIndex(step.seqAfter, step.slot)
|
|
@@ -445,9 +467,11 @@ function checkRestore(step, undoing, context) {
|
|
|
445
467
|
// column before its column is removed) is leaving, so the removed subtree is
|
|
446
468
|
// compared without it, as it was when it was recorded.
|
|
447
469
|
const leaving = (node) => context.planned.get(node) === true || (typeof context.isIgnored === 'function' && context.isIgnored(node))
|
|
470
|
+
// Text an earlier step of this replay rewrites (a field emptied before its text node
|
|
471
|
+
// is taken out) is compared as that step leaves it, as it was when it was recorded.
|
|
448
472
|
for (let i = 0; i < step.nodes.length; i++) {
|
|
449
473
|
const node = step.nodes[i]
|
|
450
|
-
if (node.parentNode !== step.parent || durableProjection(node, context.ignoreAttribute, step.parent, leaving) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
474
|
+
if (node.parentNode !== step.parent || durableProjection(node, context.ignoreAttribute, step.parent, leaving, context.durableForm, context.texts) !== step.content[i]) return refuseStep('content-changed', step.targets)
|
|
451
475
|
}
|
|
452
476
|
for (const node of step.nodes) {
|
|
453
477
|
context.operations.push({ type: 'remove', node })
|
|
@@ -505,6 +529,15 @@ function findChildByIdentity(parent, id) {
|
|
|
505
529
|
return null
|
|
506
530
|
}
|
|
507
531
|
|
|
532
|
+
function changedEnd(before, after) {
|
|
533
|
+
const max = Math.min(before.length, after.length)
|
|
534
|
+
let prefix = 0
|
|
535
|
+
while (prefix < max && before[prefix] === after[prefix]) prefix++
|
|
536
|
+
let suffix = 0
|
|
537
|
+
while (suffix < max - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++
|
|
538
|
+
return after.length - suffix
|
|
539
|
+
}
|
|
540
|
+
|
|
508
541
|
function writeValue(op, value) {
|
|
509
542
|
if (typeof op.write === 'function') op.write(op.target, value)
|
|
510
543
|
else op.target[op.prop] = value
|
|
@@ -513,8 +546,9 @@ function writeValue(op, value) {
|
|
|
513
546
|
export function applyOperations(operations) {
|
|
514
547
|
const journal = []
|
|
515
548
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
549
|
+
let caret = null
|
|
550
|
+
for (const op of operations) caret = applyOne(op, journal) || caret
|
|
551
|
+
return { ok: true, caret }
|
|
518
552
|
} catch (error) {
|
|
519
553
|
for (let i = journal.length - 1; i >= 0; i--) {
|
|
520
554
|
try { journal[i]() } catch (_) { /* the rollback keeps trying the rest */ }
|
|
@@ -538,13 +572,16 @@ function applyOne(op, journal) {
|
|
|
538
572
|
const previous = op.target.data
|
|
539
573
|
op.target.data = op.data
|
|
540
574
|
journal.push(() => { op.target.data = previous })
|
|
541
|
-
return
|
|
575
|
+
return { node: op.target, offset: changedEnd(previous, op.data) }
|
|
542
576
|
}
|
|
543
577
|
if (op.type === 'value') {
|
|
544
578
|
const previous = typeof op.read === 'function' ? op.read(op.target) : op.target[op.prop]
|
|
579
|
+
const textBefore = op.content ? op.target.textContent : null
|
|
545
580
|
writeValue(op, op.value)
|
|
546
581
|
journal.push(() => writeValue(op, previous))
|
|
547
|
-
return
|
|
582
|
+
if (!op.content) return
|
|
583
|
+
const textAfter = op.target.textContent
|
|
584
|
+
return textAfter === textBefore ? undefined : { root: op.target, textOffset: changedEnd(textBefore, textAfter) }
|
|
548
585
|
}
|
|
549
586
|
if (op.type === 'insert') {
|
|
550
587
|
const before = op.beforeId == null ? null : findChildByIdentity(op.parent, op.beforeId)
|
|
@@ -556,13 +593,17 @@ function applyOne(op, journal) {
|
|
|
556
593
|
if (fromParent) fromParent.insertBefore(op.node, fromNext && fromNext.parentNode === fromParent ? fromNext : null)
|
|
557
594
|
else if (op.node.parentNode === op.parent) op.parent.removeChild(op.node)
|
|
558
595
|
})
|
|
559
|
-
return
|
|
596
|
+
if (op.node.nodeType !== 3) return
|
|
597
|
+
return { node: op.node, offset: op.node.data.length }
|
|
560
598
|
}
|
|
561
599
|
if (op.type === 'remove') {
|
|
562
600
|
const parent = op.node.parentNode
|
|
563
601
|
if (!parent) throw new Error('hyper-undo: replay target is no longer attached')
|
|
564
602
|
const next = op.node.nextSibling
|
|
603
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, op.node)
|
|
565
604
|
parent.removeChild(op.node)
|
|
566
605
|
journal.push(() => parent.insertBefore(op.node, next))
|
|
606
|
+
if (op.node.nodeType !== 3) return
|
|
607
|
+
return { node: parent, offset: index }
|
|
567
608
|
}
|
|
568
609
|
}
|
package/src/scope.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { recordsToPrimitives } from './primitives.js'
|
|
2
|
-
import { compileCertificate, preflight, applyOperations, described } from './replay-plan.js'
|
|
2
|
+
import { compileCertificate, preflight, applyOperations, described, sameContent } from './replay-plan.js'
|
|
3
3
|
import { shouldIgnore } from './filter.js'
|
|
4
4
|
import { createEmitter } from './emitter.js'
|
|
5
|
+
import { watchTyping } from './typing.js'
|
|
5
6
|
|
|
6
7
|
const DEFAULTS = {
|
|
7
8
|
maxHistory: 100,
|
|
@@ -11,6 +12,9 @@ const DEFAULTS = {
|
|
|
11
12
|
ignoreAttribute: null,
|
|
12
13
|
isProtected: null,
|
|
13
14
|
ignoreNode: null,
|
|
15
|
+
durableForm: null,
|
|
16
|
+
typingBreak: null,
|
|
17
|
+
refused: null,
|
|
14
18
|
debug: false,
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -32,6 +36,10 @@ export function createScope(opts) {
|
|
|
32
36
|
let recordingMode = config.recording === 'commands' ? 'commands' : 'automatic'
|
|
33
37
|
let capture = null // { records, primitives } while an explicit command runs
|
|
34
38
|
let replaying = false // true only inside an undo/redo preflight and apply
|
|
39
|
+
let tracked = new WeakMap() // root element -> { prop, read, write, baseline }
|
|
40
|
+
let composing = false // an input method composition is open
|
|
41
|
+
let movedUnderStep = false // a paused writer changed what the open step changed
|
|
42
|
+
let typingCleanup = null // watchTyping's cleanup, kept for stop()
|
|
35
43
|
|
|
36
44
|
function log(...args) { if (config.debug) console.log('[hyper-undo]', ...args) }
|
|
37
45
|
|
|
@@ -49,6 +57,15 @@ export function createScope(opts) {
|
|
|
49
57
|
attributeOldValue: true,
|
|
50
58
|
characterDataOldValue: true,
|
|
51
59
|
})
|
|
60
|
+
typingCleanup = watchTyping(config.scope, {
|
|
61
|
+
sentence: config.typingBreak === 'sentence',
|
|
62
|
+
stepOpen: () => {
|
|
63
|
+
if (pauseDepth === 0 && observer) handleRecords(observer.takeRecords())
|
|
64
|
+
return pendingPrimitives.length > 0
|
|
65
|
+
},
|
|
66
|
+
endStep: flush,
|
|
67
|
+
composing: setComposing,
|
|
68
|
+
})
|
|
52
69
|
log('started')
|
|
53
70
|
}
|
|
54
71
|
|
|
@@ -74,6 +91,8 @@ export function createScope(opts) {
|
|
|
74
91
|
if (!started) return
|
|
75
92
|
started = false
|
|
76
93
|
if (observer) { observer.disconnect(); observer = null }
|
|
94
|
+
if (typingCleanup) { typingCleanup(); typingCleanup = null }
|
|
95
|
+
composing = false
|
|
77
96
|
clearIdleTimer()
|
|
78
97
|
undoStack.length = 0
|
|
79
98
|
redoStack.length = 0
|
|
@@ -82,6 +101,7 @@ export function createScope(opts) {
|
|
|
82
101
|
capture = null
|
|
83
102
|
replaying = false
|
|
84
103
|
pauseDepth = 0 // reset so a reused (created) scope doesn't restart paused
|
|
104
|
+
tracked = new WeakMap()
|
|
85
105
|
log('stopped')
|
|
86
106
|
}
|
|
87
107
|
|
|
@@ -93,6 +113,70 @@ export function createScope(opts) {
|
|
|
93
113
|
return !!(el && el.nodeType === 1 && el.isConnected === false)
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
// --- Tracked content ---
|
|
117
|
+
|
|
118
|
+
function readContent(root, read) {
|
|
119
|
+
try { const value = read(root); return typeof value === 'string' ? value : null } catch (_) { return null }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function trackContent(root, { read, write, prop = 'content' } = {}) {
|
|
123
|
+
if (!root || typeof read !== 'function' || typeof write !== 'function') return () => {}
|
|
124
|
+
const state = { prop, read, write, baseline: readContent(root, read) }
|
|
125
|
+
tracked.set(root, state)
|
|
126
|
+
return () => { if (tracked.get(root) === state) tracked.delete(root) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function trackedRootOf(node) {
|
|
130
|
+
for (let n = node; n; n = n.parentNode) if (tracked.has(n)) return n
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isTrackedContent(record) {
|
|
135
|
+
const root = trackedRootOf(record.target)
|
|
136
|
+
return !!root && !(record.type === 'attributes' && record.target === root)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function contentRootsIn(records) {
|
|
140
|
+
const roots = new Set()
|
|
141
|
+
for (const record of records) if (isTrackedContent(record)) roots.add(trackedRootOf(record.target))
|
|
142
|
+
return roots
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sampleContent(roots) {
|
|
146
|
+
const out = []
|
|
147
|
+
for (const root of roots) {
|
|
148
|
+
const state = tracked.get(root)
|
|
149
|
+
const now = readContent(root, state.read)
|
|
150
|
+
if (now == null) continue
|
|
151
|
+
if (sameContent(now, state.baseline)) { state.baseline = now; continue }
|
|
152
|
+
if (state.baseline != null) out.push({ kind: 'value', target: root, prop: state.prop, oldValue: state.baseline, newValue: now, read: state.read, write: state.write, content: true })
|
|
153
|
+
state.baseline = now
|
|
154
|
+
}
|
|
155
|
+
return out
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rebaseContent(roots) {
|
|
159
|
+
for (const root of roots) {
|
|
160
|
+
const state = tracked.get(root)
|
|
161
|
+
state.baseline = readContent(root, state.read)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Records another writer made while this scope was paused are dropped. When one of
|
|
166
|
+
// them changed a text node, an attribute or a tracked field the open step has already
|
|
167
|
+
// changed, the step has to close before this tab types again: joined with that typing
|
|
168
|
+
// it would run from before the other writer's change to after it, and its undo would
|
|
169
|
+
// take their change back.
|
|
170
|
+
function dropPaused(records) {
|
|
171
|
+
const roots = contentRootsIn(records)
|
|
172
|
+
if (!movedUnderStep && pendingPrimitives.some((p) => p.kind === 'value'
|
|
173
|
+
? roots.has(p.target)
|
|
174
|
+
: records.some((r) => r.target === p.target && (r.type === 'characterData' ? p.kind === 'text' : r.type === 'attributes' && r.attributeName === p.name)))) {
|
|
175
|
+
movedUnderStep = true
|
|
176
|
+
}
|
|
177
|
+
rebaseContent(roots)
|
|
178
|
+
}
|
|
179
|
+
|
|
96
180
|
// --- Observer callback ---
|
|
97
181
|
|
|
98
182
|
// The record source's callback. It hands over its own instance as a second
|
|
@@ -115,9 +199,10 @@ export function createScope(opts) {
|
|
|
115
199
|
// Commands mode: an unbracketed record is some other writer's (a merge, a
|
|
116
200
|
// sap paint, a sortable gesture). It changes the document, so later guards
|
|
117
201
|
// see it, but it is not a local action and earns no entry.
|
|
118
|
-
if (recordingMode === 'commands') return
|
|
119
|
-
if (pauseDepth > 0) return
|
|
202
|
+
if (recordingMode === 'commands') { rebaseContent(contentRootsIn(records)); return }
|
|
203
|
+
if (pauseDepth > 0) { dropPaused(records); return }
|
|
120
204
|
const prims = convertRecords(dropIgnoredRecords(records), bracketed)
|
|
205
|
+
for (const p of sampleContent(contentRootsIn(records))) prims.push(p)
|
|
121
206
|
if (prims.length > 0) {
|
|
122
207
|
for (const p of prims) pendingPrimitives.push(p)
|
|
123
208
|
restartIdleTimer()
|
|
@@ -155,15 +240,27 @@ export function createScope(opts) {
|
|
|
155
240
|
// fields too; head's <title> stays with the bracket's own title capture.
|
|
156
241
|
function convertRecords(records, bracketed = false) {
|
|
157
242
|
const removedHere = removedFromScope(records)
|
|
158
|
-
const kept = records.filter((r) => (observedHere(r.target) || removedHere(r.target) || (bracketed && inHead(r.target))) && !shouldIgnore(r.target, config.ignoreAttribute, r))
|
|
243
|
+
const kept = records.filter((r) => (observedHere(r.target) || removedHere(r.target) || (bracketed && inHead(r.target))) && !isTrackedContent(r) && !shouldIgnore(r.target, config.ignoreAttribute, r))
|
|
159
244
|
return recordsToPrimitives(kept, (node) => shouldIgnore(node, config.ignoreAttribute))
|
|
160
245
|
}
|
|
161
246
|
|
|
162
247
|
// --- Idle batching ---
|
|
163
248
|
|
|
249
|
+
function setComposing(on) {
|
|
250
|
+
composing = !!on
|
|
251
|
+
if (!composing && pendingPrimitives.length > 0) restartIdleTimer()
|
|
252
|
+
}
|
|
253
|
+
|
|
164
254
|
function restartIdleTimer() {
|
|
165
255
|
clearIdleTimer()
|
|
166
|
-
idleTimer = setTimeout(
|
|
256
|
+
idleTimer = setTimeout(onIdle, config.idleWindowMs)
|
|
257
|
+
}
|
|
258
|
+
// A step never closes while a composition is open: the timer waits it out and
|
|
259
|
+
// the composed text lands in one entry.
|
|
260
|
+
function onIdle() {
|
|
261
|
+
idleTimer = null
|
|
262
|
+
if (composing) { idleTimer = setTimeout(onIdle, config.idleWindowMs); return }
|
|
263
|
+
closeIdleBatch()
|
|
167
264
|
}
|
|
168
265
|
function clearIdleTimer() {
|
|
169
266
|
if (idleTimer != null) { clearTimeout(idleTimer); idleTimer = null }
|
|
@@ -179,7 +276,7 @@ export function createScope(opts) {
|
|
|
179
276
|
// --- Commit machinery ---
|
|
180
277
|
|
|
181
278
|
function pushCommit(label, primitives) {
|
|
182
|
-
const certificate = compileCertificate(primitives, { isIgnored: isIgnoredNode, ignoreAttribute: isIgnoredAttribute })
|
|
279
|
+
const certificate = compileCertificate(primitives, { isIgnored: isIgnoredNode, ignoreAttribute: isIgnoredAttribute, durableForm: config.durableForm })
|
|
183
280
|
if (certificate.steps.length === 0) {
|
|
184
281
|
log('commit', label, 'with nothing to record')
|
|
185
282
|
emitter.emit('commit')
|
|
@@ -254,20 +351,22 @@ export function createScope(opts) {
|
|
|
254
351
|
// If an OUTER pause is still active (this capture frame is nested inside an
|
|
255
352
|
// exclusion pause), honor the outer "exclude everything" contract: discard
|
|
256
353
|
// the captured records instead of committing through the outer pause.
|
|
257
|
-
if (pauseDepth > 1) { observer.takeRecords(); return }
|
|
354
|
+
if (pauseDepth > 1) { rebaseContent(contentRootsIn(observer.takeRecords())); return }
|
|
258
355
|
// Close any pending idle batch (e.g. in-progress typing captured BEFORE the
|
|
259
356
|
// pause) as its own commit FIRST, so the structural commit we're about to
|
|
260
357
|
// push lands AFTER it in history. Without this, type-then-immediately-add
|
|
261
358
|
// would record the structural op before the typing, inverting undo order.
|
|
262
359
|
flush()
|
|
263
|
-
const
|
|
360
|
+
const records = observer.takeRecords()
|
|
361
|
+
const primitives = convertRecords(records, true)
|
|
362
|
+
for (const p of sampleContent(contentRootsIn(records))) primitives.push(p)
|
|
264
363
|
if (primitives.length === 0) return
|
|
265
364
|
clearIdleTimer()
|
|
266
365
|
pushCommit(label, primitives)
|
|
267
366
|
}
|
|
268
367
|
|
|
269
368
|
function discardCaptured() {
|
|
270
|
-
if (observer) observer.takeRecords()
|
|
369
|
+
if (observer) rebaseContent(contentRootsIn(observer.takeRecords()))
|
|
271
370
|
}
|
|
272
371
|
|
|
273
372
|
// --- Explicit commands ---
|
|
@@ -333,6 +432,7 @@ export function createScope(opts) {
|
|
|
333
432
|
capture = null
|
|
334
433
|
if (!held) return []
|
|
335
434
|
const primitives = convertRecords(held.records, true)
|
|
435
|
+
for (const p of sampleContent(contentRootsIn(held.records))) primitives.push(p)
|
|
336
436
|
for (const p of held.primitives) primitives.push(p)
|
|
337
437
|
return primitives
|
|
338
438
|
}
|
|
@@ -465,7 +565,14 @@ export function createScope(opts) {
|
|
|
465
565
|
if (pauseDepth === 0) return // underflow guard
|
|
466
566
|
pauseDepth--
|
|
467
567
|
// Discard records captured during the pause only on the outermost release.
|
|
468
|
-
if (pauseDepth === 0 && observer)
|
|
568
|
+
if (pauseDepth === 0 && observer) {
|
|
569
|
+
dropPaused(observer.takeRecords())
|
|
570
|
+
if (movedUnderStep) {
|
|
571
|
+
movedUnderStep = false
|
|
572
|
+
clearIdleTimer()
|
|
573
|
+
closeIdleBatch()
|
|
574
|
+
}
|
|
575
|
+
}
|
|
469
576
|
log('resumed', pauseDepth)
|
|
470
577
|
}
|
|
471
578
|
|
|
@@ -474,51 +581,88 @@ export function createScope(opts) {
|
|
|
474
581
|
// One entry is applied whole or not at all. Preflight compares the certificate
|
|
475
582
|
// against the live document without writing; only a fully validated plan
|
|
476
583
|
// reaches the DOM, and a native failure rolls it back and keeps both stacks.
|
|
584
|
+
// With `refused: 'skip'` a refused entry is dropped and the call goes on to the
|
|
585
|
+
// next one, so a step that can no longer be replayed never blocks older ones.
|
|
477
586
|
function replay(direction, requestedId) {
|
|
478
587
|
if (scopeDisconnected()) { stop(); return { ok: false, code: 'disconnected' } }
|
|
588
|
+
// A consumer can hold replay (an open gesture, a pending save) without
|
|
589
|
+
// refusing the entry: the stacks and the document stay as they are.
|
|
590
|
+
if (typeof config.holdReplay === 'function' && config.holdReplay()) return { ok: false, code: 'held' }
|
|
479
591
|
flush()
|
|
480
592
|
const source = direction === 'undo' ? undoStack : redoStack
|
|
481
593
|
const destination = direction === 'undo' ? redoStack : undoStack
|
|
482
|
-
const
|
|
594
|
+
const skipped = []
|
|
595
|
+
let entry = source.at(-1)
|
|
483
596
|
if (!entry) return { ok: false, code: 'empty' }
|
|
484
597
|
if (requestedId && requestedId !== entry.id) {
|
|
485
598
|
const requested = findEntry(requestedId) || entry
|
|
486
599
|
return refuse(requested, direction, 'stale-entry')
|
|
487
600
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
601
|
+
const canSkip = config.refused === 'skip' && !requestedId
|
|
602
|
+
while (entry) {
|
|
603
|
+
let checked
|
|
604
|
+
replaying = true
|
|
605
|
+
try {
|
|
606
|
+
checked = preflight(entry.certificate, direction, {
|
|
607
|
+
isIgnored: isIgnoredNode,
|
|
608
|
+
ignoreAttribute: isIgnoredAttribute,
|
|
609
|
+
durableForm: config.durableForm,
|
|
610
|
+
isProtected: isProtectedNode,
|
|
611
|
+
inScope,
|
|
612
|
+
liveNodesWithIds,
|
|
613
|
+
})
|
|
614
|
+
} finally {
|
|
615
|
+
replaying = false
|
|
616
|
+
}
|
|
617
|
+
if (!checked.ok) {
|
|
618
|
+
if (!canSkip) return refuse(entry, direction, checked.code, checked.targets)
|
|
619
|
+
skipEntry(entry, direction, checked.code, checked.targets, skipped)
|
|
620
|
+
entry = source.at(-1)
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
pause()
|
|
624
|
+
let applied
|
|
625
|
+
replaying = true
|
|
626
|
+
try {
|
|
627
|
+
applied = applyOperations(checked.operations)
|
|
628
|
+
} finally {
|
|
629
|
+
replaying = false
|
|
630
|
+
resume()
|
|
631
|
+
}
|
|
632
|
+
if (!applied.ok) {
|
|
633
|
+
log(`${direction} failed and was rolled back`, applied.error)
|
|
634
|
+
if (!canSkip) return refuse(entry, direction, 'replay-error')
|
|
635
|
+
skipEntry(entry, direction, 'replay-error', [], skipped)
|
|
636
|
+
entry = source.at(-1)
|
|
637
|
+
continue
|
|
638
|
+
}
|
|
639
|
+
rebaseContent(new Set(checked.operations.filter((op) => op.type === 'value' && tracked.has(op.target)).map((op) => op.target)))
|
|
640
|
+
source.pop()
|
|
641
|
+
destination.push(entry)
|
|
642
|
+
refusedTop = null
|
|
643
|
+
const receipt = { ok: true, entryId: entry.id, label: entry.label, direction }
|
|
644
|
+
if (skipped.length) receipt.skipped = skipped
|
|
645
|
+
if (applied.caret) receipt.caret = applied.caret
|
|
646
|
+
log(direction, entry.label)
|
|
647
|
+
emitter.emit(direction, receipt)
|
|
648
|
+
return receipt
|
|
514
649
|
}
|
|
515
|
-
|
|
516
|
-
|
|
650
|
+
return { ok: false, code: 'empty', direction, skipped }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Drop a refused entry in the same press. Its description is built while it is
|
|
654
|
+
// still on its stack (describe() reads refusedTop), so a consumer can keep a
|
|
655
|
+
// recovery copy of what the removed entry would have restored.
|
|
656
|
+
function skipEntry(entry, direction, code, targets, skipped) {
|
|
657
|
+
refusedTop = { entryId: entry.id, direction, code, targets }
|
|
658
|
+
const description = describe(entry.id)
|
|
659
|
+
const stack = direction === 'undo' ? undoStack : redoStack
|
|
660
|
+
stack.pop()
|
|
517
661
|
refusedTop = null
|
|
518
|
-
const
|
|
519
|
-
log(direction,
|
|
520
|
-
emitter.emit(
|
|
521
|
-
|
|
662
|
+
const record = { entryId: entry.id, label: entry.label, direction, code, targets, description }
|
|
663
|
+
log('skipped', direction, code)
|
|
664
|
+
emitter.emit('skipped', record)
|
|
665
|
+
skipped.push(record)
|
|
522
666
|
}
|
|
523
667
|
|
|
524
668
|
function undo(opts) { return replay('undo', opts && opts.entryId) }
|
|
@@ -573,6 +717,15 @@ export function createScope(opts) {
|
|
|
573
717
|
}
|
|
574
718
|
}
|
|
575
719
|
|
|
720
|
+
// A host's durable form is stored as #form:"<json string>"; a description shows the
|
|
721
|
+
// string itself, not the token.
|
|
722
|
+
function readableForm(content) {
|
|
723
|
+
if (typeof content !== 'string') return null
|
|
724
|
+
return content.replace(/#form:("(?:[^"\\]|\\.)*")/g, (_, json) => {
|
|
725
|
+
try { return JSON.parse(json) } catch (_) { return '' }
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
|
|
576
729
|
function describeStep(step) {
|
|
577
730
|
if (step.type === 'move') {
|
|
578
731
|
return {
|
|
@@ -588,7 +741,7 @@ export function createScope(opts) {
|
|
|
588
741
|
return {
|
|
589
742
|
kind: step.type === 'add' ? 'add' : 'remove',
|
|
590
743
|
parent: { id: described(step.parent).id },
|
|
591
|
-
nodes: step.nodes.map((node, i) => ({ id: described(node).id, html: step.content[i]
|
|
744
|
+
nodes: step.nodes.map((node, i) => ({ id: described(node).id, html: readableForm(step.content[i]) })),
|
|
592
745
|
}
|
|
593
746
|
}
|
|
594
747
|
const primitive = step.primitive
|
|
@@ -616,7 +769,9 @@ export function createScope(opts) {
|
|
|
616
769
|
grew = false
|
|
617
770
|
for (const r of records) {
|
|
618
771
|
if (r.type !== 'childList' || !r.removedNodes?.length) continue
|
|
772
|
+
if (isTrackedContent(r)) continue
|
|
619
773
|
if (!observedHere(r.target) && !insideRemoved(r.target, removed)) continue
|
|
774
|
+
if (shouldIgnore(r.target, config.ignoreAttribute, r)) continue
|
|
620
775
|
for (const node of r.removedNodes) if (!removed.has(node)) { removed.add(node); grew = true }
|
|
621
776
|
}
|
|
622
777
|
}
|
|
@@ -630,7 +785,7 @@ export function createScope(opts) {
|
|
|
630
785
|
|
|
631
786
|
function observedHere(node) {
|
|
632
787
|
const root = config.scope
|
|
633
|
-
if (!root || !node) return false
|
|
788
|
+
if (!root || !node || typeof node.nodeType !== 'number') return false
|
|
634
789
|
return node === root || (typeof root.contains === 'function' && root.contains(node))
|
|
635
790
|
}
|
|
636
791
|
|
|
@@ -677,7 +832,7 @@ export function createScope(opts) {
|
|
|
677
832
|
// Discard any same-tick buffered records too, so a bootstrap mutation
|
|
678
833
|
// followed by clear() in the same turn doesn't resurface as a commit once
|
|
679
834
|
// the observer callback fires.
|
|
680
|
-
if (observer) observer.takeRecords()
|
|
835
|
+
if (observer) rebaseContent(contentRootsIn(observer.takeRecords()))
|
|
681
836
|
undoStack.length = 0
|
|
682
837
|
redoStack.length = 0
|
|
683
838
|
pendingPrimitives = []
|
|
@@ -691,10 +846,12 @@ export function createScope(opts) {
|
|
|
691
846
|
|
|
692
847
|
return {
|
|
693
848
|
_config: config, // private: used by keys.js to read shadowKeydownIn
|
|
849
|
+
_tracksContentAt: (node) => recordingMode !== 'commands' && !!trackedRootOf(node), // private: keys.js rule 4
|
|
850
|
+
_recordsAt: (node) => recordingMode !== 'commands' && !isIgnoredNode(node), // private: keys.js rule 4
|
|
694
851
|
start, stop,
|
|
695
852
|
commit, commitCaptured, discardCaptured, flush,
|
|
696
853
|
command, setRecording,
|
|
697
|
-
record, recordValue,
|
|
854
|
+
record, recordValue, trackContent,
|
|
698
855
|
undo, redo, clear, discard, describe,
|
|
699
856
|
pause, resume,
|
|
700
857
|
on: emitter.on, off: emitter.off,
|
package/src/typing.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Every listener sits on the scope root, in the capture phase: typing only ever happens
|
|
2
|
+
// inside it, and the window keeps only the history key bindings.
|
|
3
|
+
// A sentence end, a field switch, a change of intent, a command, a replaced
|
|
4
|
+
// selection and a caret jump each close the open typing step, so one undo press
|
|
5
|
+
// undoes one sentence of typing. The idle timer never closes a step while an
|
|
6
|
+
// input method composition is open.
|
|
7
|
+
|
|
8
|
+
const SENTENCE_END = /[.?!]["'\u201D\u2019)\]]*$/
|
|
9
|
+
const LINE_BREAKS = new Set(['insertParagraph', 'insertLineBreak'])
|
|
10
|
+
const ALONE = new Set([
|
|
11
|
+
'insertFromPaste',
|
|
12
|
+
'insertFromDrop',
|
|
13
|
+
'insertFromYank',
|
|
14
|
+
'insertReplacementText',
|
|
15
|
+
'deleteByCut',
|
|
16
|
+
'deleteByDrag',
|
|
17
|
+
])
|
|
18
|
+
const TYPING_KEYS = new Set(['Enter', 'Backspace', 'Delete'])
|
|
19
|
+
const NAV_KEYS = new Set([
|
|
20
|
+
'ArrowLeft',
|
|
21
|
+
'ArrowRight',
|
|
22
|
+
'ArrowUp',
|
|
23
|
+
'ArrowDown',
|
|
24
|
+
'Home',
|
|
25
|
+
'End',
|
|
26
|
+
'PageUp',
|
|
27
|
+
'PageDown',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
export function endsSentence(textBefore) {
|
|
31
|
+
return SENTENCE_END.test(textBefore)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function watchTyping(root, { sentence, endStep, stepOpen = () => true, composing }) {
|
|
35
|
+
const view = root.ownerDocument && root.ownerDocument.defaultView
|
|
36
|
+
if (!view) return () => {}
|
|
37
|
+
const inside = (node) => !!node && typeof node.nodeType === 'number' && (node === root || root.contains(node))
|
|
38
|
+
let breakAfter = false
|
|
39
|
+
let jumped = false
|
|
40
|
+
let early = false
|
|
41
|
+
let last = { kind: 'other', host: null }
|
|
42
|
+
|
|
43
|
+
const kindOf = (type) => ALONE.has(type) || type.startsWith('format') ? 'alone'
|
|
44
|
+
: type.startsWith('delete') ? 'delete'
|
|
45
|
+
: type.startsWith('insert') ? 'insert' : 'other'
|
|
46
|
+
|
|
47
|
+
const hostOf = (target) => {
|
|
48
|
+
if (!target) return null
|
|
49
|
+
const el = target.nodeType === 1 ? target : target.parentElement
|
|
50
|
+
const host = el && el.closest && el.closest('[contenteditable]:not([contenteditable="false"])')
|
|
51
|
+
return host || target
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A rich editor may cancel the line break and insert the paragraph itself, so
|
|
55
|
+
// no input event follows: the timer closes the step either way, and the flush
|
|
56
|
+
// takes the pending records synchronously.
|
|
57
|
+
const closeAfter = () => {
|
|
58
|
+
if (!breakAfter) return
|
|
59
|
+
breakAfter = false
|
|
60
|
+
endStep()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const onBeforeInput = (event) => {
|
|
64
|
+
if (!sentence || event.isComposing || !inside(event.target)) return
|
|
65
|
+
const type = event.inputType || ''
|
|
66
|
+
const kind = kindOf(type)
|
|
67
|
+
const host = hostOf(event.target)
|
|
68
|
+
const selection = view.getSelection && view.getSelection()
|
|
69
|
+
const collapsed = !selection || selection.isCollapsed
|
|
70
|
+
const wasJumped = jumped
|
|
71
|
+
jumped = false
|
|
72
|
+
const wasEarly = early
|
|
73
|
+
early = false
|
|
74
|
+
if (
|
|
75
|
+
kind === 'alone' ||
|
|
76
|
+
(!wasEarly && kind !== last.kind && kind !== 'other' && last.kind !== 'other') ||
|
|
77
|
+
(!wasEarly && ((last.host !== null && host !== last.host) || !collapsed || wasJumped))
|
|
78
|
+
) {
|
|
79
|
+
endStep()
|
|
80
|
+
}
|
|
81
|
+
last = { kind, host }
|
|
82
|
+
if (kind === 'alone') {
|
|
83
|
+
breakAfter = true
|
|
84
|
+
setTimeout(closeAfter, 0)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
const space = type === 'insertText' && (event.data === ' ' || event.data === '\u00A0')
|
|
88
|
+
if (!space && !LINE_BREAKS.has(type)) return
|
|
89
|
+
if (space && !stepOpen()) return
|
|
90
|
+
if (!endsSentence(textBeforeCaret(view))) return
|
|
91
|
+
breakAfter = true
|
|
92
|
+
setTimeout(closeAfter, 0)
|
|
93
|
+
}
|
|
94
|
+
const onInput = (event) => { if (inside(event.target)) closeAfter() }
|
|
95
|
+
const onStart = (event) => { if (inside(event.target)) composing(true) }
|
|
96
|
+
const onEnd = (event) => { if (inside(event.target)) composing(false) }
|
|
97
|
+
const onPointerDown = (event) => { if (inside(event.target)) jumped = true }
|
|
98
|
+
// A rich editor may write in its own keydown, before beforeinput: it deletes the
|
|
99
|
+
// selection the key replaces, and it tidies its markup on the first key. A caret jump,
|
|
100
|
+
// a field switch and a replaced selection close the step here, so those writes belong
|
|
101
|
+
// to the step the key starts.
|
|
102
|
+
const onKeyDown = (event) => {
|
|
103
|
+
if (!inside(event.target)) return
|
|
104
|
+
if (NAV_KEYS.has(event.key)) { jumped = true; return }
|
|
105
|
+
if (!sentence || event.isComposing || event.keyCode === 229 || event.ctrlKey || event.metaKey) return
|
|
106
|
+
if (!event.key || (event.key.length !== 1 && !TYPING_KEYS.has(event.key))) return
|
|
107
|
+
const selection = view.getSelection && view.getSelection()
|
|
108
|
+
const switched = last.host !== null && hostOf(event.target) !== last.host
|
|
109
|
+
if (!jumped && !switched && (!selection || selection.isCollapsed)) return
|
|
110
|
+
endStep()
|
|
111
|
+
jumped = false
|
|
112
|
+
early = true
|
|
113
|
+
setTimeout(() => { early = false }, 0)
|
|
114
|
+
}
|
|
115
|
+
root.addEventListener('beforeinput', onBeforeInput, true)
|
|
116
|
+
root.addEventListener('input', onInput, true)
|
|
117
|
+
root.addEventListener('compositionstart', onStart, true)
|
|
118
|
+
root.addEventListener('compositionend', onEnd, true)
|
|
119
|
+
root.addEventListener('pointerdown', onPointerDown, true)
|
|
120
|
+
root.addEventListener('keydown', onKeyDown, true)
|
|
121
|
+
return () => {
|
|
122
|
+
root.removeEventListener('beforeinput', onBeforeInput, true)
|
|
123
|
+
root.removeEventListener('input', onInput, true)
|
|
124
|
+
root.removeEventListener('compositionstart', onStart, true)
|
|
125
|
+
root.removeEventListener('compositionend', onEnd, true)
|
|
126
|
+
root.removeEventListener('pointerdown', onPointerDown, true)
|
|
127
|
+
root.removeEventListener('keydown', onKeyDown, true)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function textBeforeCaret(view) {
|
|
132
|
+
const selection = view.getSelection && view.getSelection()
|
|
133
|
+
if (!selection || !selection.isCollapsed || !selection.anchorNode) return ''
|
|
134
|
+
const anchor = selection.anchorNode
|
|
135
|
+
const element = anchor.nodeType === 1 ? anchor : anchor.parentElement
|
|
136
|
+
const host = element && element.closest('[contenteditable]:not([contenteditable="false"])')
|
|
137
|
+
if (!host) return ''
|
|
138
|
+
const range = host.ownerDocument.createRange()
|
|
139
|
+
range.setStart(host, 0)
|
|
140
|
+
range.setEnd(anchor, selection.anchorOffset)
|
|
141
|
+
return range.toString().replace(/\u200B/g, '').slice(-8)
|
|
142
|
+
}
|