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 +23 -0
- package/dist/hyper-undo.min.js +1 -1
- package/package.json +13 -2
- package/src/extension-noise.js +5 -1
- package/src/filter.js +36 -11
- package/src/scope.js +25 -2
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
|
|
package/dist/hyper-undo.min.js
CHANGED
|
@@ -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
|
+
"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": "
|
|
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
|
}
|
package/src/extension-noise.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
2
|
-
// a mutation
|
|
3
|
-
//
|
|
4
|
-
// record is an extension marker attribute
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 =
|
|
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
|
-
|
|
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)
|