hyper-undo 0.5.1 → 0.6.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 +12 -0
- package/dist/hyper-undo.min.js +1 -1
- package/package.json +1 -1
- package/src/filter.js +23 -5
- package/src/index.js +8 -0
- package/src/lib/content-dom.js +110 -0
- package/src/lib/region-capabilities.js +71 -0
- package/src/primitives.js +41 -18
- package/src/scope.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# hyper-undo changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- An undo step only replays while the change it describes still stands
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.5.2] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Undo and redo inside a live richclay text region now reach the editor instead of the page. `[data-richclay-active]` joins the default `shadowKeydownIn` list, alongside CodeMirror, Monaco, Ace, Quill, TipTap and ProseMirror. richclay stamps `no-undo` on its region, so the page-level stack holds no record of what was typed there; the capture-phase key handler nevertheless swallowed Cmd+Z, which meant the keystroke never reached the editor's own stack and the page instead reverted an unrelated earlier edit. richclay has always intended this deferral, and this is the other half of it.
|
|
14
|
+
|
|
3
15
|
## [0.5.1] - 2026-08-21
|
|
4
16
|
|
|
5
17
|
### Added
|
package/dist/hyper-undo.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var hyperundo=(()=>{var T=Object.defineProperty;var ee=Object.getOwnPropertyDescriptor;var te=Object.getOwnPropertyNames;var re=Object.prototype.hasOwnProperty;var ne=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},oe=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of te(t))!re.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=ee(t,o))||r.enumerable});return e};var ae=e=>oe(T({},"__esModule",{value:!0}),e);var me={};ne(me,{default:()=>pe,undo:()=>C});function ie(e,t=null){let n=[];if(e.type==="attributes"){let r=e.target,o=e.attributeName,i=e.oldValue,u=r.getAttribute(o);return i==null&&u!=null?n.push({kind:"attr-add",target:r,name:o,newValue:u}):i!=null&&u==null?n.push({kind:"attr-remove",target:r,name:o,oldValue:i}):i!==u&&n.push({kind:"attr-set",target:r,name:o,oldValue:i,newValue:u}),n}if(e.type==="characterData")return n.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),n;if(e.type==="childList"){let r=e.target,o=e.nextSibling,i=t?Array.from(e.addedNodes).filter(d=>!t(d)):Array.from(e.addedNodes),u=t?Array.from(e.removedNodes).filter(d=>!t(d)):Array.from(e.removedNodes);return i.length>0&&n.push({kind:"add",parent:r,nodes:i,before:o}),u.length>0&&n.push({kind:"remove",parent:r,nodes:u,before:o}),n}return n}function P(e,t=null){let n=[],r=new Map;for(let o of e){if(o.type==="attributes"){let i=o.target,u=o.attributeName,d=r.get(i);d||(d=new Map,r.set(i,d)),d.has(u)||(d.set(u,{idx:n.length,oldValue:o.oldValue}),n.push(null));continue}for(let i of ie(o,t))n.push(i)}for(let[o,i]of r)for(let[u,d]of i){let l=d.oldValue,m=o.getAttribute(u),p=null;l===m?p=null:l==null?p={kind:"attr-add",target:o,name:u,newValue:m}:m==null?p={kind:"attr-remove",target:o,name:u,oldValue:l}:p={kind:"attr-set",target:o,name:u,oldValue:l,newValue:m},n[d.idx]=p}return n.filter(o=>o!=null)}function L(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.target[e.prop]=e.newValue;return;case"add":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function D(e){switch(e.kind){case"attr-set":e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":e.target.removeAttribute(e.name);return;case"attr-remove":e.target.setAttribute(e.name,e.oldValue);return;case"text":e.target.data=e.oldValue;return;case"value":e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":for(let t of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t);return}}var se=['[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]"],ue=["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"],M=se.join(","),U=new RegExp("^(?:"+ue.join("|")+")(?:-|$)");var de=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function le(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function ce(e){let t=le();if(t)try{return!t(e).undoable}catch{}let n=e&&e.nodeType!==1?e.parentElement:e;if(n&&n.closest&&n.closest(M))return!0;for(;n&&n.nodeType===1;){for(let r of de)if(n.hasAttribute&&n.hasAttribute(r))return!0;n=n.parentElement}return!1}function R(e,t,n){if(ce(e))return!0;if(n&&n.type==="attributes"){if(n.attributeName&&U.test(n.attributeName.toLowerCase()))return!0;if(t)try{if(t(n.attributeName,n.target))return!0}catch{}}return!1}function X(){let e=new Map;function t(o,i){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(i),()=>n(o,i)}function n(o,i){let u=e.get(o);u&&u.delete(i)}function r(o,i){let u=e.get(o);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:t,off:n,emit:r}}var fe={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function N(e){let t={...fe,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let n=X(),r=[],o=[],i=[],u=null,d=null,l=0,m=!1;function p(...s){t.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=W(g),d.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),p("started"))}function W(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from the platform Mutation hub (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),y(),r.length=0,o.length=0,i=[],l=0,p("stopped"))}function V(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(l>0)return;let c=_(s);if(c.length>0){for(let f of c)i.push(f);I()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return P(c,f=>R(f,t.ignoreAttribute))}function I(){y(),u=setTimeout(K,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],v(t.idleLabel,s)}function v(s,c){for(r.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;r.length>t.maxHistory;)r.shift();p("commit",s,"primitives:",c.length),n.emit("commit")}function j(s,c){if(!m)throw new Error("hyper-undo: scope not started");g(d.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(d.takeRecords()),i.length===0){n.emit("commit");return}y();let b=i;i=[],v(s,b)}function G(s){if(!m)throw new Error("hyper-undo: scope not started");if(!d)return;if(l>1){d.takeRecords();return}w();let c=_(d.takeRecords());c.length!==0&&(y(),v(s,c))}function Z(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function $(s,{prop:c="value",oldValue:f,newValue:b}={}){!s||f===b||O({kind:"value",target:s,prop:c,oldValue:f,newValue:b})}function w(){l===0&&d&&g(d.takeRecords()),i.length!==0&&(y(),K())}function k(){l===0&&d&&g(d.takeRecords()),l++,p("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),p("resumed",l))}function J(){if(V()){E();return}w();let s=r.pop();if(s){k();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(f){p("undo primitive failed (continuing)",f)}}finally{x()}o.push(s),p("undo",s.label),n.emit("undo")}}function Q(){if(V()){E();return}w();let s=o.pop();if(s){k();try{for(let c of s.primitives)try{L(c)}catch(f){p("redo primitive failed (continuing)",f)}}finally{x()}r.push(s),p("redo",s.label),n.emit("redo")}}function Y(){d&&d.takeRecords(),r.length=0,o.length=0,i=[],y(),p("cleared"),n.emit("clear")}return{_config:t,start:H,stop:E,commit:j,commitCaptured:G,discardCaptured:Z,flush:w,record:O,recordValue:$,undo:J,redo:Q,clear:Y,pause:k,resume:x,on:n.on,off:n.off,get canUndo(){return r.length>0},get canRedo(){return o.length>0},get isPaused(){return l>0},get history(){return r.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function q(e,t){let n=r=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let i=r.target,u=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(u))for(let m of u)try{if(i.closest(m))return}catch{}if(!(r.metaKey||r.ctrlKey))return;let l=(r.key||"").toLowerCase();if(l==="z"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.undo();return}if(l==="z"&&r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}if(l==="y"&&!r.shiftKey){r.preventDefault(),r.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",n,!0),()=>window.removeEventListener("keydown",n,!0)}var A=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,S=null,z=null,h=null;function F(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let n=q(e,t);return h={inst:e,cleanup:n},n}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var C={defaults:{shadowKeydownIn:A},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(S===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),a;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 n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A};return a=N(n),a.start(),S=t,e.bindKeys!==!1&&(z=F(a,()=>C.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,S=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let n={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||A},r=N(n),o=null,i={_config:r._config,commit:r.commit,commitCaptured:r.commitCaptured,discardCaptured:r.discardCaptured,flush:r.flush,record:r.record,recordValue:r.recordValue,undo:r.undo,redo:r.redo,clear:r.clear,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 scope(){return r.scope},start(){e.bindKeys&&(o=F(r,()=>i.stop())),r.start()},stop(){B(r),o=null,r.stop()}};return i},commit(e,t){return a?a.commit(e,t):void 0},commitCaptured(e){return a?a.commitCaptured(e):void 0},discardCaptured(){return a?a.discardCaptured():void 0},record(e){return a?a.record(e):void 0},recordValue(e,t){return a?a.recordValue(e,t):void 0},undo(){return a?a.undo():void 0},redo(){return a?a.redo():void 0},clear(){return a?a.clear():void 0},pause(){return a?a.pause():void 0},resume(){return a?a.resume():void 0},flush(){return a?a.flush():void 0},on(e,t){return a?a.on(e,t):void 0},off(e,t){return a?a.off(e,t):void 0},get canUndo(){return a?a.canUndo:!1},get canRedo(){return a?a.canRedo:!1},get history(){return a?a.history:[]},get isPaused(){return a?a.isPaused:!1}};var pe=C;return ae(me);})();
|
|
1
|
+
var hyperundo=(()=>{var T=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ue=Object.getOwnPropertyNames;var le=Object.prototype.hasOwnProperty;var de=(e,t)=>{for(var r in t)T(e,r,{get:t[r],enumerable:!0})},ce=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ue(t))!le.call(e,o)&&o!==r&&T(e,o,{get:()=>t[o],enumerable:!(n=se(t,o))||n.enumerable});return e};var fe=e=>ce(T({},"__esModule",{value:!0}),e);var xe={};de(xe,{default:()=>ke,undo:()=>I});function pe(e,t=null){let r=[];if(e.type==="attributes"){let n=e.target,o=e.attributeName,a=e.oldValue,u=n.getAttribute(o);return a==null&&u!=null?r.push({kind:"attr-add",target:n,name:o,newValue:u}):a!=null&&u==null?r.push({kind:"attr-remove",target:n,name:o,oldValue:a}):a!==u&&r.push({kind:"attr-set",target:n,name:o,oldValue:a,newValue:u}),r}if(e.type==="characterData")return r.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),r;if(e.type==="childList"){let n=e.target,o=e.nextSibling,a=e.nextSibling;for(;a&&t?.(a);)a=a.nextSibling;let u=e.previousSibling;for(;u&&t?.(u);)u=u.previousSibling;let l=t?Array.from(e.addedNodes).filter(p=>!t(p)):Array.from(e.addedNodes),d=t?Array.from(e.removedNodes).filter(p=>!t(p)):Array.from(e.removedNodes);return l.length>0&&r.push({kind:"add",parent:n,nodes:l,before:o,afterAuthored:a,beforeAuthored:u}),d.length>0&&r.push({kind:"remove",parent:n,nodes:d,before:o,afterAuthored:a,beforeAuthored:u}),r}return r}function D(e,t=null){let r=[],n=new Map;for(let o of e){if(o.type==="attributes"){let a=o.target,u=o.attributeName,l=n.get(a);l||(l=new Map,n.set(a,l)),l.has(u)||(l.set(u,{idx:r.length,oldValue:o.oldValue}),r.push(null));continue}for(let a of pe(o,t))r.push(a)}for(let[o,a]of n)for(let[u,l]of a){let d=l.oldValue,p=o.getAttribute(u),m=null;d===p?m=null:d==null?m={kind:"attr-add",target:o,name:u,newValue:p}:p==null?m={kind:"attr-remove",target:o,name:u,oldValue:d}:m={kind:"attr-set",target:o,name:u,oldValue:d,newValue:p},r[l.idx]=m}return r.filter(o=>o!=null)}function j(e){switch(e.kind){case"attr-set":case"attr-add":e.target.setAttribute(e.name,e.newValue);return;case"attr-remove":e.target.removeAttribute(e.name);return;case"text":e.target.data=e.newValue;return;case"value":e.write?e.write(e.target,e.newValue):e.target[e.prop]=e.newValue;return;case"add":M(e);return;case"remove":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return}}function U(e){switch(e.kind){case"attr-set":if(e.target.getAttribute(e.name)!==e.newValue)return;e.target.setAttribute(e.name,e.oldValue);return;case"attr-add":if(e.target.getAttribute(e.name)!==e.newValue)return;e.target.removeAttribute(e.name);return;case"attr-remove":if(e.target.hasAttribute(e.name))return;e.target.setAttribute(e.name,e.oldValue);return;case"text":if(e.target.data!==e.newValue)return;e.target.data=e.oldValue;return;case"value":if((e.read?e.read(e.target):e.target[e.prop])!==e.newValue)return;e.write?e.write(e.target,e.oldValue):e.target[e.prop]=e.oldValue;return;case"add":for(let t of e.nodes)t.parentNode===e.parent&&e.parent.removeChild(t);return;case"remove":M(e);return}}function M(e){if(e.afterAuthored?.parentNode===e.parent){for(let t of e.nodes)e.parent.insertBefore(t,e.afterAuthored);return}if(e.beforeAuthored?.parentNode===e.parent){let t=e.beforeAuthored;for(let r of e.nodes)t.after(r),t=r;return}for(let t of e.nodes)e.before?.parentNode===e.parent?e.parent.insertBefore(t,e.before):e.parent.appendChild(t)}var me=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],he=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],X=me.join(","),B=new RegExp("^(?:"+he.join("|")+")(?:-|$)");var Se=Object.freeze({data:Object.freeze({tokens:["no-data"],bundles:["editor-ui"]}),save:Object.freeze({tokens:["no-save"],bundles:["editor-ui"]}),snapshot:Object.freeze({tokens:["no-snapshot"],bundles:["editor-ui"]}),watch:Object.freeze({tokens:["no-watch"],bundles:["editor-ui"]}),undo:Object.freeze({tokens:["no-undo"],bundles:["editor-ui"]}),history:Object.freeze({tokens:[],bundles:["editor-ui"]})}),ye=Object.freeze({"editor-ui":Object.freeze(["no-data","no-save","no-snapshot","no-watch","no-undo"])}),ge=Object.freeze(["no-save","no-snapshot","no-trigger-autosave","no-dirty","no-watch","no-undo","no-data","freeze","editor-ui"]);function E(e,t){if(!e||e.nodeType!==1)return!1;let r=e.getAttribute?.("clay");return!!(r&&r.split(/\s+/).includes(t)||e.hasAttribute?.(t))}function N(e,t){if(E(e,t))return!0;for(let[r,n]of Object.entries(ye))if(n.includes(t)&&E(e,r))return!0;return!1}function q(e){return e==="clay"||ge.includes(e)}var we=["mutations-ignore","save-remove","save-ignore","save-freeze","no-undo","no-watch"];function be(){if(typeof window>"u")return null;let e=window.clay?.region||window.hyperclay?.region;return e&&typeof e.resolveRegionPolicy=="function"?e.resolveRegionPolicy:null}function S(e){let t=be();if(t)try{if(!t(e).undoable)return!0}catch{}let r=e&&e.nodeType!==1?e.parentElement:e;if(r&&r.closest&&r.closest(X))return!0;for(;r&&r.nodeType===1;){if(N(r,"no-undo")||N(r,"no-watch"))return!0;for(let n of we)if(E(r,n))return!0;r=r.parentElement}return!1}function Ee(e){let t=e.target,r=t.cloneNode(!1),n=r;for(;t.parentElement;){t=t.parentElement;let o=t.cloneNode(!1);o.appendChild(r),r=o}return e.oldValue==null?n.removeAttribute(e.attributeName):n.setAttribute(e.attributeName,e.oldValue),S(n)}function R(e,t,r){if(r?.type==="attributes"&&q(r.attributeName)){if(S(e)&&Ee(r))return!0}else if(S(e))return!0;if(r&&r.type==="attributes"){if(r.attributeName&&B.test(r.attributeName.toLowerCase()))return!0;if(t)try{if(t(r.attributeName,r.target))return!0}catch{}}return!1}function F(){let e=new Map;function t(o,a){let u=e.get(o);return u||(u=new Set,e.set(o,u)),u.add(a),()=>r(o,a)}function r(o,a){let u=e.get(o);u&&u.delete(a)}function n(o,a){let u=e.get(o);if(u)for(let l of Array.from(u))try{l(a)}catch(d){console.error("[hyper-undo] listener threw",d)}}return{on:t,off:r,emit:n}}var ve={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function O(e){let t={...ve,...e};if(!t.scope)throw new Error("hyper-undo: scope is required");let r=F(),n=[],o=[],a=[],u=null,l=null,d=0,p=!1;function m(...s){t.debug&&console.log("[hyper-undo]",...s)}function Y(){p||(p=!0,l=Z(g),l.observe(t.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),m("started"))}function Z(s){let c=typeof document<"u"&&t.scope===document.body,f=typeof window<"u"&&(window.clay?.Mutation||window.hyperclay?.Mutation)||null;return c&&f&&typeof f.createObserver=="function"?(m("sourcing records from the platform Mutation hub (shared observer)"),f.createObserver(s)):new MutationObserver(s)}function v(){p&&(p=!1,l&&(l.disconnect(),l=null),y(),n.length=0,o.length=0,a=[],d=0,m("stopped"))}function P(){let s=t.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function g(s){if(d>0)return;let c=_(s);if(c.length>0){for(let f of c)a.push(f);K()}}function _(s){let c=s.filter(f=>!R(f.target,t.ignoreAttribute,f));return D(c,f=>R(f,t.ignoreAttribute))}function K(){y(),u=setTimeout(L,t.idleWindowMs)}function y(){u!=null&&(clearTimeout(u),u=null)}function L(){if(u=null,a.length===0)return;let s=a;a=[],k(t.idleLabel,s)}function k(s,c){for(n.push({label:s,timestamp:Date.now(),primitives:c}),o.length=0;n.length>t.maxHistory;)n.shift();m("commit",s,"primitives:",c.length),r.emit("commit")}function J(s,c){if(!p)throw new Error("hyper-undo: scope not started");g(l.takeRecords()),w();let f=c();if(f&&typeof f.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(g(l.takeRecords()),a.length===0){r.emit("commit");return}y();let b=a;a=[],k(s,b)}function Q(s){if(!p)throw new Error("hyper-undo: scope not started");if(!l)return;if(d>1){l.takeRecords();return}w();let c=_(l.takeRecords());c.length!==0&&(y(),k(s,c))}function ee(){l&&l.takeRecords()}function z(s){!p||d>0||!s||(a.push(s),K())}function te(s,{prop:c="value",oldValue:f,newValue:b,read:ae,write:ie}={}){!s||f===b||z({kind:"value",target:s,prop:c,oldValue:f,newValue:b,read:ae,write:ie})}function w(){d===0&&l&&g(l.takeRecords()),a.length!==0&&(y(),L())}function x(){d===0&&l&&g(l.takeRecords()),d++,m("paused",d)}function A(){d!==0&&(d--,d===0&&l&&l.takeRecords(),m("resumed",d))}function re(){if(P()){v();return}w();let s=n.pop();if(s){x();try{for(let c=s.primitives.length-1;c>=0;c--)try{U(s.primitives[c])}catch(f){m("undo primitive failed (continuing)",f)}}finally{A()}o.push(s),m("undo",s.label),r.emit("undo")}}function ne(){if(P()){v();return}w();let s=o.pop();if(s){x();try{for(let c of s.primitives)try{j(c)}catch(f){m("redo primitive failed (continuing)",f)}}finally{A()}n.push(s),m("redo",s.label),r.emit("redo")}}function oe(){l&&l.takeRecords(),n.length=0,o.length=0,a=[],y(),m("cleared"),r.emit("clear")}return{_config:t,start:Y,stop:v,commit:J,commitCaptured:Q,discardCaptured:ee,flush:w,record:z,recordValue:te,undo:re,redo:ne,clear:oe,pause:x,resume:A,on:r.on,off:r.off,get canUndo(){return n.length>0},get canRedo(){return o.length>0},get isPaused(){return d>0},get history(){return n.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return t.scope}}}function W(e,t){let r=n=>{let o=e.scope;if(o&&o.nodeType===1&&o.isConnected===!1){t&&t();return}let a=n.target,u=e._config.shadowKeydownIn;if(a&&a.closest&&Array.isArray(u))for(let p of u)try{if(a.closest(p))return}catch{}if(!(n.metaKey||n.ctrlKey))return;let d=(n.key||"").toLowerCase();if(d==="z"&&!n.shiftKey){n.preventDefault(),n.stopPropagation(),e.undo();return}if(d==="z"&&n.shiftKey){n.preventDefault(),n.stopPropagation(),e.redo();return}if(d==="y"&&!n.shiftKey){n.preventDefault(),n.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",r,!0),()=>window.removeEventListener("keydown",r,!0)}var C=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror","[data-richclay-active]"],i=null,V=null,$=null,h=null;function H(e,t){if(h){if(h.inst===e)return h.cleanup;throw new Error("hyper-undo: another scope already owns the global Cmd+Z binding (bindKeys: true). Stop it first, or use bindKeys: false for additional scopes.")}let r=W(e,t);return h={inst:e,cleanup:r},r}function G(e){h&&h.inst===e&&(h.cleanup(),h=null)}var I={defaults:{shadowKeydownIn:C},start(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(i){if(V===t)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),i;throw new Error("hyper-undo: start() called with a different scope while the singleton is already started. Use undo.create({ scope }) for additional scopes, or call undo.stop() first.")}let r={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||C};return i=O(r),i.start(),V=t,e.bindKeys!==!1&&($=H(i,()=>I.stop())),i},stop(){i&&(G(i),$=null,i.stop(),i=null,V=null)},create(e={}){let t=e.scope||(typeof document<"u"?document.body:null);if(!t)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let r={...e,scope:t,shadowKeydownIn:e.shadowKeydownIn||C},n=O(r),o=null,a={_config:n._config,commit:n.commit,commitCaptured:n.commitCaptured,discardCaptured:n.discardCaptured,flush:n.flush,record:n.record,recordValue:n.recordValue,undo:n.undo,redo:n.redo,clear:n.clear,pause:n.pause,resume:n.resume,on:n.on,off:n.off,get canUndo(){return n.canUndo},get canRedo(){return n.canRedo},get isPaused(){return n.isPaused},get history(){return n.history},get scope(){return n.scope},start(){e.bindKeys&&(o=H(n,()=>a.stop())),n.start()},stop(){G(n),o=null,n.stop()}};return a},commit(e,t){return i?i.commit(e,t):void 0},commitCaptured(e){return i?i.commitCaptured(e):void 0},discardCaptured(){return i?i.discardCaptured():void 0},record(e){return i?i.record(e):void 0},recordValue(e,t){return i?i.recordValue(e,t):void 0},undo(){return i?i.undo():void 0},redo(){return i?i.redo():void 0},clear(){return i?i.clear():void 0},pause(){return i?i.pause():void 0},resume(){return i?i.resume():void 0},flush(){return i?i.flush():void 0},on(e,t){return i?i.on(e,t):void 0},off(e,t){return i?i.off(e,t):void 0},get canUndo(){return i?i.canUndo:!1},get canRedo(){return i?i.canRedo:!1},get history(){return i?i.history:[]},get isPaused(){return i?i.isPaused:!1}};var ke=I;return fe(xe);})();
|
package/package.json
CHANGED
package/src/filter.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// walk that mirrors the resolver's `undoable` axis.
|
|
12
12
|
|
|
13
13
|
import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from './extension-noise.js'
|
|
14
|
+
import { expandsTo, hasPolicyToken, isPolicyAttribute } from './lib/region-capabilities.js'
|
|
14
15
|
|
|
15
16
|
// Local fallback: attributes whose region is NOT undoable. The new no-save /
|
|
16
17
|
// no-trigger-autosave / freeze are deliberately ABSENT — those regions ARE
|
|
@@ -30,23 +31,40 @@ function sharedResolver() {
|
|
|
30
31
|
function regionNotUndoable(node) {
|
|
31
32
|
const resolve = sharedResolver()
|
|
32
33
|
if (resolve) {
|
|
33
|
-
try {
|
|
34
|
+
try { if (!resolve(node).undoable) return true }
|
|
34
35
|
catch (_) { /* fall through to the local walk */ }
|
|
35
36
|
}
|
|
36
37
|
let el = (node && node.nodeType !== 1) ? node.parentElement : node
|
|
37
38
|
// Browser-extension injected elements (and their descendants) are not page content.
|
|
38
39
|
if (el && el.closest && el.closest(EXTENSION_NODE_SELECTOR)) return true
|
|
39
40
|
while (el && el.nodeType === 1) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
41
|
+
if (expandsTo(el, 'no-undo') || expandsTo(el, 'no-watch')) return true
|
|
42
|
+
for (const attr of IGNORE_ATTRS) if (hasPolicyToken(el, attr)) return true
|
|
43
43
|
el = el.parentElement
|
|
44
44
|
}
|
|
45
45
|
return false
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function regionWasNotUndoable(record) {
|
|
49
|
+
let source = record.target
|
|
50
|
+
let clone = source.cloneNode(false)
|
|
51
|
+
const leaf = clone
|
|
52
|
+
while (source.parentElement) {
|
|
53
|
+
source = source.parentElement
|
|
54
|
+
const parent = source.cloneNode(false)
|
|
55
|
+
parent.appendChild(clone)
|
|
56
|
+
clone = parent
|
|
57
|
+
}
|
|
58
|
+
if (record.oldValue == null) leaf.removeAttribute(record.attributeName)
|
|
59
|
+
else leaf.setAttribute(record.attributeName, record.oldValue)
|
|
60
|
+
return regionNotUndoable(leaf)
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function shouldIgnore(node, ignoreAttributePredicate, record) {
|
|
49
|
-
|
|
64
|
+
const policyTransition = record?.type === 'attributes' && isPolicyAttribute(record.attributeName)
|
|
65
|
+
if (policyTransition) {
|
|
66
|
+
if (regionNotUndoable(node) && regionWasNotUndoable(record)) return true
|
|
67
|
+
} else if (regionNotUndoable(node)) return true
|
|
50
68
|
|
|
51
69
|
if (record && record.type === 'attributes') {
|
|
52
70
|
// Extension marker attributes on a real element (password-manager field tags) are noise.
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,14 @@ const DEFAULT_SHADOW_LIST = [
|
|
|
9
9
|
'.ql-editor',
|
|
10
10
|
'.tiptap',
|
|
11
11
|
'.ProseMirror',
|
|
12
|
+
// richclay marks its live region with this and stamps no-undo on it, so the
|
|
13
|
+
// page-level stack holds no record of what was typed there. Without the
|
|
14
|
+
// bypass, Cmd+Z inside a richclay editor never reached Squire: it either did
|
|
15
|
+
// nothing or reverted an unrelated earlier edit elsewhere on the page.
|
|
16
|
+
// richclay has always intended this; see the comment on setupEditorAttributes
|
|
17
|
+
// in richclay/src/richclay.js, which says it marks the region so hyper-undo
|
|
18
|
+
// "defers to Squire instead of fighting it".
|
|
19
|
+
'[data-richclay-active]',
|
|
12
20
|
]
|
|
13
21
|
|
|
14
22
|
let singleton = null // the Scope instance returned by createScope
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// GENERATED from clayjs/src/lib/content-dom.js. Edit ClayJS and run `npm run propagate:foundation`.
|
|
2
|
+
|
|
3
|
+
import { capabilitySelector, hasCapability } from './region-capabilities.js'
|
|
4
|
+
|
|
5
|
+
const UNSUPPORTED_SELECTOR = /:(?:focus(?:-within|-visible)?|hover|active|visited|defined)\b/i
|
|
6
|
+
|
|
7
|
+
function copyControlState(source, copy) {
|
|
8
|
+
if (!/^(INPUT|TEXTAREA|SELECT|OPTION)$/.test(source.tagName || '')) return
|
|
9
|
+
const type = (source.getAttribute?.('type') || '').toLowerCase()
|
|
10
|
+
if ('value' in source && 'value' in copy && source.tagName !== 'OPTION' && type !== 'checkbox' && type !== 'radio') copy.value = source.value
|
|
11
|
+
if ('checked' in source && 'checked' in copy) copy.checked = source.checked
|
|
12
|
+
if ('selected' in source && 'selected' in copy) copy.selected = source.selected
|
|
13
|
+
if (source.tagName === 'SELECT') {
|
|
14
|
+
for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected
|
|
15
|
+
}
|
|
16
|
+
if ('indeterminate' in source && 'indeterminate' in copy) copy.indeterminate = source.indeterminate
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function matchesWithin(node, selector, boundary, inherit) {
|
|
20
|
+
if (inherit) return !!node.closest?.(selector)
|
|
21
|
+
let current = node
|
|
22
|
+
while (current?.nodeType === 1) {
|
|
23
|
+
if (current.matches(selector)) return true
|
|
24
|
+
if (current === boundary) break
|
|
25
|
+
current = current.parentElement
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
|
|
31
|
+
if (source.nodeType === 1 && (
|
|
32
|
+
(inherit ? hasCapability(source, capability) : matchesWithin(source, capabilityMatch, boundary, false)) ||
|
|
33
|
+
(exclude && matchesWithin(source, exclude, boundary, inherit))
|
|
34
|
+
)) return null
|
|
35
|
+
const copy = targetDocument.importNode(source, false)
|
|
36
|
+
maps.cloneToLive.set(copy, source)
|
|
37
|
+
maps.liveToClone.set(source, copy)
|
|
38
|
+
const sourceChildren = source.nodeType === 1 && source.tagName === 'TEMPLATE' ? source.content : source
|
|
39
|
+
const copyChildren = copy.nodeType === 1 && copy.tagName === 'TEMPLATE' ? copy.content : copy
|
|
40
|
+
if (sourceChildren !== source) {
|
|
41
|
+
maps.cloneToLive.set(copyChildren, sourceChildren)
|
|
42
|
+
maps.liveToClone.set(sourceChildren, copyChildren)
|
|
43
|
+
}
|
|
44
|
+
for (const child of sourceChildren.childNodes || []) {
|
|
45
|
+
const childCopy = importTree(child, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps)
|
|
46
|
+
if (childCopy) {
|
|
47
|
+
copyChildren.appendChild(childCopy)
|
|
48
|
+
if (child.nodeType === 1) copyControlState(child, childCopy)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (source.nodeType === 1) copyControlState(source, copy)
|
|
52
|
+
return copy
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createContentView(context, { capability = 'data', exclude = null, inherit = true } = {}) {
|
|
56
|
+
if (!context) throw new TypeError('createContentView requires a DOM context')
|
|
57
|
+
const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument
|
|
58
|
+
if (!sourceDocument?.implementation?.createHTMLDocument) {
|
|
59
|
+
throw new TypeError('createContentView requires an HTML DOM implementation')
|
|
60
|
+
}
|
|
61
|
+
const inertDocument = sourceDocument.implementation.createHTMLDocument('')
|
|
62
|
+
const cloneToLive = new WeakMap()
|
|
63
|
+
const liveToClone = new WeakMap()
|
|
64
|
+
const sourceRoot = context.nodeType === 9 ? context.documentElement : context
|
|
65
|
+
if (exclude) inertDocument.documentElement.matches(exclude)
|
|
66
|
+
const capabilityMatch = capabilitySelector(capability)
|
|
67
|
+
const root = importTree(sourceRoot, inertDocument, capability, capabilityMatch, exclude, sourceRoot, inherit, { cloneToLive, liveToClone })
|
|
68
|
+
if (context.nodeType === 9 && root) {
|
|
69
|
+
inertDocument.replaceChild(root, inertDocument.documentElement)
|
|
70
|
+
liveToClone.set(context, inertDocument)
|
|
71
|
+
cloneToLive.set(inertDocument, context)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const assertSelector = selector => {
|
|
75
|
+
if (UNSUPPORTED_SELECTOR.test(selector)) {
|
|
76
|
+
throw new Error(`Filtered content queries do not support stateful selector: ${selector}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
root,
|
|
82
|
+
document: inertDocument,
|
|
83
|
+
capability,
|
|
84
|
+
selector: capabilitySelector(capability),
|
|
85
|
+
cloneToLive,
|
|
86
|
+
liveToClone,
|
|
87
|
+
original(node) { return cloneToLive.get(node) || null },
|
|
88
|
+
cloneOf(node) { return liveToClone.get(node) || null },
|
|
89
|
+
query(selector, queryRoot = root) {
|
|
90
|
+
assertSelector(selector)
|
|
91
|
+
return Array.from(queryRoot.querySelectorAll(selector), node => cloneToLive.get(node) || node)
|
|
92
|
+
},
|
|
93
|
+
text(node = root) {
|
|
94
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
95
|
+
return projected?.textContent || ''
|
|
96
|
+
},
|
|
97
|
+
html(node = root) {
|
|
98
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
99
|
+
return projected?.innerHTML ?? ''
|
|
100
|
+
},
|
|
101
|
+
clone(node = root) {
|
|
102
|
+
const projected = node === root ? root : liveToClone.get(node)
|
|
103
|
+
return projected ? inertDocument.importNode(projected, true) : null
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function cleanContentClone(node, options) {
|
|
109
|
+
return createContentView(node, options).root
|
|
110
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// GENERATED from clayjs/src/lib/region-capabilities.js. Edit ClayJS and run `npm run propagate:foundation`.
|
|
2
|
+
|
|
3
|
+
export const CAPABILITIES = Object.freeze({
|
|
4
|
+
data: Object.freeze({ tokens: ['no-data'], bundles: ['editor-ui'] }),
|
|
5
|
+
save: Object.freeze({ tokens: ['no-save'], bundles: ['editor-ui'] }),
|
|
6
|
+
snapshot: Object.freeze({ tokens: ['no-snapshot'], bundles: ['editor-ui'] }),
|
|
7
|
+
watch: Object.freeze({ tokens: ['no-watch'], bundles: ['editor-ui'] }),
|
|
8
|
+
undo: Object.freeze({ tokens: ['no-undo'], bundles: ['editor-ui'] }),
|
|
9
|
+
history: Object.freeze({ tokens: [], bundles: ['editor-ui'] }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export const BUNDLES = Object.freeze({
|
|
13
|
+
'editor-ui': Object.freeze(['no-data', 'no-save', 'no-snapshot', 'no-watch', 'no-undo']),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const POLICY_TOKENS = Object.freeze([
|
|
17
|
+
'no-save',
|
|
18
|
+
'no-snapshot',
|
|
19
|
+
'no-trigger-autosave',
|
|
20
|
+
'no-dirty',
|
|
21
|
+
'no-watch',
|
|
22
|
+
'no-undo',
|
|
23
|
+
'no-data',
|
|
24
|
+
'freeze',
|
|
25
|
+
'editor-ui',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
export function hasPolicyToken(element, token) {
|
|
29
|
+
if (!element || element.nodeType !== 1) return false
|
|
30
|
+
const clay = element.getAttribute?.('clay')
|
|
31
|
+
return !!(
|
|
32
|
+
(clay && clay.split(/\s+/).includes(token)) ||
|
|
33
|
+
element.hasAttribute?.(token)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function expandsTo(element, token) {
|
|
38
|
+
if (hasPolicyToken(element, token)) return true
|
|
39
|
+
for (const [bundle, members] of Object.entries(BUNDLES)) {
|
|
40
|
+
if (members.includes(token) && hasPolicyToken(element, bundle)) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function capabilitySelector(capability) {
|
|
46
|
+
const definition = CAPABILITIES[capability]
|
|
47
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
48
|
+
return [...definition.tokens, ...definition.bundles]
|
|
49
|
+
.flatMap(token => [`[clay~="${token}"]`, `[${token}]`])
|
|
50
|
+
.join(', ')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function closestWithCapability(node, capability) {
|
|
54
|
+
let element = node && node.nodeType === 1 ? node : node?.parentElement
|
|
55
|
+
while (element && element.nodeType === 1) {
|
|
56
|
+
const definition = CAPABILITIES[capability]
|
|
57
|
+
if (!definition) throw new Error(`Unknown region capability: ${capability}`)
|
|
58
|
+
if (definition.tokens.some(token => expandsTo(element, token)) ||
|
|
59
|
+
definition.bundles.some(bundle => hasPolicyToken(element, bundle))) return element
|
|
60
|
+
element = element.parentElement
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasCapability(node, capability) {
|
|
66
|
+
return !!closestWithCapability(node, capability)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isPolicyAttribute(name) {
|
|
70
|
+
return name === 'clay' || POLICY_TOKENS.includes(name)
|
|
71
|
+
}
|
package/src/primitives.js
CHANGED
|
@@ -37,6 +37,10 @@ export function recordToPrimitives(record, ignoreNode = null) {
|
|
|
37
37
|
if (record.type === 'childList') {
|
|
38
38
|
const parent = record.target
|
|
39
39
|
const before = record.nextSibling // the slot to re-insert before
|
|
40
|
+
let afterAuthored = record.nextSibling
|
|
41
|
+
while (afterAuthored && ignoreNode?.(afterAuthored)) afterAuthored = afterAuthored.nextSibling
|
|
42
|
+
let beforeAuthored = record.previousSibling
|
|
43
|
+
while (beforeAuthored && ignoreNode?.(beforeAuthored)) beforeAuthored = beforeAuthored.previousSibling
|
|
40
44
|
// The parent target already passed the caller's ignore filter, but an
|
|
41
45
|
// individual added/removed node can itself be an ignored subtree (e.g. a
|
|
42
46
|
// save-ignore CMS shell appended directly under <body>). Drop those nodes
|
|
@@ -49,10 +53,10 @@ export function recordToPrimitives(record, ignoreNode = null) {
|
|
|
49
53
|
? Array.from(record.removedNodes).filter((n) => !ignoreNode(n))
|
|
50
54
|
: Array.from(record.removedNodes)
|
|
51
55
|
if (added.length > 0) {
|
|
52
|
-
out.push({ kind: 'add', parent, nodes: added, before })
|
|
56
|
+
out.push({ kind: 'add', parent, nodes: added, before, afterAuthored, beforeAuthored })
|
|
53
57
|
}
|
|
54
58
|
if (removed.length > 0) {
|
|
55
|
-
out.push({ kind: 'remove', parent, nodes: removed, before })
|
|
59
|
+
out.push({ kind: 'remove', parent, nodes: removed, before, afterAuthored, beforeAuthored })
|
|
56
60
|
}
|
|
57
61
|
return out
|
|
58
62
|
}
|
|
@@ -132,16 +136,11 @@ export function replayForward(p) {
|
|
|
132
136
|
// An element PROPERTY write (e.g. input.value / checkbox.checked). These
|
|
133
137
|
// fire no MutationRecord, so they never arrive via the observer — a caller
|
|
134
138
|
// records them explicitly through scope.recordValue().
|
|
135
|
-
p.
|
|
139
|
+
if (p.write) p.write(p.target, p.newValue)
|
|
140
|
+
else p.target[p.prop] = p.newValue
|
|
136
141
|
return
|
|
137
142
|
case 'add':
|
|
138
|
-
|
|
139
|
-
if (p.before && p.before.parentNode === p.parent) {
|
|
140
|
-
p.parent.insertBefore(node, p.before)
|
|
141
|
-
} else {
|
|
142
|
-
p.parent.appendChild(node)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
143
|
+
insertAtRecordedContentSlot(p)
|
|
145
144
|
return
|
|
146
145
|
case 'remove':
|
|
147
146
|
for (const node of p.nodes) {
|
|
@@ -155,21 +154,32 @@ export function replayForward(p) {
|
|
|
155
154
|
|
|
156
155
|
// Reverse replay. Used by undo().
|
|
157
156
|
export function replayReverse(p) {
|
|
157
|
+
// A step only replays while the change it describes is still standing. With
|
|
158
|
+
// live sync the value can have moved on since it was recorded, and writing the
|
|
159
|
+
// old one back would not undo the person's own edit, it would overwrite
|
|
160
|
+
// somebody else's newer one. Skipping is the lesser surprise: an undo press
|
|
161
|
+
// that appears to do nothing costs a moment, an overwrite costs their work.
|
|
158
162
|
switch (p.kind) {
|
|
159
163
|
case 'attr-set':
|
|
164
|
+
if (p.target.getAttribute(p.name) !== p.newValue) return
|
|
160
165
|
p.target.setAttribute(p.name, p.oldValue)
|
|
161
166
|
return
|
|
162
167
|
case 'attr-add':
|
|
168
|
+
if (p.target.getAttribute(p.name) !== p.newValue) return
|
|
163
169
|
p.target.removeAttribute(p.name)
|
|
164
170
|
return
|
|
165
171
|
case 'attr-remove':
|
|
172
|
+
if (p.target.hasAttribute(p.name)) return
|
|
166
173
|
p.target.setAttribute(p.name, p.oldValue)
|
|
167
174
|
return
|
|
168
175
|
case 'text':
|
|
176
|
+
if (p.target.data !== p.newValue) return
|
|
169
177
|
p.target.data = p.oldValue
|
|
170
178
|
return
|
|
171
179
|
case 'value':
|
|
172
|
-
p.target[p.prop]
|
|
180
|
+
if ((p.read ? p.read(p.target) : p.target[p.prop]) !== p.newValue) return
|
|
181
|
+
if (p.write) p.write(p.target, p.oldValue)
|
|
182
|
+
else p.target[p.prop] = p.oldValue
|
|
173
183
|
return
|
|
174
184
|
case 'add':
|
|
175
185
|
// forward was add → reverse is remove
|
|
@@ -181,13 +191,26 @@ export function replayReverse(p) {
|
|
|
181
191
|
return
|
|
182
192
|
case 'remove':
|
|
183
193
|
// forward was remove → reverse is add. Re-insert at the same slot.
|
|
184
|
-
|
|
185
|
-
if (p.before && p.before.parentNode === p.parent) {
|
|
186
|
-
p.parent.insertBefore(node, p.before)
|
|
187
|
-
} else {
|
|
188
|
-
p.parent.appendChild(node)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
194
|
+
insertAtRecordedContentSlot(p)
|
|
191
195
|
return
|
|
192
196
|
}
|
|
193
197
|
}
|
|
198
|
+
|
|
199
|
+
function insertAtRecordedContentSlot(p) {
|
|
200
|
+
if (p.afterAuthored?.parentNode === p.parent) {
|
|
201
|
+
for (const node of p.nodes) p.parent.insertBefore(node, p.afterAuthored)
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
if (p.beforeAuthored?.parentNode === p.parent) {
|
|
205
|
+
let anchor = p.beforeAuthored
|
|
206
|
+
for (const node of p.nodes) {
|
|
207
|
+
anchor.after(node)
|
|
208
|
+
anchor = node
|
|
209
|
+
}
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
for (const node of p.nodes) {
|
|
213
|
+
if (p.before?.parentNode === p.parent) p.parent.insertBefore(node, p.before)
|
|
214
|
+
else p.parent.appendChild(node)
|
|
215
|
+
}
|
|
216
|
+
}
|
package/src/scope.js
CHANGED
|
@@ -193,9 +193,9 @@ export function createScope(opts) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Convenience for the common case: a value/checked property write on an element.
|
|
196
|
-
function recordValue(target, { prop = 'value', oldValue, newValue } = {}) {
|
|
196
|
+
function recordValue(target, { prop = 'value', oldValue, newValue, read, write } = {}) {
|
|
197
197
|
if (!target || oldValue === newValue) return
|
|
198
|
-
record({ kind: 'value', target, prop, oldValue, newValue })
|
|
198
|
+
record({ kind: 'value', target, prop, oldValue, newValue, read, write })
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// Force-close current idle batch as its own commit. Used by undo()/redo() and
|