hyper-undo 0.3.0 → 0.5.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,28 @@
1
1
  # hyper-undo changelog
2
2
 
3
+ ## [0.5.0] - 2026-08-12
4
+
5
+ ### Added
6
+ - Script to copy the built bundle to clayjs
7
+ - `kind`, `status`, and `url` declarations in the hyper key
8
+
9
+ ### Changed
10
+ - Platform namespace is now read off `window.clay` in addition to `hyperclay`
11
+ - Updated hyper-undo
12
+
13
+ ### Fixed
14
+ - Documentation that blocked outside users
15
+
16
+
17
+
18
+ ## [0.4.0] - 2026-06-16
19
+
20
+ ### Changed
21
+ - Integrated the region capability model into hyper-undo
22
+ - Adopted a shared MutationObserver in hyper-undo
23
+
24
+
25
+
3
26
  ## [0.3.0] - 2026-06-07
4
27
 
5
28
 
@@ -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(){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);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-undo",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "DOM-state undo/redo via MutationObserver inverse-op replay. Standalone or paired with hyperclayjs.",
5
5
  "keywords": [
6
6
  "undo",
@@ -15,6 +15,10 @@
15
15
  "url": "https://github.com/panphora/hyper-undo/issues"
16
16
  },
17
17
  "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/panphora/hyper-undo.git"
21
+ },
18
22
  "author": "David Miranda",
19
23
  "type": "module",
20
24
  "main": "src/index.js",
@@ -32,13 +36,20 @@
32
36
  "scripts": {
33
37
  "build": "npx esbuild src/index.js --bundle --minify --format=iife --global-name=hyperundo --outfile=dist/hyper-undo.min.js",
34
38
  "copy-to-hyperclayjs": "npm run build && node scripts/copy-to-hyperclayjs.js",
39
+ "copy-to-clayjs": "npm run build && node scripts/copy-to-clayjs.js",
35
40
  "test:node": "node --test 'test-node/**/*.test.js'",
36
- "test": "npm run test:node",
41
+ "test:shimmed": "HYPER_UNDO_SHIMMED=1 node --test 'test-node/**/*.test.js'",
42
+ "test": "npm run test:node && npm run test:shimmed",
37
43
  "release": "./scripts/release.sh",
38
44
  "prepublishOnly": "npm run build && npm test"
39
45
  },
40
46
  "devDependencies": {
41
47
  "esbuild": "^0.25.0",
42
48
  "jsdom": "^25.0.0"
49
+ },
50
+ "hyper": {
51
+ "kind": "lib",
52
+ "status": "active",
53
+ "url": "https://github.com/panphora/hyper-undo"
43
54
  }
44
55
  }
@@ -52,7 +52,11 @@ export function stripExtensionNoise(root) {
52
52
  if (!root || !root.querySelectorAll) return
53
53
  try {
54
54
  for (const el of root.querySelectorAll(EXTENSION_NODE_SELECTOR)) el.remove()
55
- for (const el of root.querySelectorAll('*')) {
55
+ // querySelectorAll('*') never returns the root itself, so an extension stamp on
56
+ // <html> used to ride into every saved file. The root is the one element an
57
+ // extension can mark on a page with no other match.
58
+ for (const el of [root, ...root.querySelectorAll('*')]) {
59
+ if (!el.attributes) continue
56
60
  for (const attr of [...el.attributes]) {
57
61
  if (EXTENSION_ATTR_PATTERN.test(attr.name.toLowerCase())) el.removeAttribute(attr.name)
58
62
  }
package/src/filter.js CHANGED
@@ -1,27 +1,52 @@
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 a platform client we delegate the region decision to the SAME
8
+ // resolver it ships (clay.region.resolveRegionPolicy under clayjs,
9
+ // hyperclay.region.resolveRegionPolicy under hyperclayjs), so undo and the platform
10
+ // can never drift. Standalone (neither on window) we fall back to a local marker
11
+ // walk that mirrors the resolver's `undoable` axis.
7
12
 
8
13
  import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from './extension-noise.js'
9
14
 
10
- const IGNORE_ATTRS = ['mutations-ignore', 'save-remove', 'save-ignore', 'save-freeze']
15
+ // Local fallback: attributes whose region is NOT undoable. The new no-save /
16
+ // no-trigger-autosave / freeze are deliberately ABSENT — those regions ARE
17
+ // undoable in the capability model; only no-undo / no-watch (and the legacy
18
+ // markers, which all imply no-undo) suppress recording.
19
+ const IGNORE_ATTRS = ['mutations-ignore', 'save-remove', 'save-ignore', 'save-freeze', 'no-undo', 'no-watch']
11
20
 
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
21
+ function sharedResolver() {
22
+ if (typeof window === 'undefined') return null
23
+ const region = window.clay?.region || window.hyperclay?.region
24
+ return region && typeof region.resolveRegionPolicy === 'function'
25
+ ? region.resolveRegionPolicy
26
+ : null
27
+ }
15
28
 
29
+ // Is the node in a region that should not be recorded by undo?
30
+ function regionNotUndoable(node) {
31
+ const resolve = sharedResolver()
32
+ if (resolve) {
33
+ try { return !resolve(node).undoable }
34
+ catch (_) { /* fall through to the local walk */ }
35
+ }
36
+ let el = (node && node.nodeType !== 1) ? node.parentElement : node
16
37
  // Browser-extension injected elements (and their descendants) are not page content.
17
38
  if (el && el.closest && el.closest(EXTENSION_NODE_SELECTOR)) return true
18
-
19
39
  while (el && el.nodeType === 1) {
20
40
  for (const attr of IGNORE_ATTRS) {
21
41
  if (el.hasAttribute && el.hasAttribute(attr)) return true
22
42
  }
23
43
  el = el.parentElement
24
44
  }
45
+ return false
46
+ }
47
+
48
+ export function shouldIgnore(node, ignoreAttributePredicate, record) {
49
+ if (regionNotUndoable(node)) return true
25
50
 
26
51
  if (record && record.type === 'attributes') {
27
52
  // 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,24 @@ 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' &&
55
+ (window.clay?.Mutation || window.hyperclay?.Mutation)) || null
56
+ if (isBodySingleton && hub && typeof hub.createObserver === 'function') {
57
+ log('sourcing records from the platform Mutation hub (shared observer)')
58
+ return hub.createObserver(handler)
59
+ }
60
+ return new MutationObserver(handler)
61
+ }
62
+
45
63
  function stop() {
46
64
  if (!started) return
47
65
  started = false
@@ -65,7 +83,12 @@ export function createScope(opts) {
65
83
  // --- Observer callback ---
66
84
 
67
85
  function handleRecords(records) {
68
- if (pauseDepth > 0) return // records remain in observer buffer until takeRecords() is called
86
+ // While paused, these callback-delivered records are DROPPED — they have
87
+ // already left the observer's buffer, so they're gone. resume()'s
88
+ // takeRecords() separately purges only the remainder that was never
89
+ // delivered. Those two halves together are the pause contract, and the
90
+ // shimmed (paired) path reproduces both.
91
+ if (pauseDepth > 0) return
69
92
  const prims = convertRecords(records)
70
93
  if (prims.length > 0) {
71
94
  for (const p of prims) pendingPrimitives.push(p)