@panphora/clayjs 0.7.4 → 1.1.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.
Files changed (39) hide show
  1. package/README.md +99 -85
  2. package/THIRD-PARTY-NOTICES.md +2 -4
  3. package/dist/clay.standalone.js +22030 -0
  4. package/{all.js → entries/all.js} +7 -1
  5. package/entries/clay-data.js +16 -0
  6. package/{clay-dom.js → entries/clay-dom.js} +7 -1
  7. package/{clay-events.js → entries/clay-events.js} +7 -1
  8. package/{clay-internals.js → entries/clay-internals.js} +7 -1
  9. package/{clay-options.js → entries/clay-options.js} +7 -1
  10. package/{clay-ui.js → entries/clay-ui.js} +7 -1
  11. package/{clay-utils.js → entries/clay-utils.js} +7 -1
  12. package/{clay.js → entries/clay.js} +15 -1
  13. package/package.json +46 -15
  14. package/src/core/autosave.js +3 -2
  15. package/src/core/etag.js +114 -0
  16. package/src/core/host-attrs.js +7 -2
  17. package/src/core/host-meta.js +20 -3
  18. package/src/core/persist.js +3 -2
  19. package/src/core/save-conflict-notice.js +196 -0
  20. package/src/core/save-core.js +60 -3
  21. package/src/core/save.js +80 -1
  22. package/src/core/snapshot.js +34 -17
  23. package/src/lib/attr-aliases.js +13 -0
  24. package/src/lib/dirty-gate.js +2 -1
  25. package/src/lib/root-attrs.js +23 -7
  26. package/src/loader-logic.js +39 -0
  27. package/src/loader.js +11 -5
  28. package/src/plugins/demo.js +2 -2
  29. package/src/plugins/indicator.js +13 -2
  30. package/src/plugins/sortable.js +5 -5
  31. package/src/standalone.js +98 -0
  32. package/src/sync/live-sync.js +42 -0
  33. package/src/ui/index.js +7 -0
  34. package/src/vendor/hypercms.vendor.js +13 -13
  35. package/src/vendor/richclay.vendor.js +15 -15
  36. package/_headers +0 -14
  37. package/clay-data.js +0 -16
  38. package/src/lib/load-vendor-script.js +0 -57
  39. /package/{sap.js → entries/sap.js} +0 -0
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.all = import(base + "/src/dom/all.js")
15
21
  .then(function (m) {
@@ -0,0 +1,16 @@
1
+ // GENERATED — do not edit. Vendored from
2
+ // hyper-html-api/dist/hyper-html-api.engine.min.js via hyper-html-api
3
+ // `npm run copy-to-clayjs`. Edit the hyper-html-api source and re-run.
4
+ (function () {
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
+ window.clay = window.clay || {};
7
+ window.clay.extractData = hyperHtmlApiData.extractData;
8
+ // clayjs-level sugar: applyData(data) writes into the whole document, matching
9
+ // extractData's root sniff. Explicit-root applyData(el, data, source?) passes through.
10
+ window.clay.applyData = function (root, data, source) {
11
+ if (root && typeof root.nodeType === "number") return hyperHtmlApiData.applyData(root, data, source);
12
+ return hyperHtmlApiData.applyData(document, root, data);
13
+ };
14
+ window.clay.loaded = window.clay.loaded || {};
15
+ window.clay.loaded.data = Promise.resolve();
16
+ })();
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.dom = import(base + "/src/dom/dom-helpers.js")
15
21
  .catch(function (err) { console.error("clay-dom failed to load:", err); throw err; });
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // The mutation-backed attrs subscribe to a hub that observes document.body, so
15
21
  // wait for DOM ready before importing (same gate as clay.js).
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // No DOM gate: nothing here touches document.body at import time, unlike clay-ui.
15
21
  clay.loaded.internals = import(base + "/src/internals/index.js")
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // Subscribes to the mutation hub (observes document.body); wait for DOM ready.
15
21
  function domReady() {
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  // Toasts and dialogs append to document.body, so `loaded.ui` must not resolve
15
21
  // before the DOM is ready (same gate as clay-events).
@@ -9,7 +9,13 @@
9
9
  return;
10
10
  }
11
11
  var url = new URL(script.src, location.href);
12
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
12
+ // Query and fragment off first, then step out of the tarball's entries/ directory
13
+ // so package-path CDNs resolve src/. See clay.js for the full reason on both.
14
+ var path = url.href.split("#")[0].split("?")[0];
15
+ var base = path.slice(0, path.lastIndexOf("/"));
16
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
17
+ base = base.slice(0, -8);
18
+ }
13
19
 
14
20
  clay.loaded.utils = import(base + "/src/utils/index.js")
15
21
  .catch(function (err) { console.error("clay-utils failed to load:", err); throw err; });
@@ -31,7 +31,21 @@
31
31
  clay.__booted = true;
32
32
 
33
33
  var url = new URL(script.src, location.href);
34
- var base = url.href.slice(0, url.href.lastIndexOf("/"));
34
+ // The query and the fragment come off before the last slash is found. Leaving them
35
+ // on split the base inside them, so clay.js?next=/a/b imported ".../clay.js?next=/a/src/loader.js"
36
+ // and the library never booted.
37
+ var path = url.href.split("#")[0].split("?")[0];
38
+ var base = path.slice(0, path.lastIndexOf("/"));
39
+ // In the npm tarball the entry scripts sit in entries/ while src/ stays beside it
40
+ // at the package root, so a CDN that serves package paths literally (jsDelivr,
41
+ // unpkg) would look for src/ one directory too deep and load nothing. Stepping out
42
+ // of an "entries" segment makes those URLs resolve. clayjs.com never has the
43
+ // segment: build.js flattens entries/ into each version prefix, so this is a no-op
44
+ // there. The question is asked of the pathname, because asking it of the whole URL
45
+ // matched a host merely named "entries" and sent the import to another origin.
46
+ if (url.pathname.slice(0, url.pathname.lastIndexOf("/")).slice(-8) === "/entries") {
47
+ base = base.slice(0, -8);
48
+ }
35
49
  import(base + "/src/loader.js")
36
50
  .then(function (m) { return m.boot(base, url.searchParams, clay.__readyResolve); })
37
51
  .catch(function (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "0.7.4",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable HTML files. Save lifecycle for self-saving HTML.",
6
6
  "license": "MIT-0",
@@ -9,18 +9,35 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/panphora/clayjs.git"
11
11
  },
12
+ "bugs": {
13
+ "url": "https://github.com/panphora/clayjs/issues"
14
+ },
15
+ "keywords": [
16
+ "html",
17
+ "self-saving",
18
+ "malleable",
19
+ "local-first",
20
+ "no-build",
21
+ "contenteditable",
22
+ "dom",
23
+ "autosave",
24
+ "single-file",
25
+ "static-site",
26
+ "vanilla-javascript",
27
+ "html-editor"
28
+ ],
29
+ "exports": {
30
+ "./package.json": "./package.json",
31
+ "./THIRD-PARTY-NOTICES.md": "./THIRD-PARTY-NOTICES.md",
32
+ "./src/*": "./src/*",
33
+ "./entries/*": "./entries/*",
34
+ "./clay.standalone.js": "./dist/clay.standalone.js",
35
+ "./dist/*": "./dist/*",
36
+ "./*": "./entries/*"
37
+ },
12
38
  "files": [
13
- "clay.js",
14
- "clay-ui.js",
15
- "clay-internals.js",
16
- "clay-events.js",
17
- "clay-options.js",
18
- "clay-dom.js",
19
- "all.js",
20
- "clay-utils.js",
21
- "sap.js",
22
- "clay-data.js",
23
- "_headers",
39
+ "entries/",
40
+ "dist/",
24
41
  "THIRD-PARTY-NOTICES.md",
25
42
  "src/"
26
43
  ],
@@ -29,9 +46,12 @@
29
46
  },
30
47
  "scripts": {
31
48
  "test": "NODE_OPTIONS=--experimental-vm-modules jest",
32
- "test:conformance": "UPDATE_GOLDENS=0 web-test-runner",
33
- "conformance:update": "UPDATE_GOLDENS=1 web-test-runner",
34
- "prepublishOnly": "npm test && npm run test:conformance",
49
+ "test:conformance": "UPDATE_GOLDENS=0 web-test-runner --config conformance/wtr.config.mjs",
50
+ "conformance:update": "UPDATE_GOLDENS=1 web-test-runner --config conformance/wtr.config.mjs",
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",
35
55
  "dev": "node tests/stub-server.mjs",
36
56
  "build": "node build.js",
37
57
  "deploy": "node build.js && wrangler deploy && git rev-parse HEAD > .deploy"
@@ -41,6 +61,7 @@
41
61
  "@web/test-runner": "^0.20.2",
42
62
  "@web/test-runner-commands": "^0.9.0",
43
63
  "@web/test-runner-playwright": "^0.11.1",
64
+ "esbuild": "^0.28.2",
44
65
  "jest": "^29.7.0",
45
66
  "jest-environment-jsdom": "^29.7.0",
46
67
  "playwright": "1.62.1"
@@ -49,5 +70,15 @@
49
70
  "kind": "lib",
50
71
  "status": "active",
51
72
  "url": "https://clayjs.com"
73
+ },
74
+ "jest": {
75
+ "testEnvironment": "jsdom",
76
+ "testMatch": [
77
+ "**/tests/unit/**/*.test.js"
78
+ ],
79
+ "transform": {},
80
+ "setupFiles": [
81
+ "<rootDir>/tests/jest.setup.js"
82
+ ]
52
83
  }
53
84
  }
