@panphora/clayjs 1.0.0 → 1.2.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/README.md +23 -0
- package/dist/clay.standalone.js +22327 -0
- package/entries/clay-data.js +1 -1
- package/package.json +9 -2
- package/src/core/etag.js +127 -0
- package/src/core/host-attrs.js +58 -3
- package/src/core/host-meta.js +20 -3
- package/src/core/is-edit-mode.js +14 -5
- package/src/core/save-conflict-notice.js +191 -0
- package/src/core/save-core.js +329 -7
- package/src/core/save.js +98 -4
- package/src/core/snapshot.js +38 -17
- package/src/core/stale-host-notice.js +95 -0
- package/src/lib/hostile-css.js +49 -0
- package/src/lib/root-attrs.js +62 -7
- package/src/loader-logic.js +44 -0
- package/src/loader.js +11 -5
- package/src/plugins/indicator.js +13 -2
- package/src/plugins/sortable.js +5 -5
- package/src/standalone.js +98 -0
- package/src/sync/live-sync.js +208 -15
- package/src/ui/index.js +7 -0
- package/src/vendor/hypercms.vendor.js +13 -13
- package/src/lib/load-vendor-script.js +0 -57
package/entries/clay-data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// hyper-html-api/dist/hyper-html-api.engine.min.js via hyper-html-api
|
|
3
3
|
// `npm run copy-to-clayjs`. Edit the hyper-html-api source and re-run.
|
|
4
4
|
(function () {
|
|
5
|
-
var hyperHtmlApiData=(()=>{var U=Object.defineProperty;var pt=Object.getOwnPropertyDescriptor;var dt=Object.getOwnPropertyNames;var mt=Object.prototype.hasOwnProperty;var tt=(t,e)=>{for(var n in e)U(t,n,{get:e[n],enumerable:!0})},gt=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of dt(e))!mt.call(t,r)&&r!==n&&U(t,r,{get:()=>e[r],enumerable:!(o=pt(e,r))||o.enumerable});return t};var yt=t=>gt(U({},"__esModule",{value:!0}),t);var jt={};tt(jt,{applyData:()=>Y,default:()=>Ct,engine:()=>L,extractData:()=>K});var j=["textContent","innerText","innerHTML","outerHTML","value","checked","selected","disabled","readOnly","type","tagName","nodeName","nodeType","nodeValue","childElementCount","id","className","classList","baseURI","offsetWidth","offsetHeight","clientWidth","clientHeight","scrollWidth","scrollHeight","dataset","currentSrc","duration","paused","title","documentURI","contentType"],q=new Set(j),et=new Set(["textContent","innerText","innerHTML","value","checked","selected","disabled","readOnly","type","id","className","title"]),nt=new Set(["tagName","nodeName","nodeType","nodeValue","childElementCount","classList","baseURI","documentURI","contentType","offsetWidth","offsetHeight","clientWidth","clientHeight","scrollWidth","scrollHeight","currentSrc","duration","paused","dataset"]);var $={};tt($,{EmptyListInsert:()=>w,MAX_RULE_DEPTH:()=>O,MaxRuleDepthExceeded:()=>y,RuleTargetReadOnly:()=>M,RulesParseError:()=>A,ShapeMismatch:()=>N,UnknownRulesVersion:()=>b});var A=class extends Error{constructor(e,n){super(e),this.name="RulesParseError",this.cause=n}},b=class extends Error{constructor(e){super(`unknown rules version: ${e}. Library supports "1".`),this.name="UnknownRulesVersion",this.version=e}},O=20,y=class extends Error{constructor(e){super(`rule depth exceeded ${O} at path: ${e.join(".")}`),this.name="MaxRuleDepthExceeded",this.path=e}},N=class extends Error{constructor(e){super(`shape mismatch: ${e.length} field(s) failed validation`),this.name="ShapeMismatch",this.mismatches=e}},w=class extends Error{constructor(e){super(`cannot add items to empty list at "${e.join(".")}" \u2014 no sibling to clone as template. Seed the list with a hidden item first.`),this.name="EmptyListInsert",this.path=e}},M=class extends Error{constructor(e){super(`cannot write to read-only DOM property "${e}"`),this.name="RuleTargetReadOnly",this.target=e}};function m(t){let e=0,n=null,o=-1;for(let r=0;r<t.length;r++){let i=t[r];i==="\\"?r++:n?i===n&&(n=null):i==='"'||i==="'"?n=i:i==="["||i==="("?e++:i==="]"||i===")"?e>0&&e--:i==="@"&&e===0&&(o=r)}return o}function v(t){let e=m(t);return e===-1?{selector:t,prop:null}:{selector:t.slice(0,e),prop:t.slice(e+1)||null}}function g(t,e,n,o={}){return J(t,e,n,{depth:0,path:[]},o)}function J(t,e,n,o,r){if(o.depth>O)throw new y(o.path);if(typeof n=="string")return Et(t,e,n,r);if(Array.isArray(n)){let[i,c]=n;return t.find(e,i,r).map((l,f)=>J(t,l,c,{depth:o.depth+1,path:[...o.path,f]},r))}if(typeof n=="object"&&n!==null){let i={};for(let[c,s]of Object.entries(n))i[c]=J(t,e,s,{depth:o.depth+1,path:[...o.path,c]},r);return i}return null}function Et(t,e,n,o){if(n.endsWith("[]")){let c=n.slice(0,-2);return t.find(e,c,o).map(s=>t.text(s))}if(n.startsWith("@"))return rt(t,e,n.slice(1));let r=m(n);if(r!==-1){let c=n.slice(0,r),s=n.slice(r+1),l=c?t.find(e,c,o):[e];return l.length===0?null:rt(t,l[0],s)}if(n===".")return t.text(e);let i=t.find(e,n,o);return i.length===0?null:t.text(i[0])}function rt(t,e,n){if(q.has(n)){let r=t.prop(e,n);return r==null?null:String(r)}let o=t.attr(e,n);return o||null}var Rt=.5;function F(t,e,n,o,r,i,c,s={}){let l=t.find(e,n,s);if(r.length===0){l.forEach(p=>t.remove(p));return}let f=r.length>l.length,u=l[0]||null;if(f&&!u&&(u=xt(t,e,n,s),!u))throw new w(i.path);let a=l.map(p=>At(t,p,o,s)),h=null;if(u){h=t.clone(u),s.templateAttr&&t.removeAttr(h,s.templateAttr);let p=t.stripIds(h);p>0&&console.warn(`[hyper-html-api] stripped ${p} id attribute(s) from cloned template at "${i.path.join(".")||"(root)"}"`)}let k=Ot(r,a,o),E=l[0]||u,T=t.parent(E),Q=l.length>0?it(t,T,E):0,ht=Tt(t,l),Z=new Set,W=[],C=r.map((p,I)=>{let R=k[I];if(R>=0)return Z.add(R),W.push(!1),l[R];W.push(!0);let B=t.clone(h);return t.stripIds(B),B});if(l.forEach((p,I)=>{Z.has(I)||t.remove(p)}),ht){C.forEach((p,I)=>{let R=Q+I;t.children(T).findIndex(at=>t.sameNode(at,p))!==R&&t.insertAt(T,p,R)}),ot(t,C,o,r,i,c,s);return}It(t,C,W,T,Q),ot(t,C,o,r,i,c,s)}function ot(t,e,n,o,r,i,c){e.forEach((s,l)=>{if(n===null){let f=o[l],u=f==null?"":String(f);t.text(s)!==u&&t.text(s,u)}else{let f=i(t,s,n,o[l],{depth:r.depth+1,path:[...r.path,l]},c);f&&f!==s&&(e[l]=f)}})}function At(t,e,n,o){return n===null?t.text(e):g(t,e,n,o)}function Ot(t,e,n){let o=new Array(t.length).fill(-1),r=new Set;return t.forEach((i,c)=>{let s=-1,l=-1;e.forEach((f,u)=>{if(r.has(u))return;let a=St(i,f,n),h=a===l&&s>=0?Math.abs(u-c)<Math.abs(s-c):!1;(a>l||h)&&(l=a,s=u)}),l>=Rt&&(o[c]=s,r.add(s))}),o}function St(t,e,n){if(n===null)return t===e?1:0;let o=Object.keys(n||{});if(o.length===0)return 0;let r=0;for(let i of o)JSON.stringify(t?.[i])===JSON.stringify(e?.[i])&&r++;return r/o.length}function it(t,e,n){let o=t.children(e);for(let r=0;r<o.length;r++)if(t.sameNode(o[r],n))return r;return-1}function xt(t,e,n,o){if(!o.templateAttr)return null;let r=e;for(;r;){let i=t.find(r,n,{includeRulesTag:!1,templateAttr:null});for(let c of i)if(t.attr(c,o.templateAttr)!=null)return c;r=t.parent(r)}return null}function Tt(t,e){if(e.length<=1)return!0;let n=t.parent(e[0]);if(!n)return!1;let o=t.children(n),r=[];for(let i of e){let c=o.findIndex(s=>t.sameNode(s,i));if(c===-1)return!1;r.push(c)}return r.sort((i,c)=>i-c),r[r.length-1]-r[0]===r.length-1}function It(t,e,n,o,r){let i=null,c=r;for(let s=0;s<e.length;s++){if(!n[s]){i=e[s];continue}let l=i?t.parent(i):o;if(!l)continue;let f=i?it(t,l,i)+1:c++;t.insertAt(l,e[s],f),i=e[s]}}var st=new Set(["checked","selected","disabled","readOnly","paused"]);function S(t,e,n,o,r={}){let i=[];if(V(n,o,[],i),i.length)throw new N(i);H(t,e,n,o,{depth:0,path:[]},r)}function H(t,e,n,o,r,i={}){if(r.depth>O)throw new y(r.path);if(o===void 0)return e;if(typeof n=="string")return bt(t,e,n,o,r,i);if(Array.isArray(n)){let[c,s]=n;return F(t,e,c,s,o,r,H,i),e}if(typeof n=="object"&&n!==null){for(let[c,s]of Object.entries(n)){let l=H(t,e,s,o==null?o:o[c],{depth:r.depth+1,path:[...r.path,c]},i);l&&l!==e&&(e=l)}return e}return e}function bt(t,e,n,o,r,i){if(n.endsWith("[]")){let l=n.slice(0,-2);return F(t,e,l,null,o,r,H,i),e}if(n.startsWith("@"))return lt(t,e,n.slice(1),o);let c=m(n);if(c!==-1){let l=n.slice(0,c),f=n.slice(c+1),u=l?t.find(e,l,i):[e];return u.length===0||lt(t,u[0],f,o),e}if(n===".")return t.text(e,o==null?"":String(o)),e;let s=t.find(e,n,i);return s.length===0||t.text(s[0],o==null?"":String(o)),e}function lt(t,e,n,o){if(nt.has(n))throw new M(n);if(n==="outerHTML"){let r=o==null?"":String(o);return t.replaceWith(e,r)}return et.has(n)?(t.prop(e,n,Nt(n,o)),e):(t.attr(e,n,o==null?"":String(o)),e)}function Nt(t,e){return e==null?st.has(t)?!1:"":st.has(t)?!!e:e}function V(t,e,n,o){if(e!==void 0){if(typeof t=="string"){if(t.endsWith("[]")){Array.isArray(e)?e.forEach((r,i)=>{typeof r=="object"&&r!==null&&o.push({path:D([...n,i]),expected:"scalar",got:_(r)})}):o.push({path:D(n),expected:"array",got:_(e)});return}e!==null&&typeof e=="object"&&o.push({path:D(n),expected:"scalar",got:_(e)});return}if(Array.isArray(t)){if(!Array.isArray(e)){o.push({path:D(n),expected:"array",got:_(e)});return}let r=t[1];e.forEach((i,c)=>V(r,i,[...n,c],o));return}if(typeof t=="object"&&t!==null){if(e===null||Array.isArray(e)||typeof e!="object"){o.push({path:D(n),expected:"object",got:_(e)});return}for(let[r,i]of Object.entries(t))V(i,e[r],[...n,r],o)}}}function _(t){return t===null?"null":Array.isArray(t)?"array":typeof t}function D(t){return t.join(".")}function G(t){try{return JSON.parse(t)}catch(e){throw new A(`Invalid strict JSON: ${e.message}`,e)}}function P(t){try{return JSON.parse(t)}catch{}let e={BRACE_OPEN:"{",BRACE_CLOSE:"}",BRACKET_OPEN:"[",BRACKET_CLOSE:"]",COLON:":",COMMA:",",STRING:"STRING",SELECTOR:"SELECTOR",IDENTIFIER:"IDENTIFIER",NUMBER:"NUMBER",BOOLEAN:"BOOLEAN"};function n(r){let i=[],c=0;for(;c<r.length;){let s=r[c];if(/\s/.test(s)){c++;continue}if("{}".includes(s)){i.push({type:s,value:s}),c++;continue}if(s==="["){let a=!1,h=c+1;for(;h<r.length&&/\s/.test(r[h]);)h++;if(h<r.length&&/[a-zA-Z_]/.test(r[h])&&(a=!0),!a){i.push({type:s,value:s}),c++;continue}}if(s==="]"){i.push({type:s,value:s}),c++;continue}if(s===":"){i.push({type:e.COLON,value:s}),c++;continue}if(s===","){i.push({type:e.COMMA,value:s}),c++;continue}if(s==='"'||s==="'"){let a=s,h=c+1;for(;h<r.length&&r[h]!==a;)r[h]==="\\"&&h++,h++;i.push({type:e.STRING,value:r.substring(c+1,h),quoted:!0,sourceQuote:a}),c=h+1;continue}let l=c,f;for(;l<r.length&&!/[{},]/.test(r[l]);)if(r[l]===":"){let a=[":first",":last",":nth-child",":nth-of-type",":first-child",":last-child",":first-of-type",":last-of-type",":only-child",":only-of-type",":hover",":focus",":active",":visited",":disabled",":enabled",":checked",":empty",":root",":target",":not",":before",":after",":nth-last-child",":nth-last-of-type"],h=!1;for(let k of a){let E=k.substring(1);if(r.substring(l+1,l+1+E.length)===E){h=!0,l+=E.length;break}}if(!h)break}else if(r[l]==="["){for(l++;l<r.length&&r[l]!=="]";){if(r[l]==='"'||r[l]==="'"){let a=r[l];for(l++;l<r.length&&r[l]!==a;)r[l]==="\\"&&l++,l++}l++}l<r.length&&r[l]==="]"&&l++}else l++;f=r.substring(c,l);let u=e.IDENTIFIER;/^-?\d+(\.\d+)?$/.test(f)?u=e.NUMBER:f==="true"||f==="false"||f==="null"?u=e.BOOLEAN:/^[.#@\[]|[.#@\[]| /.test(f)&&(u=e.SELECTOR),i.push({type:u,value:f,quoted:!1}),c=l}return i}function o(r){let i="";for(let c=0;c<r.length;c++){let s=r[c];if("{}".includes(s.type)||"[]".includes(s.type)){i+=s.value;continue}if(s.type===e.COLON){i+=s.value;continue}if(s.type===e.COMMA){let l=r[c+1];if(l&&(l.type==="}"||l.type==="]"))continue;i+=s.value;continue}if(s.type===e.STRING&&s.quoted){let l=s.value;s.sourceQuote==="'"&&(l=l.replace(/\\'/g,"'"),l=l.replace(/(\\*)"/g,(f,u)=>u.length%2===0?u+'\\"':f)),i+=`"${l}"`;continue}if(s.type===e.NUMBER||s.type===e.BOOLEAN){i+=s.value;continue}if(s.type===e.SELECTOR||s.type===e.IDENTIFIER){i+=`"${s.value}"`;continue}i+=`"${s.value}"`}return i}try{let r=n(t),i=o(r);return JSON.parse(i)}catch(r){throw new A("Invalid extraction rules syntax: "+r.message,r)}}var wt="1",ct=/^[a-zA-Z0-9_-]+$/;function x(t,e,n){let o;if(n===void 0)o="script[data-rules-name]";else{if(typeof n!="string"||!ct.test(n))throw new Error(`hyper-html-api: invalid rules token ${JSON.stringify(n)} (must match ${ct})`);o=`script[data-rules-name~="${n}"]`}let r=t.find(e,o,{includeRulesTag:!0});if(r.length===0)return null;n!==void 0&&r.length>1&&console.warn(`hyper-html-api: ${r.length} rules tags match data-rules-name~="${n}"; using the first.`);let i=r[0],c=t.attr(i,"data-rules-version");if(c!==wt)throw new b(c);return{rules:P(t.text(i)),tagNode:i}}function X(t,e,n){if(n&&typeof n=="object")return{rules:n,tagNode:null};if(typeof n=="string"){let o=e&&e.ownerDocument?e.ownerDocument:e;return x(t,o,n)}return null}function ft(t,e,n,o){let r=X(t,e,n);if(!r){let s=typeof n=="string"?`data-rules-name~="${n}"`:"the provided rules object";throw new Error(`hyper-html-api: could not resolve rules for ${s}`)}let{rules:i,tagNode:c}=r;return{rules:i,tagNode:c,get:()=>g(t,e,i,o),set:s=>S(t,e,i,s,o)}}function _t(t){return t&&t.nodeType===1&&t.tagName==="SCRIPT"&&t.hasAttribute&&t.hasAttribute("data-rules-name")}function Dt(t){return t?(t.nodeType===9||t.nodeType===11,t):null}var Pt={find(t,e,n={}){let o=Dt(t);if(!o||!o.querySelectorAll)return[];let r=Array.from(o.querySelectorAll(e));n.includeRulesTag||(r=r.filter(s=>!_t(s)));let i=[];n.skip&&i.push(n.skip);let c=n.templateAttr===null?null:n.templateAttr||"cms-template";if(c&&i.push("["+c+"]"),i.length){let s=i.join(", ");r=r.filter(l=>!l.closest||!l.closest(s))}return r},parent(t){return t?t.parentElement:null},children(t){return t?Array.from(t.children):[]},text(t,e){if(e===void 0)return(t.textContent||"").trim();t.textContent=e},attr(t,e,n){if(n===void 0)return t.hasAttribute&&t.hasAttribute(e)?t.getAttribute(e):null;t.setAttribute(e,n)},removeAttr(t,e){t&&t.removeAttribute&&t.removeAttribute(e)},prop(t,e,n){if(n===void 0){let o=t?t[e]:void 0;return o!==void 0?o:null}t[e]=n},clone(t){return t.cloneNode(!0)},insertAt(t,e,n){let o=t.children[n]||null;t.insertBefore(e,o)},remove(t){t&&t.parentNode&&t.parentNode.removeChild(t)},replaceWith(t,e){if(!t||!t.parentNode)throw new Error("dom.replaceWith: node has no parent");let o=t.ownerDocument.createElement("template");o.innerHTML=e;let r=o.content.firstElementChild;if(!r)throw new Error("dom.replaceWith: html did not parse to an element");return t.parentNode.replaceChild(r,t),r},stripIds(t){let e=0;return t.id&&(t.removeAttribute("id"),e++),(t.querySelectorAll?t.querySelectorAll("[id]"):[]).forEach(o=>{o.removeAttribute("id"),e++}),e},sameNode(t,e){return t===e}},d=Pt;var L={extract:(t,e,n)=>g(d,t,e,n),apply:(t,e,n,o)=>S(d,t,e,n,o),findRulesIn:(t,e)=>x(d,t,e),findRules:(t,e)=>X(d,t,e),bind:(t,e,n)=>ft(d,t,e,n),parseStrict:G,parseRelaxed:P,ruleAttrIndex:m,splitRule:v,errors:$,DOM_PROPERTIES:j},z=t=>!!t&&typeof t.nodeType=="number";function ut(t){let e=t&&t.ownerDocument?t.ownerDocument:t,n=d.find(e,"script[data-rules-name]",{includeRulesTag:!0});if(n.length===0)throw new Error('hyper-html-api: no rules tag found. Add <script type="application/json" data-rules-name="\u2026" data-rules-version="1"> or pass rules.');if(n.length>1)throw new Error('hyper-html-api: multiple rules tags found; pass a name, e.g. extractData("api").');return x(d,e).rules}function K(t,e){let n=z(t)?t:document,o=z(t)?e:t;return o===void 0?g(d,n,ut(n)):L.bind(n,o).get()}function Y(t,e,n){if(!z(t))throw new Error("hyper-html-api: applyData(root, data, source?) needs a DOM root as the first argument.");return n===void 0?S(d,t,ut(t),e):L.bind(t,n).set(e),t}var Lt={engine:L,extractData:K,applyData:Y},Ct=Lt;return yt(jt);})();
|
|
5
|
+
var hyperHtmlApiData=(()=>{var q=Object.defineProperty;var Rt=Object.getOwnPropertyDescriptor;var At=Object.getOwnPropertyNames;var Ot=Object.prototype.hasOwnProperty;var nt=(t,e)=>{for(var n in e)q(t,n,{get:e[n],enumerable:!0})},wt=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of At(e))!Ot.call(t,r)&&r!==n&&q(t,r,{get:()=>e[r],enumerable:!(o=Rt(e,r))||o.enumerable});return t};var St=t=>wt(q({},"__esModule",{value:!0}),t);var vt={};nt(vt,{applyData:()=>et,default:()=>Ut,engine:()=>k,extractData:()=>tt});var W=["textContent","innerText","innerHTML","outerHTML","value","checked","selected","disabled","readOnly","type","tagName","nodeName","nodeType","nodeValue","childElementCount","id","className","classList","baseURI","offsetWidth","offsetHeight","clientWidth","clientHeight","scrollWidth","scrollHeight","dataset","currentSrc","duration","paused","title","documentURI","contentType"],F=new Set(W),rt=new Set(["textContent","innerText","innerHTML","value","checked","selected","disabled","readOnly","type","id","className","title"]),ot=new Set(["tagName","nodeName","nodeType","nodeValue","childElementCount","classList","baseURI","documentURI","contentType","offsetWidth","offsetHeight","clientWidth","clientHeight","scrollWidth","scrollHeight","currentSrc","duration","paused","dataset"]);var B={};nt(B,{EmptyListInsert:()=>P,MAX_RULE_DEPTH:()=>b,MaxRuleDepthExceeded:()=>S,RuleTargetReadOnly:()=>j,RulesParseError:()=>T,ShapeMismatch:()=>D,UnknownRulesVersion:()=>_});var T=class extends Error{constructor(e,n){super(e),this.name="RulesParseError",this.cause=n}},_=class extends Error{constructor(e){super(`unknown rules version: ${e}. Library supports "1".`),this.name="UnknownRulesVersion",this.version=e}},b=20,S=class extends Error{constructor(e){super(`rule depth exceeded ${b} at path: ${e.join(".")}`),this.name="MaxRuleDepthExceeded",this.path=e}},D=class extends Error{constructor(e){super(`shape mismatch: ${e.length} field(s) failed validation`),this.name="ShapeMismatch",this.mismatches=e}},P=class extends Error{constructor(e){super(`cannot add items to empty list at "${e.join(".")}" \u2014 no sibling to clone as template. Seed the list with a hidden item first.`),this.name="EmptyListInsert",this.path=e}},j=class extends Error{constructor(e){super(`cannot write to read-only DOM property "${e}"`),this.name="RuleTargetReadOnly",this.target=e}};function A(t){let e=0,n=null,o=-1;for(let r=0;r<t.length;r++){let i=t[r];i==="\\"?r++:n?i===n&&(n=null):i==='"'||i==="'"?n=i:i==="["||i==="("?e++:i==="]"||i===")"?e>0&&e--:i==="@"&&e===0&&(o=r)}return o}function K(t){let e=A(t);return e===-1?{selector:t,prop:null}:{selector:t.slice(0,e),prop:t.slice(e+1)||null}}function O(t,e,n,o={}){return G(t,e,n,{depth:0,path:[]},o)}function G(t,e,n,o,r){if(o.depth>b)throw new S(o.path);if(typeof n=="string")return xt(t,e,n,o,r);if(Array.isArray(n)){let[i,f]=n,s=t.find(e,i,r);return st(r,o,s),s.map((l,c)=>G(t,l,f,{depth:o.depth+1,path:[...o.path,c]},r))}if(typeof n=="object"&&n!==null){let i={};for(let[f,s]of Object.entries(n))i[f]=G(t,e,s,{depth:o.depth+1,path:[...o.path,f]},r);return i}return null}function xt(t,e,n,o,r){if(n.endsWith("[]")){let s=n.slice(0,-2),l=t.find(e,s,r);return st(r,o,l),l.map(c=>t.text(c))}if(n.startsWith("@"))return it(t,e,n.slice(1));let i=A(n);if(i!==-1){let s=n.slice(0,i),l=n.slice(i+1),c=s?t.find(e,s,r):[e];return c.length===0?null:it(t,c[0],l)}if(n===".")return t.text(e);let f=t.find(e,n,r);return f.length===0?null:t.text(f[0])}function st(t,e,n){if(typeof t.onRowsRead=="function")try{t.onRowsRead(e.path.slice(),n)}catch(o){console.warn(`[hyper-html-api] onRowsRead threw at "${e.path.join(".")||"(root)"}"`,o)}}function it(t,e,n){if(F.has(n)){let r=t.prop(e,n);return r==null?null:String(r)}let o=t.attr(e,n);return o||null}function ct(t,e,n,o){let r=t.length,i=e.length,f=new Array(r).fill(-1);if(o){let u=new Set;for(let d=0;d<r;d++){let g=o[d];!(g>=0&&g<i)||u.has(g)||(f[d]=g,u.add(g))}}if(r===0||i===0)return f;let s=t.map(u=>lt(u,n)),l=e.map(u=>lt(u,n)),c=new Array(i).fill(!1);for(let u of f)u>=0&&(c[u]=!0);let p=new Map;l.forEach((u,d)=>{c[d]||p.set(u,p.has(u)?-1:d)});let a=new Map;s.forEach((u,d)=>{f[d]>=0||a.set(u,(a.get(u)||0)+1)}),s.forEach((u,d)=>{if(f[d]>=0||a.get(u)!==1)return;let g=p.get(u);g===void 0||g===-1||c[g]||(f[d]=g,c[g]=!0)});let h=[];for(let u=0;u<r;u++)f[u]<0&&h.push(u);let E=[];for(let u=0;u<i;u++)c[u]||E.push(u);if(h.length===0||E.length===0)return f;let y=r*i+1,w=(u,d)=>Tt(t[u],e[d],n)*y+Math.abs(u-d);for(let[u,d]of bt(h,E,w))f[u]=d;return f}var ut=t=>typeof t=="object"&&t!==null;function lt(t,e){if(!ut(e))return t==null?" null":String(t);let n=Object.keys(e);return JSON.stringify(n.map(o=>{let r=JSON.stringify(t?.[o]);return r===void 0?" undef":r}))}function Tt(t,e,n){if(!ut(n))return t===e?0:1;let o=Object.keys(n);if(o.length===0)return 0;let r=0;for(let i of o){let f=JSON.stringify(t?.[i]),s=JSON.stringify(e?.[i]);f!==s&&r++}return r}function bt(t,e,n){return t.length<=e.length?ft(t,e,n,!1):ft(e,t,(o,r)=>n(r,o),!0)}function ft(t,e,n,o){let r=t.length,i=e.length,f=[];for(let a=0;a<=r;a++)f.push(new Float64Array(i+1).fill(1/0));let s=[];for(let a=0;a<=r;a++)s.push(new Uint8Array(i+1));for(let a=0;a<=i;a++)f[r][a]=0;for(let a=r-1;a>=0;a--)for(let h=i-1;h>=0;h--){let E=n(t[a],e[h])+f[a+1][h+1],y=f[a][h+1];E<=y?(f[a][h]=E,s[a][h]=1):f[a][h]=y}let l=[],c=0,p=0;for(;c<r&&p<i;)s[c][p]&&(l.push(o?[e[p],t[c]]:[t[c],e[p]]),c++),p++;return l}function X(t,e,n,o,r,i,f,s={}){let l=t.find(e,n,s);if(r.length===0){l.forEach(m=>t.remove(m)),V(s,"onRowsApplied",i.path,[]);return}let c=r.length>l.length,p=l[0]||null;if(c&&!p&&(p=_t(t,e,n,s),!p))throw new P(i.path);let a=l.map(m=>Mt(t,m,o,s)),h=null;if(c&&p){h=t.clone(p),s.templateAttr&&t.removeAttr(h,s.templateAttr);let m=t.stripIds(h);m>0&&console.warn(`[hyper-html-api] stripped ${m} id attribute(s) from cloned template at "${i.path.join(".")||"(root)"}"`)}let E=ct(r,a,o,Nt(t,l,r,i,s)),y=l[0]||p,w=t.parent(y),u=l.length>0?at(t,w,y):0,d=Dt(t,l),g=new Set,v=[],H=r.map((m,M)=>{let x=E[M];if(x>=0)return g.add(x),v.push(!1),l[x];v.push(!0);let J=t.clone(h);return t.stripIds(J),J});l.forEach((m,M)=>{g.has(M)||t.remove(m)}),d?H.forEach((m,M)=>{let x=u+M;t.children(w).findIndex(Et=>t.sameNode(Et,m))!==x&&t.insertAt(w,m,x)}):Pt(t,H,v,w,u),It(t,H,o,r,i,f,s),V(s,"onRowsApplied",i.path,H)}function V(t,e,n,o){if(typeof t[e]=="function")try{return t[e](n.slice(),o)}catch(r){console.warn(`[hyper-html-api] ${e} threw at "${n.join(".")||"(root)"}"`,r);return}}function Nt(t,e,n,o,r){let i=V(r,"identifyRows",o.path,n);if(!Array.isArray(i))return null;let f=new Array(n.length).fill(-1),s=new Set;for(let l=0;l<n.length;l++)if(i[l]){for(let c=0;c<e.length;c++)if(!(s.has(c)||!t.sameNode(e[c],i[l]))){f[l]=c,s.add(c);break}}return f}function It(t,e,n,o,r,i,f){e.forEach((s,l)=>{if(n===null){let c=o[l],p=c==null?"":String(c);t.text(s)!==p&&t.text(s,p)}else{let c=i(t,s,n,o[l],{depth:r.depth+1,path:[...r.path,l]},f);c&&c!==s&&(e[l]=c)}})}function Mt(t,e,n,o){return n===null?t.text(e):O(t,e,n,o.onRowsRead?{...o,onRowsRead:void 0}:o)}function at(t,e,n){let o=t.children(e);for(let r=0;r<o.length;r++)if(t.sameNode(o[r],n))return r;return-1}function _t(t,e,n,o){if(!o.templateAttr)return null;let r=e;for(;r;){let i=t.find(r,n,{includeRulesTag:!1,templateAttr:null});for(let f of i)if(t.attr(f,o.templateAttr)!=null)return f;r=t.parent(r)}return null}function Dt(t,e){if(e.length<=1)return!0;let n=t.parent(e[0]);if(!n)return!1;let o=t.children(n),r=[];for(let i of e){let f=o.findIndex(s=>t.sameNode(s,i));if(f===-1)return!1;r.push(f)}return r.sort((i,f)=>i-f),r[r.length-1]-r[0]===r.length-1}function Pt(t,e,n,o,r){let i=null,f=r;for(let s=0;s<e.length;s++){if(!n[s]){i=e[s];continue}let l=i?t.parent(i):o;if(!l)continue;let c=i?at(t,l,i)+1:f++;t.insertAt(l,e[s],c),i=e[s]}}var ht=new Set(["checked","selected","disabled","readOnly","paused"]);function N(t,e,n,o,r={}){let i=[];if(z(n,o,[],i),i.length)throw new D(i);U(t,e,n,o,{depth:0,path:[]},r)}function U(t,e,n,o,r,i={}){if(r.depth>b)throw new S(r.path);if(o===void 0)return e;if(typeof n=="string")return jt(t,e,n,o,r,i);if(Array.isArray(n)){let[f,s]=n;return X(t,e,f,s,o,r,U,i),e}if(typeof n=="object"&&n!==null){for(let[f,s]of Object.entries(n)){let l=U(t,e,s,o==null?o:o[f],{depth:r.depth+1,path:[...r.path,f]},i);l&&l!==e&&(e=l)}return e}return e}function jt(t,e,n,o,r,i){if(n.endsWith("[]")){let l=n.slice(0,-2);return X(t,e,l,null,o,r,U,i),e}if(n.startsWith("@"))return dt(t,e,n.slice(1),o);let f=A(n);if(f!==-1){let l=n.slice(0,f),c=n.slice(f+1),p=l?t.find(e,l,i):[e];return p.length===0||dt(t,p[0],c,o),e}if(n===".")return pt(t,e,o),e;let s=t.find(e,n,i);return s.length===0||pt(t,s[0],o),e}function pt(t,e,n){let o=n==null?"":String(n);t.text(e)!==o&&t.text(e,o)}function dt(t,e,n,o){if(ot.has(n))throw new j(n);if(n==="outerHTML"){let r=o==null?"":String(o);return t.replaceWith(e,r)}return rt.has(n)?(t.prop(e,n,Ct(n,o)),e):(t.attr(e,n,o==null?"":String(o)),e)}function Ct(t,e){return e==null?ht.has(t)?!1:"":ht.has(t)?e==="false"?!1:!!e:e}function z(t,e,n,o){if(e!==void 0){if(typeof t=="string"){if(t.endsWith("[]")){Array.isArray(e)?e.forEach((r,i)=>{typeof r=="object"&&r!==null&&o.push({path:L([...n,i]),expected:"scalar",got:C(r)})}):o.push({path:L(n),expected:"array",got:C(e)});return}e!==null&&typeof e=="object"&&o.push({path:L(n),expected:"scalar",got:C(e)});return}if(Array.isArray(t)){if(!Array.isArray(e)){o.push({path:L(n),expected:"array",got:C(e)});return}let r=t[1];e.forEach((i,f)=>z(r,i,[...n,f],o));return}if(typeof t=="object"&&t!==null){if(e===null||Array.isArray(e)||typeof e!="object"){o.push({path:L(n),expected:"object",got:C(e)});return}for(let[r,i]of Object.entries(t))z(i,e[r],[...n,r],o)}}}function C(t){return t===null?"null":Array.isArray(t)?"array":typeof t}function L(t){return t.join(".")}function Q(t){try{return JSON.parse(t)}catch(e){throw new T(`Invalid strict JSON: ${e.message}`,e)}}function $(t){try{return JSON.parse(t)}catch{}let e={BRACE_OPEN:"{",BRACE_CLOSE:"}",BRACKET_OPEN:"[",BRACKET_CLOSE:"]",COLON:":",COMMA:",",STRING:"STRING",SELECTOR:"SELECTOR",IDENTIFIER:"IDENTIFIER",NUMBER:"NUMBER",BOOLEAN:"BOOLEAN"};function n(r){let i=[],f=0;for(;f<r.length;){let s=r[f];if(/\s/.test(s)){f++;continue}if("{}".includes(s)){i.push({type:s,value:s}),f++;continue}if(s==="["){let a=!1,h=f+1;for(;h<r.length&&/\s/.test(r[h]);)h++;if(h<r.length&&/[a-zA-Z_]/.test(r[h])&&(a=!0),!a){i.push({type:s,value:s}),f++;continue}}if(s==="]"){i.push({type:s,value:s}),f++;continue}if(s===":"){i.push({type:e.COLON,value:s}),f++;continue}if(s===","){i.push({type:e.COMMA,value:s}),f++;continue}if(s==='"'||s==="'"){let a=s,h=f+1;for(;h<r.length&&r[h]!==a;)r[h]==="\\"&&h++,h++;i.push({type:e.STRING,value:r.substring(f+1,h),quoted:!0,sourceQuote:a}),f=h+1;continue}let l=f,c;for(;l<r.length&&!/[{},]/.test(r[l]);)if(r[l]===":"){let a=[":first",":last",":nth-child",":nth-of-type",":first-child",":last-child",":first-of-type",":last-of-type",":only-child",":only-of-type",":hover",":focus",":active",":visited",":disabled",":enabled",":checked",":empty",":root",":target",":not",":before",":after",":nth-last-child",":nth-last-of-type"],h=!1;for(let E of a){let y=E.substring(1);if(r.substring(l+1,l+1+y.length)===y){h=!0,l+=y.length;break}}if(!h)break}else if(r[l]==="["){for(l++;l<r.length&&r[l]!=="]";){if(r[l]==='"'||r[l]==="'"){let a=r[l];for(l++;l<r.length&&r[l]!==a;)r[l]==="\\"&&l++,l++}l++}l<r.length&&r[l]==="]"&&l++}else l++;c=r.substring(f,l);let p=e.IDENTIFIER;/^-?\d+(\.\d+)?$/.test(c)?p=e.NUMBER:c==="true"||c==="false"||c==="null"?p=e.BOOLEAN:/^[.#@\[]|[.#@\[]| /.test(c)&&(p=e.SELECTOR),i.push({type:p,value:c,quoted:!1}),f=l}return i}function o(r){let i="";for(let f=0;f<r.length;f++){let s=r[f];if("{}".includes(s.type)||"[]".includes(s.type)){i+=s.value;continue}if(s.type===e.COLON){i+=s.value;continue}if(s.type===e.COMMA){let l=r[f+1];if(l&&(l.type==="}"||l.type==="]"))continue;i+=s.value;continue}if(s.type===e.STRING&&s.quoted){let l=s.value;s.sourceQuote==="'"&&(l=l.replace(/\\'/g,"'"),l=l.replace(/(\\*)"/g,(c,p)=>p.length%2===0?p+'\\"':c)),i+=`"${l}"`;continue}if(s.type===e.NUMBER||s.type===e.BOOLEAN){i+=s.value;continue}if(s.type===e.SELECTOR||s.type===e.IDENTIFIER){i+=`"${s.value}"`;continue}i+=`"${s.value}"`}return i}try{let r=n(t),i=o(r);return JSON.parse(i)}catch(r){throw new T("Invalid extraction rules syntax: "+r.message,r)}}var Lt="1",gt=/^[a-zA-Z0-9_-]+$/;function I(t,e,n){let o;if(n===void 0)o="script[data-rules-name]";else{if(typeof n!="string"||!gt.test(n))throw new Error(`hyper-html-api: invalid rules token ${JSON.stringify(n)} (must match ${gt})`);o=`script[data-rules-name~="${n}"]`}let r=t.find(e,o,{includeRulesTag:!0});if(r.length===0)return null;n!==void 0&&r.length>1&&console.warn(`hyper-html-api: ${r.length} rules tags match data-rules-name~="${n}"; using the first.`);let i=r[0],f=t.attr(i,"data-rules-version");if(f!==Lt)throw new _(f);return{rules:$(t.text(i)),tagNode:i}}function Y(t,e,n){if(n&&typeof n=="object")return{rules:n,tagNode:null};if(typeof n=="string"){let o=e&&e.ownerDocument?e.ownerDocument:e;return I(t,o,n)}return null}function mt(t,e,n,o){let r=Y(t,e,n);if(!r){let s=typeof n=="string"?`data-rules-name~="${n}"`:"the provided rules object";throw new Error(`hyper-html-api: could not resolve rules for ${s}`)}let{rules:i,tagNode:f}=r;return{rules:i,tagNode:f,get:()=>O(t,e,i,o),set:s=>N(t,e,i,s,o)}}function kt(t){return t&&t.nodeType===1&&t.tagName==="SCRIPT"&&t.hasAttribute&&t.hasAttribute("data-rules-name")}function Ht(t){return t?(t.nodeType===9||t.nodeType===11,t):null}var Wt={find(t,e,n={}){let o=Ht(t);if(!o||!o.querySelectorAll)return[];let r=Array.from(o.querySelectorAll(e));n.includeRulesTag||(r=r.filter(s=>!kt(s)));let i=[];n.skip&&i.push(n.skip);let f=n.templateAttr===null?null:n.templateAttr||"cms-template";if(f&&i.push("["+f+"]"),i.length){let s=i.join(", ");r=r.filter(l=>!l.closest||!l.closest(s))}return r},parent(t){return t?t.parentElement:null},children(t){return t?Array.from(t.children):[]},text(t,e){if(e===void 0)return(t.textContent||"").trim();t.textContent=e},attr(t,e,n){if(n===void 0)return t.hasAttribute&&t.hasAttribute(e)?t.getAttribute(e):null;t.setAttribute(e,n)},removeAttr(t,e){t&&t.removeAttribute&&t.removeAttribute(e)},prop(t,e,n){if(n===void 0){let o=t?t[e]:void 0;return o!==void 0?o:null}t[e]=n},clone(t){return t.cloneNode(!0)},insertAt(t,e,n){let o=t.children[n]||null;t.insertBefore(e,o)},remove(t){t&&t.parentNode&&t.parentNode.removeChild(t)},replaceWith(t,e){if(!t||!t.parentNode)throw new Error("dom.replaceWith: node has no parent");let o=t.ownerDocument.createElement("template");o.innerHTML=e;let r=o.content.firstElementChild;if(!r)throw new Error("dom.replaceWith: html did not parse to an element");return t.parentNode.replaceChild(r,t),r},stripIds(t){let e=0;return t.id&&(t.removeAttribute("id"),e++),(t.querySelectorAll?t.querySelectorAll("[id]"):[]).forEach(o=>{o.removeAttribute("id"),e++}),e},sameNode(t,e){return t===e}},R=Wt;var k={extract:(t,e,n)=>O(R,t,e,n),apply:(t,e,n,o)=>N(R,t,e,n,o),findRulesIn:(t,e)=>I(R,t,e),findRules:(t,e)=>Y(R,t,e),bind:(t,e,n)=>mt(R,t,e,n),parseStrict:Q,parseRelaxed:$,ruleAttrIndex:A,splitRule:K,errors:B,DOM_PROPERTIES:W},Z=t=>!!t&&typeof t.nodeType=="number";function yt(t){let e=t&&t.ownerDocument?t.ownerDocument:t,n=R.find(e,"script[data-rules-name]",{includeRulesTag:!0});if(n.length===0)throw new Error('hyper-html-api: no rules tag found. Add <script type="application/json" data-rules-name="\u2026" data-rules-version="1"> or pass rules.');if(n.length>1)throw new Error('hyper-html-api: multiple rules tags found; pass a name, e.g. extractData("api").');return I(R,e).rules}function tt(t,e){let n=Z(t)?t:document,o=Z(t)?e:t;return o===void 0?O(R,n,yt(n)):k.bind(n,o).get()}function et(t,e,n){if(!Z(t))throw new Error("hyper-html-api: applyData(root, data, source?) needs a DOM root as the first argument.");return n===void 0?N(R,t,yt(t),e):k.bind(t,n).set(e),t}var Bt={engine:k,extractData:tt,applyData:et},Ut=Bt;return St(vt);})();
|
|
6
6
|
window.clay = window.clay || {};
|
|
7
7
|
window.clay.extractData = hyperHtmlApiData.extractData;
|
|
8
8
|
// clayjs-level sugar: applyData(data) writes into the whole document, matching
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
"./THIRD-PARTY-NOTICES.md": "./THIRD-PARTY-NOTICES.md",
|
|
32
32
|
"./src/*": "./src/*",
|
|
33
33
|
"./entries/*": "./entries/*",
|
|
34
|
+
"./clay.standalone.js": "./dist/clay.standalone.js",
|
|
35
|
+
"./dist/*": "./dist/*",
|
|
34
36
|
"./*": "./entries/*"
|
|
35
37
|
},
|
|
36
38
|
"files": [
|
|
37
39
|
"entries/",
|
|
40
|
+
"dist/",
|
|
38
41
|
"THIRD-PARTY-NOTICES.md",
|
|
39
42
|
"src/"
|
|
40
43
|
],
|
|
@@ -45,7 +48,10 @@
|
|
|
45
48
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
46
49
|
"test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
|
|
47
50
|
"conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
|
|
48
|
-
"
|
|
51
|
+
"build:standalone": "node build.js --standalone",
|
|
52
|
+
"prepack": "node build.js --standalone",
|
|
53
|
+
"test:standalone": "node build.js --standalone && web-test-runner --config tests/browser/wtr.config.mjs",
|
|
54
|
+
"prepublishOnly": "npm test && npm run test:conformance && npm run test:standalone",
|
|
49
55
|
"dev": "node tests/stub-server.mjs",
|
|
50
56
|
"build": "node build.js",
|
|
51
57
|
"deploy": "node build.js && wrangler deploy && git rev-parse HEAD > .deploy"
|
|
@@ -55,6 +61,7 @@
|
|
|
55
61
|
"@web/test-runner": "^0.20.2",
|
|
56
62
|
"@web/test-runner-commands": "^0.9.0",
|
|
57
63
|
"@web/test-runner-playwright": "^0.11.1",
|
|
64
|
+
"esbuild": "^0.28.2",
|
|
58
65
|
"jest": "^29.7.0",
|
|
59
66
|
"jest-environment-jsdom": "^29.7.0",
|
|
60
67
|
"playwright": "1.62.1"
|
package/src/core/etag.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* etag.js — the version stamp this tab last saw on disk (spec §6, `conditional`).
|
|
3
|
+
*
|
|
4
|
+
* One value, and one rule that decides everything about it: this client never
|
|
5
|
+
* computes a stamp, it only ever echoes one the host handed it. The host stamps
|
|
6
|
+
* the bytes it STORED, and on a document carrying formathtml="true" those are not
|
|
7
|
+
* the bytes it was sent, so a stamp computed here would disagree with disk from
|
|
8
|
+
* the first save onward and every later save would be refused against a value it
|
|
9
|
+
* could never have matched.
|
|
10
|
+
*
|
|
11
|
+
* Three things move the stamp, and nothing else does:
|
|
12
|
+
*
|
|
13
|
+
* - discovery seeds it, because a freshly loaded page has never saved and so
|
|
14
|
+
* holds no stamp of its own,
|
|
15
|
+
* - an accepted save replaces it with the one that response carried, and clears
|
|
16
|
+
* it when a response carries none: after our own write, any stamp still held
|
|
17
|
+
* here is known to describe bytes the host has stopped storing,
|
|
18
|
+
* - a disk-sourced live-sync frame replaces it with the stamp that frame
|
|
19
|
+
* carried, because the file changed under a tab that never saved. A frame
|
|
20
|
+
* with no stamp on it falls back to clearing and asking the host.
|
|
21
|
+
*
|
|
22
|
+
* A peer's SNAPSHOT does not move it. §10 relays never write to disk, so the
|
|
23
|
+
* stamp is still true after one lands, and treating one as a disk change would
|
|
24
|
+
* refetch discovery on every keystroke another editor makes.
|
|
25
|
+
*
|
|
26
|
+
* No stamp means no `If-Match`, which means last write wins for that one save.
|
|
27
|
+
* That is what every save did before this module existed, so an absent stamp is
|
|
28
|
+
* always a step back to the old behaviour and never a new failure.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { hostMeta } from "./host-meta.js";
|
|
32
|
+
import { isEditMode } from "./is-edit-mode.js";
|
|
33
|
+
|
|
34
|
+
let lastSeen = null;
|
|
35
|
+
let conditional = false;
|
|
36
|
+
// Bumped by every write. A discovery answer that resolves after a save has
|
|
37
|
+
// already recorded a newer stamp must not overwrite it.
|
|
38
|
+
let generation = 0;
|
|
39
|
+
|
|
40
|
+
/** The stamp to send on the next save, or null. */
|
|
41
|
+
export function lastSeenEtag() {
|
|
42
|
+
return lastSeen;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True only when the host announced `conditional` by name (§5). */
|
|
46
|
+
export function conditionalSaves() {
|
|
47
|
+
return conditional;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Take the stamp from a save response. A response with no stamp clears it.
|
|
52
|
+
* @param {?string} value
|
|
53
|
+
*/
|
|
54
|
+
export function recordEtag(value) {
|
|
55
|
+
lastSeen = typeof value === "string" && value !== "" ? value : null;
|
|
56
|
+
generation++;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Forget the stamp: the next save goes out unconditional. */
|
|
60
|
+
export function forgetEtag() {
|
|
61
|
+
recordEtag(null);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Learn from discovery whether this host offers conditional saves, and what the
|
|
66
|
+
* document's current stamp is.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} [options]
|
|
69
|
+
* @param {boolean} [options.fresh] - Ask the host again instead of reusing the
|
|
70
|
+
* memoized answer, and take whatever it says as the whole truth: an answer with
|
|
71
|
+
* no stamp in it clears the one held here rather than leaving a stale value the
|
|
72
|
+
* host would refuse forever.
|
|
73
|
+
* @param {boolean} [options.clearIfMissing] - Whether an answer carrying no stamp
|
|
74
|
+
* drops the one held here. Defaults to `fresh`, which is what a person asking to
|
|
75
|
+
* overwrite wants: no stamp means the next save goes out unconditional, which is
|
|
76
|
+
* last write wins, which is what they asked for by name. Pass false for a refresh
|
|
77
|
+
* nobody requested. Clearing fails OPEN, and a background reseed that quietly
|
|
78
|
+
* turns the guard off is worse than one that leaves a stale stamp behind, because
|
|
79
|
+
* a stale stamp only costs a refusal.
|
|
80
|
+
* @returns {Promise<?string>}
|
|
81
|
+
*/
|
|
82
|
+
export async function seedEtag({ fresh = false, clearIfMissing = fresh } = {}) {
|
|
83
|
+
const at = generation;
|
|
84
|
+
const meta = await hostMeta({ fresh });
|
|
85
|
+
conditional = meta.extensions.includes("conditional");
|
|
86
|
+
|
|
87
|
+
if (generation !== at) return lastSeen;
|
|
88
|
+
|
|
89
|
+
const seed = meta.document?.etag;
|
|
90
|
+
if (typeof seed === "string" && seed !== "") {
|
|
91
|
+
lastSeen = seed;
|
|
92
|
+
generation++;
|
|
93
|
+
} else if (clearIfMissing) {
|
|
94
|
+
lastSeen = null;
|
|
95
|
+
generation++;
|
|
96
|
+
}
|
|
97
|
+
return lastSeen;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A disk-sourced frame is the one live-sync outcome that changes the file under a
|
|
101
|
+
// tab which did not save it. Holding the old stamp would refuse this tab's next
|
|
102
|
+
// save against bytes it has already morphed to and is looking at, so the stamp
|
|
103
|
+
// has to move.
|
|
104
|
+
//
|
|
105
|
+
// The frame's own stamp is the right one, and live-sync has already taken it as
|
|
106
|
+
// part of applying the frame's content. Nothing is left to do here: this listener
|
|
107
|
+
// exists for the frames that carry no stamp.
|
|
108
|
+
//
|
|
109
|
+
// Asking the host is the fallback and not the rule, because discovery answers
|
|
110
|
+
// about whatever is on disk when the ANSWER is built, which is a later moment
|
|
111
|
+
// than the frame. If a second write lands in between, the reply describes bytes
|
|
112
|
+
// this tab has never seen, and adopting it makes the next save overwrite that
|
|
113
|
+
// write silently. The frames without a stamp are an old host, and the fetch
|
|
114
|
+
// fallback for a change too large to send, whose body is the served page rather
|
|
115
|
+
// than anything the host stamped.
|
|
116
|
+
//
|
|
117
|
+
// A frame that live-sync HELD is deliberately not covered, because it never
|
|
118
|
+
// dispatches this event. That tab has unsaved local edits and has not seen the
|
|
119
|
+
// new disk bytes, which is precisely the case a 412 exists for.
|
|
120
|
+
if (isEditMode) {
|
|
121
|
+
document.addEventListener("clay:sync-applied", (event) => {
|
|
122
|
+
if (event.detail?.source !== "disk") return;
|
|
123
|
+
if (typeof event.detail.etag === "string" && event.detail.etag) return;
|
|
124
|
+
forgetEtag();
|
|
125
|
+
seedEtag({ fresh: true });
|
|
126
|
+
});
|
|
127
|
+
}
|
package/src/core/host-attrs.js
CHANGED
|
@@ -7,25 +7,80 @@
|
|
|
7
7
|
* cannot drift between the edit-mode ladder and the save lane.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { SAVE_TOKEN_ATTRS, LEGACY_SAVE_TOKEN_ATTRS } from "../lib/root-attrs.js";
|
|
11
|
+
|
|
12
|
+
let warnedAboutLegacyToken = false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Say so, once, when this response carries only the pre-rename save token.
|
|
16
|
+
*
|
|
17
|
+
* Dropping the old spelling is a deliberate break (see root-attrs.js), and its failure
|
|
18
|
+
* mode is the kind worth spending five lines on: the host that serves the old name also
|
|
19
|
+
* sets the edit-mode cookie, so the page stays editable and every save 404s. Without
|
|
20
|
+
* this the reader sees a working page that quietly keeps nothing. With it the console
|
|
21
|
+
* names the cause and the fix, which is the whole difference between a break and a bug.
|
|
22
|
+
*/
|
|
23
|
+
function warnAboutLegacyToken() {
|
|
24
|
+
if (warnedAboutLegacyToken) return;
|
|
25
|
+
const carries = LEGACY_SAVE_TOKEN_ATTRS.some(
|
|
26
|
+
(attr) => document.documentElement.getAttribute(attr)
|
|
27
|
+
);
|
|
28
|
+
if (!carries) return;
|
|
29
|
+
warnedAboutLegacyToken = true;
|
|
30
|
+
console.warn(
|
|
31
|
+
"[clay] This page was served with the pre-1.9.0 save token (" +
|
|
32
|
+
LEGACY_SAVE_TOKEN_ATTRS.join(", ") +
|
|
33
|
+
"), which this version no longer accepts. Saving will fail until the host is " +
|
|
34
|
+
"updated. If this is HTML Clay, upgrade it to 1.9.0 or newer."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
11
37
|
|
|
12
38
|
/**
|
|
13
39
|
* The per-document save token this response carries, or null.
|
|
40
|
+
*
|
|
41
|
+
* Walks SAVE_TOKEN_ATTRS and not the wider HOST_TOKEN_ATTRS: whatever comes back
|
|
42
|
+
* is put straight into the save URL and gates edit mode, so a durable file
|
|
43
|
+
* identity in that list would be treated as a capability it is not.
|
|
44
|
+
*
|
|
14
45
|
* @returns {?string}
|
|
15
46
|
*/
|
|
16
47
|
export function saveToken() {
|
|
17
48
|
if (typeof document === "undefined") return null;
|
|
18
|
-
for (const attr of
|
|
49
|
+
for (const attr of SAVE_TOKEN_ATTRS) {
|
|
19
50
|
const value = document.documentElement.getAttribute(attr);
|
|
20
51
|
if (value) return value;
|
|
21
52
|
}
|
|
53
|
+
warnAboutLegacyToken();
|
|
22
54
|
return null;
|
|
23
55
|
}
|
|
24
56
|
|
|
25
57
|
/**
|
|
26
|
-
* True when the host handed this response a save token
|
|
58
|
+
* True when the host handed this response a save token this version accepts.
|
|
59
|
+
*
|
|
60
|
+
* Only `savetoken` counts. A response carrying nothing but the pre-rename spelling is
|
|
61
|
+
* FALSE here and true from `servedStaleToken()`, which is the whole point of the pair.
|
|
62
|
+
*
|
|
27
63
|
* @returns {boolean}
|
|
28
64
|
*/
|
|
29
65
|
export function hasSaveToken() {
|
|
30
66
|
return saveToken() !== null;
|
|
31
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True when this response carries ONLY the pre-rename save token.
|
|
71
|
+
*
|
|
72
|
+
* That combination means one thing: the host is older than the rename and cannot be
|
|
73
|
+
* saved to by this version. It is worth its own name because two separate things act
|
|
74
|
+
* on it. Edit mode goes off, so the page does not offer editing it cannot keep, and
|
|
75
|
+
* stale-host-notice.js says why on the page, since the console line reaches a
|
|
76
|
+
* developer and nobody else.
|
|
77
|
+
*
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function servedStaleToken() {
|
|
81
|
+
if (typeof document === "undefined") return false;
|
|
82
|
+
if (saveToken() !== null) return false;
|
|
83
|
+
return LEGACY_SAVE_TOKEN_ATTRS.some(
|
|
84
|
+
(attr) => document.documentElement.getAttribute(attr)
|
|
85
|
+
);
|
|
86
|
+
}
|
package/src/core/host-meta.js
CHANGED
|
@@ -23,6 +23,7 @@ function bareHost() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
let inFlight = null;
|
|
26
|
+
let refreshing = null;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Ask the host what it supports, once per page.
|
|
@@ -30,11 +31,26 @@ let inFlight = null;
|
|
|
30
31
|
* Memoizes the PROMISE rather than the value, so two pickers opened together
|
|
31
32
|
* make one request instead of two and the second waits on the first.
|
|
32
33
|
*
|
|
34
|
+
* `spec` and `extensions` describe the host and never change under a loaded
|
|
35
|
+
* document, which is why one answer serves the whole page. The `document` block
|
|
36
|
+
* does change: its `etag` ticks on every save, by anyone. A caller that needs a
|
|
37
|
+
* current one passes `fresh`, which asks again and replaces the memoized answer,
|
|
38
|
+
* and concurrent fresh callers still share one request.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} [options]
|
|
41
|
+
* @param {boolean} [options.fresh]
|
|
33
42
|
* @returns {Promise<{spec: ?number, extensions: string[], document: ?Object}>}
|
|
34
43
|
*/
|
|
35
|
-
export function hostMeta() {
|
|
36
|
-
if (!
|
|
37
|
-
|
|
44
|
+
export function hostMeta({ fresh = false } = {}) {
|
|
45
|
+
if (!fresh) {
|
|
46
|
+
if (!inFlight) inFlight = fetchMeta().catch(bareHost);
|
|
47
|
+
return inFlight;
|
|
48
|
+
}
|
|
49
|
+
if (!refreshing) {
|
|
50
|
+
refreshing = fetchMeta().catch(bareHost).finally(() => { refreshing = null; });
|
|
51
|
+
inFlight = refreshing;
|
|
52
|
+
}
|
|
53
|
+
return refreshing;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
/**
|
|
@@ -54,6 +70,7 @@ export async function hostSupports(name) {
|
|
|
54
70
|
/** Drop the memoized answer. Tests only. */
|
|
55
71
|
export function resetHostMeta() {
|
|
56
72
|
inFlight = null;
|
|
73
|
+
refreshing = null;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
async function fetchMeta() {
|
package/src/core/is-edit-mode.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import cookie from "../lib/cookie.js";
|
|
2
2
|
import query from "../lib/query.js";
|
|
3
|
-
import { hasSaveToken } from "./host-attrs.js";
|
|
3
|
+
import { hasSaveToken, servedStaleToken } from "./host-attrs.js";
|
|
4
4
|
|
|
5
5
|
// Edit-mode precedence: an explicit ?editmode=true|false URL param wins, then an
|
|
6
6
|
// opt-in window.clayEditMode global (with the legacy window.__hyperclayEditMode
|
|
7
7
|
// still honored as a fallback for older standalone embedders — htmlclay itself
|
|
8
|
-
// uses
|
|
8
|
+
// uses its injected save token plus the admin cookie, not this global), then
|
|
9
9
|
// a save token the host put on the root, then the isAdminOfCurrentResource cookie. The
|
|
10
10
|
// global is for standalone uses (demos, htmlclay, any self-saving file) that are
|
|
11
11
|
// always editable and have no owner cookie; setting it before clayjs loads turns
|
|
@@ -25,11 +25,20 @@ if (typeof window !== "undefined") {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// A host older than the save-token rename takes edit mode off the table, above every
|
|
29
|
+
// rung below it. Such a host still sets the owner cookie, so without this the page
|
|
30
|
+
// would look fully editable while every save 404s against a route that no longer
|
|
31
|
+
// matches: an editable page that keeps nothing is worse than a read-only one. Only an
|
|
32
|
+
// explicit ?editmode=true outranks it, because that is a person at the keyboard asking
|
|
33
|
+
// for it this load, not a decision baked into the document by an author who could not
|
|
34
|
+
// have known. stale-host-notice.js puts the reason on the page.
|
|
28
35
|
const isEditMode = query.editmode
|
|
29
36
|
? query.editmode === "true" // takes precedence over the global, token and cookie
|
|
30
|
-
:
|
|
31
|
-
?
|
|
32
|
-
:
|
|
37
|
+
: servedStaleToken()
|
|
38
|
+
? false
|
|
39
|
+
: forcedEditMode != null
|
|
40
|
+
? forcedEditMode
|
|
41
|
+
: hasSaveToken() || Boolean(cookie.get("isAdminOfCurrentResource"));
|
|
33
42
|
|
|
34
43
|
const isOwner = Boolean(cookie.get("isAdminOfCurrentResource"));
|
|
35
44
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isEditMode } from "./is-edit-mode.js";
|
|
2
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
3
|
+
import { style, set, make } from "../lib/hostile-css.js";
|
|
4
|
+
|
|
5
|
+
// The page's only word on a refused save. Without it a document whose host said no
|
|
6
|
+
// looks exactly like one that is saving fine, while autosave sits suspended: the
|
|
7
|
+
// status chip that would have said so is a plugin, off by default, so most
|
|
8
|
+
// documents show nothing at all.
|
|
9
|
+
//
|
|
10
|
+
// It sits at the bottom because that is where a hand already is on a phone, and
|
|
11
|
+
// one line because the decision is small: keep this version, or take the other.
|
|
12
|
+
// Discarding arms before it fires, since it is the only control here that destroys
|
|
13
|
+
// work with no undo, and the page's own CSS is treated as hostile throughout.
|
|
14
|
+
|
|
15
|
+
const BG = "var(--clay-conflict-bg,#222)";
|
|
16
|
+
const INK = "var(--clay-conflict-ink,#fff)";
|
|
17
|
+
const EDGE = "var(--clay-conflict-edge,rgba(255,255,255,.28))";
|
|
18
|
+
const WARN = "var(--clay-conflict-warn,#e3a33f)";
|
|
19
|
+
const FONT = "14px/1.45 system-ui,-apple-system,'Segoe UI',sans-serif";
|
|
20
|
+
|
|
21
|
+
// The host names what moved the file when it knows. It often cannot: a plain
|
|
22
|
+
// filesystem write has no author. Anything unrecognised falls back to the phrase
|
|
23
|
+
// that is true in every case.
|
|
24
|
+
const SOURCES = {
|
|
25
|
+
"another-tab": "in another tab",
|
|
26
|
+
"another-person": "by someone else",
|
|
27
|
+
"an-agent": "by an agent",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ARM_MS = 5000;
|
|
31
|
+
|
|
32
|
+
let root = null, line = null, keep = null, drop = null;
|
|
33
|
+
let armTimer = null, armed = false, busy = false;
|
|
34
|
+
|
|
35
|
+
const still = () => !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
36
|
+
|
|
37
|
+
// all:initial first, because a page restyling every button is the normal case, not
|
|
38
|
+
// the adversarial one. Everything the control needs is restated after it.
|
|
39
|
+
function button(label, rules) {
|
|
40
|
+
const b = make("button", [
|
|
41
|
+
"all:initial", "box-sizing:border-box", "cursor:pointer", `font:${FONT}`,
|
|
42
|
+
"font-weight:500", "border-radius:6px", "padding:6px 12px", "white-space:nowrap",
|
|
43
|
+
"flex:none", ...rules,
|
|
44
|
+
], label);
|
|
45
|
+
b.type = "button";
|
|
46
|
+
b.addEventListener("focus", () => {
|
|
47
|
+
set(b, "outline", `2px solid ${WARN}`);
|
|
48
|
+
set(b, "outline-offset", "2px");
|
|
49
|
+
});
|
|
50
|
+
b.addEventListener("blur", () => set(b, "outline", "none"));
|
|
51
|
+
return b;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to
|
|
55
|
+
// the layout viewport, which parks a bottom-anchored bar behind the keyboard at the
|
|
56
|
+
// exact moment somebody is typing. Lift it by the difference instead.
|
|
57
|
+
function place() {
|
|
58
|
+
if (!root) return;
|
|
59
|
+
const vv = window.visualViewport;
|
|
60
|
+
const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
|
|
61
|
+
set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function build() {
|
|
65
|
+
root = make("div", [
|
|
66
|
+
"position:fixed", "left:50%", "transform:translateX(-50%)",
|
|
67
|
+
"z-index:2147483001", "display:flex", "align-items:center", "gap:10px",
|
|
68
|
+
"max-width:calc(100vw - 24px)", "flex-wrap:wrap", "justify-content:center",
|
|
69
|
+
"padding:9px 12px", "border-radius:10px",
|
|
70
|
+
`background:${BG}`, `color:${INK}`, `border:1px solid ${EDGE}`,
|
|
71
|
+
"box-shadow:0 6px 24px rgba(0,0,0,.32),0 1px 2px rgba(0,0,0,.24)",
|
|
72
|
+
`font:${FONT}`, "text-align:left",
|
|
73
|
+
]);
|
|
74
|
+
root.setAttribute("clay", "no-save no-watch no-snapshot");
|
|
75
|
+
root.setAttribute("data-clay-conflict", "");
|
|
76
|
+
root.setAttribute("role", "alert");
|
|
77
|
+
|
|
78
|
+
line = make("span", ["margin-right:2px"]);
|
|
79
|
+
drop = button("Load theirs", [`color:${INK}`, "opacity:.72", "padding:6px 8px"]);
|
|
80
|
+
keep = button("Keep mine", [`background:${INK}`, `color:${BG}`, "font-weight:600"]);
|
|
81
|
+
|
|
82
|
+
drop.addEventListener("click", onDrop);
|
|
83
|
+
keep.addEventListener("click", onKeep);
|
|
84
|
+
root.append(line, drop, keep);
|
|
85
|
+
|
|
86
|
+
if (!still()) {
|
|
87
|
+
set(root, "transition", "opacity .18s");
|
|
88
|
+
set(root, "opacity", "0");
|
|
89
|
+
}
|
|
90
|
+
document.body.appendChild(root);
|
|
91
|
+
place();
|
|
92
|
+
window.visualViewport?.addEventListener("resize", place);
|
|
93
|
+
window.visualViewport?.addEventListener("scroll", place);
|
|
94
|
+
requestAnimationFrame(() => set(root, "opacity", "1"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// One arming click before the reload. A first click is a choice, not a confirmation
|
|
98
|
+
// of anything, and this is the only control here that throws away work nothing can
|
|
99
|
+
// bring back. It disarms itself so a bar left open does not stay one stray tap from
|
|
100
|
+
// discarding an afternoon.
|
|
101
|
+
function onDrop() {
|
|
102
|
+
if (armed) { window.location.reload(); return; }
|
|
103
|
+
armed = true;
|
|
104
|
+
drop.textContent = "Yes, drop my edits";
|
|
105
|
+
set(drop, "opacity", "1");
|
|
106
|
+
set(drop, "color", WARN);
|
|
107
|
+
set(drop, "box-shadow", `inset 0 0 0 1px ${WARN}`);
|
|
108
|
+
clearTimeout(armTimer);
|
|
109
|
+
armTimer = setTimeout(disarm, ARM_MS);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function disarm() {
|
|
113
|
+
clearTimeout(armTimer);
|
|
114
|
+
if (!armed) return;
|
|
115
|
+
armed = false;
|
|
116
|
+
drop.textContent = "Load theirs";
|
|
117
|
+
set(drop, "opacity", ".72");
|
|
118
|
+
set(drop, "color", INK);
|
|
119
|
+
set(drop, "box-shadow", "none");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function onKeep() {
|
|
123
|
+
if (busy) return;
|
|
124
|
+
busy = true;
|
|
125
|
+
disarm();
|
|
126
|
+
keep.textContent = "Saving…";
|
|
127
|
+
set(keep, "opacity", ".65");
|
|
128
|
+
try {
|
|
129
|
+
const result = await window.clay?.save?.overwrite?.();
|
|
130
|
+
if (result && result.ok === false) throw new Error(result.msg || "");
|
|
131
|
+
} catch {
|
|
132
|
+
// Left showing: the save still has not happened, and clearing the bar here
|
|
133
|
+
// would put the page back to looking like one that is saving normally.
|
|
134
|
+
line.textContent = "That did not save either. Your edits are still here.";
|
|
135
|
+
keep.textContent = "Try again";
|
|
136
|
+
set(keep, "opacity", "1");
|
|
137
|
+
busy = false;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
busy = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function onKeydown(e) {
|
|
144
|
+
if (e.key === "Escape" && armed) { e.preventDefault(); drop.focus(); disarm(); }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function show(e) {
|
|
148
|
+
if (!root) build();
|
|
149
|
+
set(root, "display", "flex");
|
|
150
|
+
const named = SOURCES[e?.detail?.changedBy];
|
|
151
|
+
// Reassurance first, then the fact. What a person needs to know at this moment
|
|
152
|
+
// is that nothing of theirs is gone and nothing is about to be overwritten; the
|
|
153
|
+
// detail of what happened is the second half of the sentence, not the first.
|
|
154
|
+
// "Saving is paused" led here once and read as a failure, which it is not: the
|
|
155
|
+
// page is holding a version, and the two buttons below are the whole decision.
|
|
156
|
+
//
|
|
157
|
+
// An earlier save of this tab's timed out and the host could not say what became
|
|
158
|
+
// of it, so this refusal may be answering that write rather than anybody else's.
|
|
159
|
+
// Saying so is the whole reason the stamp is kept across a timeout instead of
|
|
160
|
+
// quietly reconciled. A host that NAMED the writer, or that answered the question
|
|
161
|
+
// with a receipt, knows better than this guess, so the name wins.
|
|
162
|
+
if (!named && e?.detail?.afterTimeout) {
|
|
163
|
+
line.textContent =
|
|
164
|
+
"This page changed elsewhere, possibly by your own save that timed out. " +
|
|
165
|
+
"Your edits here are safe, and nothing will be overwritten until you choose.";
|
|
166
|
+
} else {
|
|
167
|
+
line.textContent =
|
|
168
|
+
`This page changed ${named || "elsewhere"}. ` +
|
|
169
|
+
"Your edits here are safe, and nothing will be overwritten until you choose.";
|
|
170
|
+
}
|
|
171
|
+
keep.textContent = "Keep mine";
|
|
172
|
+
set(keep, "opacity", "1");
|
|
173
|
+
busy = false;
|
|
174
|
+
disarm();
|
|
175
|
+
place();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hide() {
|
|
179
|
+
if (!root) return;
|
|
180
|
+
disarm();
|
|
181
|
+
set(root, "display", "none");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function init() {
|
|
185
|
+
if (!isEditMode) return;
|
|
186
|
+
document.addEventListener("clay:save-conflict", show);
|
|
187
|
+
document.addEventListener("clay:save-saved", hide);
|
|
188
|
+
document.addEventListener("keydown", onKeydown);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onDomReady(init);
|