hyper-undo 0.3.0 → 0.4.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,13 @@
1
1
  # hyper-undo changelog
2
2
 
3
+ ## [0.4.0] - 2026-06-16
4
+
5
+ ### Changed
6
+ - Integrated the region capability model into hyper-undo
7
+ - Adopted a shared MutationObserver in hyper-undo
8
+
9
+
10
+
3
11
  ## [0.3.0] - 2026-06-07
4
12
 
5
13
 
@@ -1 +1 @@
1
- var hyperundo=(()=>{var T=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var ee=Object.getOwnPropertyNames;var te=Object.prototype.hasOwnProperty;var re=(e,r)=>{for(var o in r)T(e,o,{get:r[o],enumerable:!0})},ne=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of ee(r))!te.call(e,n)&&n!==o&&T(e,n,{get:()=>r[n],enumerable:!(t=Y(r,n))||t.enumerable});return e};var oe=e=>ne(T({},"__esModule",{value:!0}),e);var ce={};re(ce,{default:()=>le,undo:()=>S});function ae(e,r=null){let o=[];if(e.type==="attributes"){let t=e.target,n=e.attributeName,i=e.oldValue,u=t.getAttribute(n);return i==null&&u!=null?o.push({kind:"attr-add",target:t,name:n,newValue:u}):i!=null&&u==null?o.push({kind:"attr-remove",target:t,name:n,oldValue:i}):i!==u&&o.push({kind:"attr-set",target:t,name:n,oldValue:i,newValue:u}),o}if(e.type==="characterData")return o.push({kind:"text",target:e.target,oldValue:e.oldValue,newValue:e.target.data}),o;if(e.type==="childList"){let t=e.target,n=e.nextSibling,i=r?Array.from(e.addedNodes).filter(d=>!r(d)):Array.from(e.addedNodes),u=r?Array.from(e.removedNodes).filter(d=>!r(d)):Array.from(e.removedNodes);return i.length>0&&o.push({kind:"add",parent:t,nodes:i,before:n}),u.length>0&&o.push({kind:"remove",parent:t,nodes:u,before:n}),o}return o}function P(e,r=null){let o=[],t=new Map;for(let n of e){if(n.type==="attributes"){let i=n.target,u=n.attributeName,d=t.get(i);d||(d=new Map,t.set(i,d)),d.has(u)||(d.set(u,{idx:o.length,oldValue:n.oldValue}),o.push(null));continue}for(let i of ae(n,r))o.push(i)}for(let[n,i]of t)for(let[u,d]of i){let l=d.oldValue,m=n.getAttribute(u),f=null;l===m?f=null:l==null?f={kind:"attr-add",target:n,name:u,newValue:m}:m==null?f={kind:"attr-remove",target:n,name:u,oldValue:l}:f={kind:"attr-set",target:n,name:u,oldValue:l,newValue:m},o[d.idx]=f}return o.filter(n=>n!=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 r of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(r,e.before):e.parent.appendChild(r);return;case"remove":for(let r of e.nodes)r.parentNode===e.parent&&e.parent.removeChild(r);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 r of e.nodes)r.parentNode===e.parent&&e.parent.removeChild(r);return;case"remove":for(let r of e.nodes)e.before&&e.before.parentNode===e.parent?e.parent.insertBefore(r,e.before):e.parent.appendChild(r);return}}var ie=['[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]"],se=["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=ie.join(","),U=new RegExp("^(?:"+se.join("|")+")(?:-|$)");var ue=["mutations-ignore","save-remove","save-ignore","save-freeze"];function N(e,r,o){let t=e&&e.nodeType!==1?e.parentElement:e;if(t&&t.closest&&t.closest(M))return!0;for(;t&&t.nodeType===1;){for(let n of ue)if(t.hasAttribute&&t.hasAttribute(n))return!0;t=t.parentElement}if(o&&o.type==="attributes"){if(o.attributeName&&U.test(o.attributeName.toLowerCase()))return!0;if(r)try{if(r(o.attributeName,o.target))return!0}catch{}}return!1}function X(){let e=new Map;function r(n,i){let u=e.get(n);return u||(u=new Set,e.set(n,u)),u.add(i),()=>o(n,i)}function o(n,i){let u=e.get(n);u&&u.delete(i)}function t(n,i){let u=e.get(n);if(u)for(let d of Array.from(u))try{d(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:r,off:o,emit:t}}var de={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function A(e){let r={...de,...e};if(!r.scope)throw new Error("hyper-undo: scope is required");let o=X(),t=[],n=[],i=[],u=null,d=null,l=0,m=!1;function f(...s){r.debug&&console.log("[hyper-undo]",...s)}function H(){m||(m=!0,d=new MutationObserver(y),d.observe(r.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),f("started"))}function E(){m&&(m=!1,d&&(d.disconnect(),d=null),g(),t.length=0,n.length=0,i=[],l=0,f("stopped"))}function V(){let s=r.scope;return!!(s&&s.nodeType===1&&s.isConnected===!1)}function y(s){if(l>0)return;let c=_(s);if(c.length>0){for(let p of c)i.push(p);I()}}function _(s){let c=s.filter(p=>!N(p.target,r.ignoreAttribute,p));return P(c,p=>N(p,r.ignoreAttribute))}function I(){g(),u=setTimeout(K,r.idleWindowMs)}function g(){u!=null&&(clearTimeout(u),u=null)}function K(){if(u=null,i.length===0)return;let s=i;i=[],k(r.idleLabel,s)}function k(s,c){for(t.push({label:s,timestamp:Date.now(),primitives:c}),n.length=0;t.length>r.maxHistory;)t.shift();f("commit",s,"primitives:",c.length),o.emit("commit")}function W(s,c){if(!m)throw new Error("hyper-undo: scope not started");y(d.takeRecords()),w();let p=c();if(p&&typeof p.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(y(d.takeRecords()),i.length===0){o.emit("commit");return}g();let b=i;i=[],k(s,b)}function j(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&&(g(),k(s,c))}function G(){d&&d.takeRecords()}function O(s){!m||l>0||!s||(i.push(s),I())}function Z(s,{prop:c="value",oldValue:p,newValue:b}={}){!s||p===b||O({kind:"value",target:s,prop:c,oldValue:p,newValue:b})}function w(){l===0&&d&&y(d.takeRecords()),i.length!==0&&(g(),K())}function v(){l===0&&d&&y(d.takeRecords()),l++,f("paused",l)}function x(){l!==0&&(l--,l===0&&d&&d.takeRecords(),f("resumed",l))}function $(){if(V()){E();return}w();let s=t.pop();if(s){v();try{for(let c=s.primitives.length-1;c>=0;c--)try{D(s.primitives[c])}catch(p){f("undo primitive failed (continuing)",p)}}finally{x()}n.push(s),f("undo",s.label),o.emit("undo")}}function J(){if(V()){E();return}w();let s=n.pop();if(s){v();try{for(let c of s.primitives)try{L(c)}catch(p){f("redo primitive failed (continuing)",p)}}finally{x()}t.push(s),f("redo",s.label),o.emit("redo")}}function Q(){d&&d.takeRecords(),t.length=0,n.length=0,i=[],g(),f("cleared"),o.emit("clear")}return{_config:r,start:H,stop:E,commit:W,commitCaptured:j,discardCaptured:G,flush:w,record:O,recordValue:Z,undo:$,redo:J,clear:Q,pause:v,resume:x,on:o.on,off:o.off,get canUndo(){return t.length>0},get canRedo(){return n.length>0},get isPaused(){return l>0},get history(){return t.map(s=>({label:s.label,timestamp:s.timestamp}))},get scope(){return r.scope}}}function q(e,r){let o=t=>{let n=e.scope;if(n&&n.nodeType===1&&n.isConnected===!1){r&&r();return}let i=t.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(!(t.metaKey||t.ctrlKey))return;let l=(t.key||"").toLowerCase();if(l==="z"&&!t.shiftKey){t.preventDefault(),t.stopPropagation(),e.undo();return}if(l==="z"&&t.shiftKey){t.preventDefault(),t.stopPropagation(),e.redo();return}if(l==="y"&&!t.shiftKey){t.preventDefault(),t.stopPropagation(),e.redo();return}};return window.addEventListener("keydown",o,!0),()=>window.removeEventListener("keydown",o,!0)}var R=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],a=null,C=null,z=null,h=null;function F(e,r){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 o=q(e,r);return h={inst:e,cleanup:o},o}function B(e){h&&h.inst===e&&(h.cleanup(),h=null)}var S={defaults:{shadowKeydownIn:R},start(e={}){let r=e.scope||(typeof document<"u"?document.body:null);if(!r)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");if(a){if(C===r)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 o={...e,scope:r,shadowKeydownIn:e.shadowKeydownIn||R};return a=A(o),a.start(),C=r,e.bindKeys!==!1&&(z=F(a,()=>S.stop())),a},stop(){a&&(B(a),z=null,a.stop(),a=null,C=null)},create(e={}){let r=e.scope||(typeof document<"u"?document.body:null);if(!r)throw new Error("hyper-undo: no scope (need document.body or explicit { scope })");let o={...e,scope:r,shadowKeydownIn:e.shadowKeydownIn||R},t=A(o),n=null,i={_config:t._config,commit:t.commit,commitCaptured:t.commitCaptured,discardCaptured:t.discardCaptured,flush:t.flush,record:t.record,recordValue:t.recordValue,undo:t.undo,redo:t.redo,clear:t.clear,pause:t.pause,resume:t.resume,on:t.on,off:t.off,get canUndo(){return t.canUndo},get canRedo(){return t.canRedo},get isPaused(){return t.isPaused},get history(){return t.history},get scope(){return t.scope},start(){e.bindKeys&&(n=F(t,()=>i.stop())),t.start()},stop(){B(t),n=null,t.stop()}};return i},commit(e,r){return a?a.commit(e,r):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,r){return a?a.recordValue(e,r):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,r){return a?a.on(e,r):void 0},off(e,r){return a?a.off(e,r):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 le=S;return oe(ce);})();
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(){return typeof window<"u"&&window.hyperclay&&window.hyperclay.region&&typeof window.hyperclay.region.resolveRegionPolicy=="function"?window.hyperclay.region.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.hyperclay&&window.hyperclay.Mutation||null;return c&&f&&typeof f.createObserver=="function"?(p("sourcing records from window.hyperclay.Mutation (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);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-undo",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "DOM-state undo/redo via MutationObserver inverse-op replay. Standalone or paired with hyperclayjs.",
5
5
  "keywords": [
6
6
  "undo",
@@ -33,7 +33,8 @@
33
33
  "build": "npx esbuild src/index.js --bundle --minify --format=iife --global-name=hyperundo --outfile=dist/hyper-undo.min.js",
34
34
  "copy-to-hyperclayjs": "npm run build && node scripts/copy-to-hyperclayjs.js",
35
35
  "test:node": "node --test 'test-node/**/*.test.js'",
36
- "test": "npm run test:node",
36
+ "test:shimmed": "HYPER_UNDO_SHIMMED=1 node --test 'test-node/**/*.test.js'",
37
+ "test": "npm run test:node && npm run test:shimmed",
37
38
  "release": "./scripts/release.sh",
38
39
  "prepublishOnly": "npm run build && npm test"
39
40
  },
package/src/filter.js CHANGED
@@ -1,27 +1,50 @@
1
- // Mirrors hyperclayjs/src/utilities/mutation.js _shouldIgnore. Returns true when
2
- // a mutation should be ignored: the node is (or sits inside) a browser-extension
3
- // injected element, an ancestor carries one of the four ignore attributes, or the
4
- // record is an extension marker attribute (e.g. a password-manager field tag).
5
- // `ignoreAttributePredicate` is an optional caller-supplied predicate for
6
- // additional filtering on attribute records.
1
+ // Decides whether a mutation should be ignored for undo. Mirrors hyperclayjs's
2
+ // region capability model: a mutation is ignored when its node sits in a region
3
+ // the resolver marks NOT undoable (no-undo / no-watch / the legacy markers /
4
+ // browser-extension content), when the record is an extension marker attribute,
5
+ // or when the caller's ignoreAttributePredicate rejects it.
6
+ //
7
+ // When paired with hyperclayjs we delegate the region decision to the SAME
8
+ // resolver the platform ships (window.hyperclay.region.resolveRegionPolicy), so
9
+ // undo and the platform can never drift. Standalone (no hyperclay on window) we
10
+ // fall back to a local marker walk that mirrors the resolver's `undoable` axis.
7
11
 
8
12
  import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from './extension-noise.js'
9
13
 
10
- const IGNORE_ATTRS = ['mutations-ignore', 'save-remove', 'save-ignore', 'save-freeze']
14
+ // Local fallback: attributes whose region is NOT undoable. The new no-save /
15
+ // no-trigger-autosave / freeze are deliberately ABSENT — those regions ARE
16
+ // undoable in the capability model; only no-undo / no-watch (and the legacy
17
+ // markers, which all imply no-undo) suppress recording.
18
+ const IGNORE_ATTRS = ['mutations-ignore', 'save-remove', 'save-ignore', 'save-freeze', 'no-undo', 'no-watch']
11
19
 
12
- export function shouldIgnore(node, ignoreAttributePredicate, record) {
13
- // For non-element nodes (like text nodes), start from parent.
14
- let el = (node && node.nodeType !== 1) ? node.parentElement : node
20
+ function sharedResolver() {
21
+ return (typeof window !== 'undefined' && window.hyperclay && window.hyperclay.region &&
22
+ typeof window.hyperclay.region.resolveRegionPolicy === 'function')
23
+ ? window.hyperclay.region.resolveRegionPolicy
24
+ : null
25
+ }
15
26
 
27
+ // Is the node in a region that should not be recorded by undo?
28
+ function regionNotUndoable(node) {
29
+ const resolve = sharedResolver()
30
+ if (resolve) {
31
+ try { return !resolve(node).undoable }
32
+ catch (_) { /* fall through to the local walk */ }
33
+ }
34
+ let el = (node && node.nodeType !== 1) ? node.parentElement : node
16
35
  // Browser-extension injected elements (and their descendants) are not page content.
17
36
  if (el && el.closest && el.closest(EXTENSION_NODE_SELECTOR)) return true
18
-
19
37
  while (el && el.nodeType === 1) {
20
38
  for (const attr of IGNORE_ATTRS) {
21
39
  if (el.hasAttribute && el.hasAttribute(attr)) return true
22
40
  }
23
41
  el = el.parentElement
24
42
  }
43
+ return false
44
+ }
45
+
46
+ export function shouldIgnore(node, ignoreAttributePredicate, record) {
47
+ if (regionNotUndoable(node)) return true
25
48
 
26
49
  if (record && record.type === 'attributes') {
27
50
  // Extension marker attributes on a real element (password-manager field tags) are noise.
package/src/scope.js CHANGED
@@ -30,7 +30,7 @@ export function createScope(opts) {
30
30
  function start() {
31
31
  if (started) return
32
32
  started = true
33
- observer = new MutationObserver(handleRecords)
33
+ observer = createRecordSource(handleRecords)
34
34
  observer.observe(config.scope, {
35
35
  childList: true,
36
36
  attributes: true,
@@ -42,6 +42,23 @@ export function createScope(opts) {
42
42
  log('started')
43
43
  }
44
44
 
45
+ // Choose the record source ONCE. Paired with hyperclayjs AND watching the
46
+ // document.body singleton, source records from the platform's single shared
47
+ // observer (Mutation.createObserver) so the page runs ONE MutationObserver.
48
+ // Otherwise — standalone, or a created shadow/CodeMirror scope — use a real
49
+ // MutationObserver. The result is duck-typed: only observe / disconnect /
50
+ // takeRecords are touched, and the shared hub observes with options identical
51
+ // to the ones above, so nothing here needs to change between the two paths.
52
+ function createRecordSource(handler) {
53
+ const isBodySingleton = typeof document !== 'undefined' && config.scope === document.body
54
+ const hub = (typeof window !== 'undefined' && window.hyperclay && window.hyperclay.Mutation) || null
55
+ if (isBodySingleton && hub && typeof hub.createObserver === 'function') {
56
+ log('sourcing records from window.hyperclay.Mutation (shared observer)')
57
+ return hub.createObserver(handler)
58
+ }
59
+ return new MutationObserver(handler)
60
+ }
61
+
45
62
  function stop() {
46
63
  if (!started) return
47
64
  started = false
@@ -65,7 +82,12 @@ export function createScope(opts) {
65
82
  // --- Observer callback ---
66
83
 
67
84
  function handleRecords(records) {
68
- if (pauseDepth > 0) return // records remain in observer buffer until takeRecords() is called
85
+ // While paused, these callback-delivered records are DROPPED — they have
86
+ // already left the observer's buffer, so they're gone. resume()'s
87
+ // takeRecords() separately purges only the remainder that was never
88
+ // delivered. Those two halves together are the pause contract, and the
89
+ // shimmed (paired) path reproduces both.
90
+ if (pauseDepth > 0) return
69
91
  const prims = convertRecords(records)
70
92
  if (prims.length > 0) {
71
93
  for (const p of prims) pendingPrimitives.push(p)