@@ -15,6 +15,7 @@ import { isEditMode } from "./is-edit-mode.js";
15
15
  import { savePageThrottled } from "./save.js";
16
16
  import { markUserDriven } from "../lib/user-gesture.js";
17
17
  import { resolveRegionPolicy, skipForPolicy } from "../lib/region-policy.js";
18
+ import { PERSIST, AUTOSAVE } from "../lib/attr-aliases.js";
18
19
 
19
20
  /**
20
21
  * Initialize auto-save on DOM changes
@@ -47,7 +48,7 @@ function initSavePageOnChange() {
47
48
  let inputSaveTimer = null;
48
49
  function initSaveOnPersistInput() {
49
50
  document.addEventListener('input', (e) => {
50
- if (!e.target.closest('[persist]')) return;
51
+ if (!e.target.closest(PERSIST)) return;
51
52
  // A trusted input on a [persist] field is itself a user-driven change the
52
53
  // Mutation hub can't see (form values aren't DOM mutations). Attribute it,
53
54
  // but only when the field is inside the dirty domain: a [persist] control in
@@ -62,7 +63,7 @@ function initSaveOnPersistInput() {
62
63
  }
63
64
 
64
65
  function init() {
65
- if (!document.documentElement.hasAttribute("autosave")) return;
66
+ if (!document.documentElement.matches(AUTOSAVE)) return;
66
67
  if (!isEditMode) return;
67
68
  // initUserGesture moved to save.js's init: gesture provenance belongs to every
68
69
  // editable page, not only the ones with <html autosave>.
@@ -0,0 +1,114 @@
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 clears it and asks for a fresh one, because
19
+ * the file changed under a tab that never saved.
20
+ *
21
+ * A peer's SNAPSHOT does not move it. §10 relays never write to disk, so the
22
+ * stamp is still true after one lands, and treating one as a disk change would
23
+ * refetch discovery on every keystroke another editor makes.
24
+ *
25
+ * No stamp means no `If-Match`, which means last write wins for that one save.
26
+ * That is what every save did before this module existed, so an absent stamp is
27
+ * always a step back to the old behaviour and never a new failure.
28
+ */
29
+
30
+ import { hostMeta } from "./host-meta.js";
31
+ import { isEditMode } from "./is-edit-mode.js";
32
+
33
+ let lastSeen = null;
34
+ let conditional = false;
35
+ // Bumped by every write. A discovery answer that resolves after a save has
36
+ // already recorded a newer stamp must not overwrite it.
37
+ let generation = 0;
38
+
39
+ /** The stamp to send on the next save, or null. */
40
+ export function lastSeenEtag() {
41
+ return lastSeen;
42
+ }
43
+
44
+ /** True only when the host announced `conditional` by name (§5). */
45
+ export function conditionalSaves() {
46
+ return conditional;
47
+ }
48
+
49
+ /**
50
+ * Take the stamp from a save response. A response with no stamp clears it.
51
+ * @param {?string} value
52
+ */
53
+ export function recordEtag(value) {
54
+ lastSeen = typeof value === "string" && value !== "" ? value : null;
55
+ generation++;
56
+ }
57
+
58
+ /** Forget the stamp: the next save goes out unconditional. */
59
+ export function forgetEtag() {
60
+ recordEtag(null);
61
+ }
62
+
63
+ /**
64
+ * Learn from discovery whether this host offers conditional saves, and what the
65
+ * document's current stamp is.
66
+ *
67
+ * @param {Object} [options]
68
+ * @param {boolean} [options.fresh] - Ask the host again instead of reusing the
69
+ * memoized answer, and take whatever it says as the whole truth: an answer with
70
+ * no stamp in it clears the one held here rather than leaving a stale value the
71
+ * host would refuse forever.
72
+ * @param {boolean} [options.clearIfMissing] - Whether an answer carrying no stamp
73
+ * drops the one held here. Defaults to `fresh`, which is what a person asking to
74
+ * overwrite wants: no stamp means the next save goes out unconditional, which is
75
+ * last write wins, which is what they asked for by name. Pass false for a refresh
76
+ * nobody requested. Clearing fails OPEN, and a background reseed that quietly
77
+ * turns the guard off is worse than one that leaves a stale stamp behind, because
78
+ * a stale stamp only costs a refusal.
79
+ * @returns {Promise<?string>}
80
+ */
81
+ export async function seedEtag({ fresh = false, clearIfMissing = fresh } = {}) {
82
+ const at = generation;
83
+ const meta = await hostMeta({ fresh });
84
+ conditional = meta.extensions.includes("conditional");
85
+
86
+ if (generation !== at) return lastSeen;
87
+
88
+ const seed = meta.document?.etag;
89
+ if (typeof seed === "string" && seed !== "") {
90
+ lastSeen = seed;
91
+ generation++;
92
+ } else if (clearIfMissing) {
93
+ lastSeen = null;
94
+ generation++;
95
+ }
96
+ return lastSeen;
97
+ }
98
+
99
+ // A disk-sourced frame is the one live-sync outcome that changes the file under a
100
+ // tab which did not save it. Holding the old stamp would refuse this tab's next
101
+ // save against bytes it has already morphed to and is looking at, so the stamp
102
+ // goes immediately and the replacement is asked for in the background: the save
103
+ // lane never waits on it.
104
+ //
105
+ // A frame that live-sync HELD is deliberately not covered, because it never
106
+ // dispatches this event. That tab has unsaved local edits and has not seen the
107
+ // new disk bytes, which is precisely the case a 412 exists for.
108
+ if (isEditMode) {
109
+ document.addEventListener("clay:sync-applied", (event) => {
110
+ if (event.detail?.source !== "disk") return;
111
+ forgetEtag();
112
+ seedEtag({ fresh: true });
113
+ });
114
+ }
@@ -7,15 +7,20 @@
7
7
  * cannot drift between the edit-mode ladder and the save lane.
