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 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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-undo",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "DOM-state undo/redo via MutationObserver inverse-op replay. Standalone or paired with hyperclayjs.",
5
5
  "keywords": [
6
6
  "undo",
package/src/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 { return !resolve(node).undoable }
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
- for (const attr of IGNORE_ATTRS) {
41
- if (el.hasAttribute && el.hasAttribute(attr)) return true
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
- if (regionNotUndoable(node)) return true
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.target[p.prop] = p.newValue
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
- for (const node of p.nodes) {
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] = p.oldValue
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
- for (const node of p.nodes) {
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