hyper-undo 0.1.0 → 0.3.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 +13 -0
- package/README.md +1 -1
- package/dist/hyper-undo.min.js +1 -1
- package/package.json +1 -1
- package/src/extension-noise.js +63 -0
- package/src/filter.js +21 -8
- package/src/index.js +4 -0
- package/src/primitives.js +9 -0
- package/src/scope.js +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# hyper-undo changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-06-07
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## 0.2.0 — unreleased
|
|
8
|
+
|
|
9
|
+
- **BREAKING:** the generic `change` event is removed and replaced by dedicated,
|
|
10
|
+
self-describing events: `undo` and `redo` (fire after a history navigate),
|
|
11
|
+
`commit` (after a new commit), and `clear` (after a reset). The event name is
|
|
12
|
+
the signal; payloads are unchanged (none). Subscribe with
|
|
13
|
+
`undo.on('undo', fn)` etc. A react-to-all consumer subscribes to all four.
|
|
14
|
+
Migration: replace `undo.on('change', fn)` with the specific event(s) you need.
|
|
15
|
+
|
|
3
16
|
## 0.1.0 — unreleased
|
|
4
17
|
|
|
5
18
|
Initial release. DOM-state undo/redo via MutationObserver inverse-op replay.
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ the box.
|
|
|
68
68
|
| `undo.undo()` / `undo.redo()` | navigate history |
|
|
69
69
|
| `undo.clear()` | clear both stacks |
|
|
70
70
|
| `undo.pause()` / `undo.resume()` | recorder skips while paused |
|
|
71
|
-
| `undo.on('
|
|
71
|
+
| `undo.on('undo'\|'redo'\|'commit'\|'clear', fn)` / `undo.off(...)` | subscribe to lifecycle events; `undo`/`redo` fire after a navigate, `commit` after a new commit, `clear` after a reset. (No generic `change` event.) |
|
|
72
72
|
| `undo.canUndo` / `undo.canRedo` | booleans (getters) |
|
|
73
73
|
| `undo.history` | `[{ label, timestamp }, ...]`, oldest first; `timestamp` is `Date.now()` millis |
|
|
74
74
|
| `undo.isPaused` | boolean (getter) |
|
package/dist/hyper-undo.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var hyperundo=(()=>{var x=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var Y=(e,r)=>{for(var o in r)x(e,o,{get:r[o],enumerable:!0})},$=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of Q(r))!X.call(e,n)&&n!==o&&x(e,n,{get:()=>r[n],enumerable:!(t=J(r,n))||t.enumerable});return e};var ee=e=>$(x({},"__esModule",{value:!0}),e);var ie={};Y(ie,{default:()=>oe,undo:()=>V});function te(e,r=null){let o=[];if(e.type==="attributes"){let t=e.target,n=e.attributeName,i=e.oldValue,a=t.getAttribute(n);return i==null&&a!=null?o.push({kind:"attr-add",target:t,name:n,newValue:a}):i!=null&&a==null?o.push({kind:"attr-remove",target:t,name:n,oldValue:i}):i!==a&&o.push({kind:"attr-set",target:t,name:n,oldValue:i,newValue:a}),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(u=>!r(u)):Array.from(e.addedNodes),a=r?Array.from(e.removedNodes).filter(u=>!r(u)):Array.from(e.removedNodes);return i.length>0&&o.push({kind:"add",parent:t,nodes:i,before:n}),a.length>0&&o.push({kind:"remove",parent:t,nodes:a,before:n}),o}return o}function S(e,r=null){let o=[],t=new Map;for(let n of e){if(n.type==="attributes"){let i=n.target,a=n.attributeName,u=t.get(i);u||(u=new Map,t.set(i,u)),u.has(a)||(u.set(a,{idx:o.length,oldValue:n.oldValue}),o.push(null));continue}for(let i of te(n,r))o.push(i)}for(let[n,i]of t)for(let[a,u]of i){let l=u.oldValue,p=n.getAttribute(a),f=null;l===p?f=null:l==null?f={kind:"attr-add",target:n,name:a,newValue:p}:p==null?f={kind:"attr-remove",target:n,name:a,oldValue:l}:f={kind:"attr-set",target:n,name:a,oldValue:l,newValue:p},o[u.idx]=f}return o.filter(n=>n!=null)}function D(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"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 N(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"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 re=["mutations-ignore","save-remove","save-ignore","save-freeze"];function C(e,r,o){let t=e&&e.nodeType!==1?e.parentElement:e;for(;t&&t.nodeType===1;){for(let n of re)if(t.hasAttribute&&t.hasAttribute(n))return!0;t=t.parentElement}if(r&&o&&o.type==="attributes")try{if(r(o.attributeName,o.target))return!0}catch{}return!1}function L(){let e=new Map;function r(n,i){let a=e.get(n);return a||(a=new Set,e.set(n,a)),a.add(i),()=>o(n,i)}function o(n,i){let a=e.get(n);a&&a.delete(i)}function t(n,i){let a=e.get(n);if(a)for(let u of Array.from(a))try{u(i)}catch(l){console.error("[hyper-undo] listener threw",l)}}return{on:r,off:o,emit:t}}var ne={maxHistory:100,idleWindowMs:500,idleLabel:"Edit",ignoreAttribute:null,debug:!1};function E(e){let r={...ne,...e};if(!r.scope)throw new Error("hyper-undo: scope is required");let o=L(),t=[],n=[],i=[],a=null,u=null,l=0,p=!1;function f(...d){r.debug&&console.log("[hyper-undo]",...d)}function F(){p||(p=!0,u=new MutationObserver(y),u.observe(r.scope,{childList:!0,attributes:!0,characterData:!0,subtree:!0,attributeOldValue:!0,characterDataOldValue:!0}),f("started"))}function b(){p&&(p=!1,u&&(u.disconnect(),u=null),g(),t.length=0,n.length=0,i=[],l=0,f("stopped"))}function T(){let d=r.scope;return!!(d&&d.nodeType===1&&d.isConnected===!1)}function y(d){if(l>0)return;let c=I(d);if(c.length>0){for(let m of c)i.push(m);z()}}function I(d){let c=d.filter(m=>!C(m.target,r.ignoreAttribute,m));return S(c,m=>C(m,r.ignoreAttribute))}function z(){g(),a=setTimeout(P,r.idleWindowMs)}function g(){a!=null&&(clearTimeout(a),a=null)}function P(){if(a=null,i.length===0)return;let d=i;i=[],v(r.idleLabel,d)}function v(d,c){for(t.push({label:d,timestamp:Date.now(),primitives:c}),n.length=0;t.length>r.maxHistory;)t.shift();f("commit",d,"primitives:",c.length),o.emit("change")}function B(d,c){if(!p)throw new Error("hyper-undo: scope not started");y(u.takeRecords()),w();let m=c();if(m&&typeof m.then=="function")throw new Error("hyper-undo: commit() fn must be synchronous; returned a Promise");if(y(u.takeRecords()),i.length===0){o.emit("change");return}g();let j=i;i=[],v(d,j)}function H(d){if(!p)throw new Error("hyper-undo: scope not started");if(!u)return;if(l>1){u.takeRecords();return}w();let c=I(u.takeRecords());c.length!==0&&(g(),v(d,c))}function W(){u&&u.takeRecords()}function w(){l===0&&u&&y(u.takeRecords()),i.length!==0&&(g(),P())}function k(){l===0&&u&&y(u.takeRecords()),l++,f("paused",l)}function A(){l!==0&&(l--,l===0&&u&&u.takeRecords(),f("resumed",l))}function q(){if(T()){b();return}w();let d=t.pop();if(d){k();try{for(let c=d.primitives.length-1;c>=0;c--)try{N(d.primitives[c])}catch(m){f("undo primitive failed (continuing)",m)}}finally{A()}n.push(d),f("undo",d.label),o.emit("change")}}function G(){if(T()){b();return}w();let d=n.pop();if(d){k();try{for(let c of d.primitives)try{D(c)}catch(m){f("redo primitive failed (continuing)",m)}}finally{A()}t.push(d),f("redo",d.label),o.emit("change")}}function Z(){u&&u.takeRecords(),t.length=0,n.length=0,i=[],g(),f("cleared"),o.emit("change")}return{_config:r,start:F,stop:b,commit:B,commitCaptured:H,discardCaptured:W,flush:w,undo:q,redo:G,clear:Z,pause:k,resume:A,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(d=>({label:d.label,timestamp:d.timestamp}))},get scope(){return r.scope}}}function _(e,r){let o=t=>{let n=e.scope;if(n&&n.nodeType===1&&n.isConnected===!1){r&&r();return}let i=t.target,a=e._config.shadowKeydownIn;if(i&&i.closest&&Array.isArray(a))for(let p of a)try{if(i.closest(p))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 K=[".CodeMirror",".cm-editor",".monaco-editor",".ace_editor",".ql-editor",".tiptap",".ProseMirror"],s=null,R=null,M=null,h=null;function U(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=_(e,r);return h={inst:e,cleanup:o},o}function O(e){h&&h.inst===e&&(h.cleanup(),h=null)}var V={defaults:{shadowKeydownIn:K},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(s){if(R===r)return console.warn("[hyper-undo] start() called again on existing singleton; ignoring new options"),s;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||K};return s=E(o),s.start(),R=r,e.bindKeys!==!1&&(M=U(s,()=>V.stop())),s},stop(){s&&(O(s),M=null,s.stop(),s=null,R=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||K},t=E(o),n=null,i={_config:t._config,commit:t.commit,commitCaptured:t.commitCaptured,discardCaptured:t.discardCaptured,flush:t.flush,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=U(t,()=>i.stop())),t.start()},stop(){O(t),n=null,t.stop()}};return i},commit(e,r){return s?s.commit(e,r):void 0},commitCaptured(e){return s?s.commitCaptured(e):void 0},discardCaptured(){return s?s.discardCaptured():void 0},undo(){return s?s.undo():void 0},redo(){return s?s.redo():void 0},clear(){return s?s.clear():void 0},pause(){return s?s.pause():void 0},resume(){return s?s.resume():void 0},flush(){return s?s.flush():void 0},on(e,r){return s?s.on(e,r):void 0},off(e,r){return s?s.off(e,r):void 0},get canUndo(){return s?s.canUndo:!1},get canRedo(){return s?s.canRedo:!1},get history(){return s?s.history:[]},get isPaused(){return s?s.isPaused:!1}};var oe=V;return ee(ie);})();
|
|
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);})();
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extension-noise.js — browser-extension DOM noise the page should never own.
|
|
3
|
+
*
|
|
4
|
+
* Extensions touch the live page in two distinct ways, handled differently so a
|
|
5
|
+
* save never loses real content:
|
|
6
|
+
* 1. Standalone elements they own (password-manager menus, Grammarly overlays,
|
|
7
|
+
* translate banners, extension iframes/scripts) — remove them entirely.
|
|
8
|
+
* 2. Marker attributes they stamp onto YOUR real inputs/forms — strip just the
|
|
9
|
+
* attribute and keep the element.
|
|
10
|
+
*
|
|
11
|
+
* The selectors are not stable public APIs; extend both lists from real captures.
|
|
12
|
+
* Keep this file byte-identical to the copy in the other package (hyperclayjs and
|
|
13
|
+
* hyper-undo each carry one) — their save/undo observers must agree, or one will
|
|
14
|
+
* record/save what the other ignores.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Standalone elements browser extensions inject into the page — remove them entirely.
|
|
18
|
+
export const EXTENSION_NODE_SELECTORS = [
|
|
19
|
+
'[src^="chrome-extension://"]', '[href^="chrome-extension://"]',
|
|
20
|
+
'[src^="moz-extension://"]', '[href^="moz-extension://"]',
|
|
21
|
+
'[src^="safari-web-extension://"]', '[href^="safari-web-extension://"]',
|
|
22
|
+
'[id="1p-menu-live-region"]',
|
|
23
|
+
'com-1password-button', 'com-1password-menu', 'com-1password-notification',
|
|
24
|
+
'grammarly-extension', 'grammarly-desktop-integration',
|
|
25
|
+
'lt-div', 'lt-card', 'lt-toolbar', 'lt-highlighter',
|
|
26
|
+
'[data-lastpass-root]', '[data-lastpass-icon-root]', '[data-dashlane-shadowhost]',
|
|
27
|
+
'[data-klarna-extension]', '[class*="klarna-extension"]',
|
|
28
|
+
'.skiptranslate', '#goog-gt-tt', '.goog-te-banner-frame', '.goog-te-balloon-frame',
|
|
29
|
+
'style.darkreader', 'style[data-darkreader-mode]', 'style[data-darkreader-scheme]',
|
|
30
|
+
'#loom-companion-mv3', 'loom-container',
|
|
31
|
+
'[ext-id]',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Attribute-name prefixes extensions stamp onto YOUR real elements — strip the attribute, keep the element.
|
|
35
|
+
export const EXTENSION_ATTR_PREFIXES = [
|
|
36
|
+
'data-grammarly', 'data-lt', 'data-1p', 'data-com-onepassword', 'data-lastpass', 'data-lp', 'data-kwimpala',
|
|
37
|
+
'data-bitwarden', 'data-bw', 'data-dashlane', 'data-np', 'data-protonpass', 'data-proton-pass',
|
|
38
|
+
'data-keeper', 'data-rf', 'data-roboform', 'data-honey', 'data-rakuten', 'data-capital-one',
|
|
39
|
+
'data-wikibuy', 'data-klarna', 'data-darkreader', 'data-evernote', 'data-pocket', 'data-pin',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// Pre-joined once so hot paths call matches()/closest()/querySelectorAll() without re-joining.
|
|
43
|
+
export const EXTENSION_NODE_SELECTOR = EXTENSION_NODE_SELECTORS.join(',')
|
|
44
|
+
|
|
45
|
+
// One compiled test for "is this an extension marker attribute". The (?:-|$) boundary means a
|
|
46
|
+
// prefix like `data-lt` matches `data-lt` / `data-lt-foo` but never an app attribute like `data-ltr`.
|
|
47
|
+
export const EXTENSION_ATTR_PATTERN = new RegExp('^(?:' + EXTENSION_ATTR_PREFIXES.join('|') + ')(?:-|$)')
|
|
48
|
+
|
|
49
|
+
// Remove extension-injected elements and strip extension marker attributes from a (cloned) subtree, in place.
|
|
50
|
+
// Guarded so a malformed denylist entry degrades to "noise not stripped" rather than breaking the save.
|
|
51
|
+
export function stripExtensionNoise(root) {
|
|
52
|
+
if (!root || !root.querySelectorAll) return
|
|
53
|
+
try {
|
|
54
|
+
for (const el of root.querySelectorAll(EXTENSION_NODE_SELECTOR)) el.remove()
|
|
55
|
+
for (const el of root.querySelectorAll('*')) {
|
|
56
|
+
for (const attr of [...el.attributes]) {
|
|
57
|
+
if (EXTENSION_ATTR_PATTERN.test(attr.name.toLowerCase())) el.removeAttribute(attr.name)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (typeof console !== 'undefined') console.warn('[hyperclay] stripExtensionNoise skipped:', e)
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/filter.js
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
|
-
// Mirrors hyperclayjs/src/utilities/mutation.js _shouldIgnore
|
|
2
|
-
//
|
|
3
|
-
// one of the four ignore attributes
|
|
4
|
-
//
|
|
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.
|
|
7
|
+
|
|
8
|
+
import { EXTENSION_NODE_SELECTOR, EXTENSION_ATTR_PATTERN } from './extension-noise.js'
|
|
5
9
|
|
|
6
10
|
const IGNORE_ATTRS = ['mutations-ignore', 'save-remove', 'save-ignore', 'save-freeze']
|
|
7
11
|
|
|
8
12
|
export function shouldIgnore(node, ignoreAttributePredicate, record) {
|
|
9
13
|
// For non-element nodes (like text nodes), start from parent.
|
|
10
14
|
let el = (node && node.nodeType !== 1) ? node.parentElement : node
|
|
15
|
+
|
|
16
|
+
// Browser-extension injected elements (and their descendants) are not page content.
|
|
17
|
+
if (el && el.closest && el.closest(EXTENSION_NODE_SELECTOR)) return true
|
|
18
|
+
|
|
11
19
|
while (el && el.nodeType === 1) {
|
|
12
20
|
for (const attr of IGNORE_ATTRS) {
|
|
13
21
|
if (el.hasAttribute && el.hasAttribute(attr)) return true
|
|
14
22
|
}
|
|
15
23
|
el = el.parentElement
|
|
16
24
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
|
|
26
|
+
if (record && record.type === 'attributes') {
|
|
27
|
+
// Extension marker attributes on a real element (password-manager field tags) are noise.
|
|
28
|
+
if (record.attributeName && EXTENSION_ATTR_PATTERN.test(record.attributeName.toLowerCase())) return true
|
|
29
|
+
if (ignoreAttributePredicate) {
|
|
30
|
+
try {
|
|
31
|
+
if (ignoreAttributePredicate(record.attributeName, record.target)) return true
|
|
32
|
+
} catch (_) {}
|
|
33
|
+
}
|
|
21
34
|
}
|
|
22
35
|
return false
|
|
23
36
|
}
|
package/src/index.js
CHANGED
|
@@ -97,6 +97,8 @@ const undo = {
|
|
|
97
97
|
commitCaptured: inst.commitCaptured,
|
|
98
98
|
discardCaptured: inst.discardCaptured,
|
|
99
99
|
flush: inst.flush,
|
|
100
|
+
record: inst.record,
|
|
101
|
+
recordValue: inst.recordValue,
|
|
100
102
|
undo: inst.undo,
|
|
101
103
|
redo: inst.redo,
|
|
102
104
|
clear: inst.clear,
|
|
@@ -129,6 +131,8 @@ const undo = {
|
|
|
129
131
|
commit(label, fn) { return singleton ? singleton.commit(label, fn) : undefined },
|
|
130
132
|
commitCaptured(label) { return singleton ? singleton.commitCaptured(label) : undefined },
|
|
131
133
|
discardCaptured() { return singleton ? singleton.discardCaptured() : undefined },
|
|
134
|
+
record(p) { return singleton ? singleton.record(p) : undefined },
|
|
135
|
+
recordValue(t, o) { return singleton ? singleton.recordValue(t, o) : undefined },
|
|
132
136
|
undo() { return singleton ? singleton.undo() : undefined },
|
|
133
137
|
redo() { return singleton ? singleton.redo() : undefined },
|
|
134
138
|
clear() { return singleton ? singleton.clear() : undefined },
|
package/src/primitives.js
CHANGED
|
@@ -128,6 +128,12 @@ export function replayForward(p) {
|
|
|
128
128
|
case 'text':
|
|
129
129
|
p.target.data = p.newValue
|
|
130
130
|
return
|
|
131
|
+
case 'value':
|
|
132
|
+
// An element PROPERTY write (e.g. input.value / checkbox.checked). These
|
|
133
|
+
// fire no MutationRecord, so they never arrive via the observer — a caller
|
|
134
|
+
// records them explicitly through scope.recordValue().
|
|
135
|
+
p.target[p.prop] = p.newValue
|
|
136
|
+
return
|
|
131
137
|
case 'add':
|
|
132
138
|
for (const node of p.nodes) {
|
|
133
139
|
if (p.before && p.before.parentNode === p.parent) {
|
|
@@ -162,6 +168,9 @@ export function replayReverse(p) {
|
|
|
162
168
|
case 'text':
|
|
163
169
|
p.target.data = p.oldValue
|
|
164
170
|
return
|
|
171
|
+
case 'value':
|
|
172
|
+
p.target[p.prop] = p.oldValue
|
|
173
|
+
return
|
|
165
174
|
case 'add':
|
|
166
175
|
// forward was add → reverse is remove
|
|
167
176
|
for (const node of p.nodes) {
|
package/src/scope.js
CHANGED
|
@@ -103,7 +103,7 @@ export function createScope(opts) {
|
|
|
103
103
|
redoStack.length = 0
|
|
104
104
|
while (undoStack.length > config.maxHistory) undoStack.shift()
|
|
105
105
|
log('commit', label, 'primitives:', primitives.length)
|
|
106
|
-
emitter.emit('
|
|
106
|
+
emitter.emit('commit')
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// Explicit commit: caller-driven, synchronous fn.
|
|
@@ -122,7 +122,7 @@ export function createScope(opts) {
|
|
|
122
122
|
// Drain records the observer hasn't fired callback for yet.
|
|
123
123
|
handleRecords(observer.takeRecords())
|
|
124
124
|
if (pendingPrimitives.length === 0) {
|
|
125
|
-
emitter.emit('
|
|
125
|
+
emitter.emit('commit')
|
|
126
126
|
return
|
|
127
127
|
}
|
|
128
128
|
clearIdleTimer()
|
|
@@ -155,6 +155,26 @@ export function createScope(opts) {
|
|
|
155
155
|
if (observer) observer.takeRecords()
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// --- Manual recording (for changes the observer can't see) ---
|
|
159
|
+
|
|
160
|
+
// Record a primitive the MutationObserver never delivers — an element PROPERTY
|
|
161
|
+
// write (input.value, checkbox.checked) fires no MutationRecord. The primitive
|
|
162
|
+
// joins the same idle batch as observed edits, so rapid same-field property
|
|
163
|
+
// writes coalesce into one commit exactly like text edits do. No-op while
|
|
164
|
+
// stopped or paused (a paused caller is inside an undo/redo replay or an
|
|
165
|
+
// exclusion frame, where injected primitives would be wrong).
|
|
166
|
+
function record(primitive) {
|
|
167
|
+
if (!started || pauseDepth > 0 || !primitive) return
|
|
168
|
+
pendingPrimitives.push(primitive)
|
|
169
|
+
restartIdleTimer()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Convenience for the common case: a value/checked property write on an element.
|
|
173
|
+
function recordValue(target, { prop = 'value', oldValue, newValue } = {}) {
|
|
174
|
+
if (!target || oldValue === newValue) return
|
|
175
|
+
record({ kind: 'value', target, prop, oldValue, newValue })
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
// Force-close current idle batch as its own commit. Used by undo()/redo() and
|
|
159
179
|
// by the save seam (snapshot.js calls undo.flush() before cloning).
|
|
160
180
|
function flush() {
|
|
@@ -206,7 +226,7 @@ export function createScope(opts) {
|
|
|
206
226
|
} finally { resume() }
|
|
207
227
|
redoStack.push(c)
|
|
208
228
|
log('undo', c.label)
|
|
209
|
-
emitter.emit('
|
|
229
|
+
emitter.emit('undo')
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
function redo() {
|
|
@@ -223,7 +243,7 @@ export function createScope(opts) {
|
|
|
223
243
|
} finally { resume() }
|
|
224
244
|
undoStack.push(c)
|
|
225
245
|
log('redo', c.label)
|
|
226
|
-
emitter.emit('
|
|
246
|
+
emitter.emit('redo')
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
function clear() {
|
|
@@ -236,7 +256,7 @@ export function createScope(opts) {
|
|
|
236
256
|
pendingPrimitives = []
|
|
237
257
|
clearIdleTimer()
|
|
238
258
|
log('cleared')
|
|
239
|
-
emitter.emit('
|
|
259
|
+
emitter.emit('clear')
|
|
240
260
|
}
|
|
241
261
|
|
|
242
262
|
// --- Public surface for the singleton wrapper ---
|
|
@@ -245,6 +265,7 @@ export function createScope(opts) {
|
|
|
245
265
|
_config: config, // private: used by keys.js to read shadowKeydownIn
|
|
246
266
|
start, stop,
|
|
247
267
|
commit, commitCaptured, discardCaptured, flush,
|
|
268
|
+
record, recordValue,
|
|
248
269
|
undo, redo, clear,
|
|
249
270
|
pause, resume,
|
|
250
271
|
on: emitter.on, off: emitter.off,
|