8
8
  */
9
9
 
10
- import { HOST_TOKEN_ATTRS } from "../lib/root-attrs.js";
10
+ import { SAVE_TOKEN_ATTRS } from "../lib/root-attrs.js";
11
11
 
12
12
  /**
13
13
  * The per-document save token this response carries, or null.
14
+ *
15
+ * Walks SAVE_TOKEN_ATTRS and not the wider HOST_TOKEN_ATTRS: whatever comes back
16
+ * is put straight into the save URL and gates edit mode, so a durable file
17
+ * identity in that list would be treated as a capability it is not.
18
+ *
14
19
  * @returns {?string}
15
20
  */
16
21
  export function saveToken() {
17
22
  if (typeof document === "undefined") return null;
18
- for (const attr of HOST_TOKEN_ATTRS) {
23
+ for (const attr of SAVE_TOKEN_ATTRS) {
19
24
  const value = document.documentElement.getAttribute(attr);
20
25
  if (value) return value;
21
26
  }
@@ -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 (!inFlight) inFlight = fetchMeta().catch(bareHost);
37
- return inFlight;
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() {
@@ -1,5 +1,6 @@
1
1
  import { onSnapshot } from './snapshot.js';
2
2
  import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/control-serialize.vendor.js';
3
+ import { PERSIST } from '../lib/attr-aliases.js';
3
4
 
4
5
  // Persistent Form Input Values
5
6
  //
@@ -51,7 +52,7 @@ import { serializeControlToAttributes, finalizeControlForSave } from '../vendor/
51
52
  // When event-attrs is loaded, its cloneNode() intercept (onclone.js) patches
52
53
  // data-value into textContent on cloned textareas automatically.
53
54
 
54
- export default function enablePersistentFormInputValues(filterBySelector = "[persist]") {
55
+ export default function enablePersistentFormInputValues(filterBySelector = PERSIST) {
55
56
  const inputSelector = `input${filterBySelector}:not([type="password"]):not([type="hidden"]):not([type="file"])`;
56
57
  const textareaSelector = `textarea${filterBySelector}`;
57
58
  const selectSelector = `select${filterBySelector}`;
@@ -106,7 +107,7 @@ export default function enablePersistentFormInputValues(filterBySelector = "[per
106
107
 
107
108
  // Auto-initialize with default selector
108
109
  export function init() {
109
- enablePersistentFormInputValues("[persist]");
110
+ enablePersistentFormInputValues(PERSIST);
110
111
  }
111
112
 
112
113
  // Auto-init when module is imported