domma-cms 1.10.16 → 1.10.18

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.
@@ -1,4 +1,4 @@
1
- import{apiRequest as A}from"/admin/js/api.js";import{addBannerAction as Y}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as De,whileLoading as Le}from"/admin/js/lib/loading.js";import{helpPopovers as me}from"/admin/js/lib/help-popover.js";import{onceClosed as Re,openMenuAt as pe,sidebarCount as Q,viewLifetime as Ie}from"/admin/js/lib/tool-kit.js";import{FILTERS as Oe,answerFields as qe,answerText as q,arrangeSubmissions as Ue,submissionCounts as Fe,submissionRow as L,submissionText as je}from"/admin/js/lib/form-submissions-arrange.js";import{fmtDateTime as Pe}from"../lib/dates.js";import{openEmail as Be}from"../lib/mail-compose.js";const ue=/^#\/forms\/[^/?#]+\/submissions\/?(\?.*)?$/,Z="form-submissions.filter",Ke=`
1
+ import{apiRequest as R}from"/admin/js/api.js";import{addBannerAction as Q}from"/admin/js/lib/plugin-chrome.js";import{SHAPES as Le,whileLoading as Re}from"/admin/js/lib/loading.js";import{helpPopovers as me}from"/admin/js/lib/help-popover.js";import{onceClosed as Oe,openMenuAt as pe,sidebarCount as Z,viewLifetime as Ie}from"/admin/js/lib/tool-kit.js";import{FILTERS as qe,answerFields as Ue,answerText as F,arrangeSubmissions as Fe,fileRefsOf as je,safeFileHref as ze,submissionCounts as Be,submissionRow as O,submissionText as Pe}from"/admin/js/lib/form-submissions-arrange.js";import{fmtDateTime as Ke}from"../lib/dates.js";import{openEmail as _e}from"../lib/mail-compose.js";const ue=/^#\/forms\/[^/?#]+\/submissions\/?(\?.*)?$/,ee="form-submissions.filter",He=`
2
2
  <div class="fsx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
3
3
  <span class="fsx-dot" aria-hidden="true"></span>
4
4
  <span class="fsx-main"><strong>{{title}}</strong><small>{{meta}}</small></span>
@@ -8,4 +8,4 @@ import{apiRequest as A}from"/admin/js/api.js";import{addBannerAction as Y}from"/
8
8
  </span>
9
9
  <span class="fsx-when" title="{{whenFull}}">{{when}}</span>
10
10
  <button type="button" class="fsx-kebab" aria-label="Actions for this submission" title="Actions">\u22EE</button>
11
- </div>`,ee=i=>String(i??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;"),U=(i,r,y=`${r}s`)=>`${i} ${i===1?r:y}`,te=i=>i?Pe(i):"";async function fe(i){try{return await navigator.clipboard.writeText(i),!0}catch{}try{const r=document.createElement("textarea");r.value=i,r.style.cssText="position:fixed;opacity:0;",document.body.appendChild(r),r.select();const y=document.execCommand("copy");return r.remove(),y}catch{return!1}}const b=Ie(ue);export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(i){b.reset();const r=i.get(0).querySelector(".fsx")||i.get(0),y=()=>ue.test(location.hash)&&r.isConnected,u=decodeURIComponent((location.hash.match(/^#\/forms\/([^/?#]+)\/submissions/)||[])[1]||"");if(!u){E.toast("No form selected.",{type:"error"});return}const k=encodeURIComponent(u),w=M.observable(null),l=M.observable([]),ne=M.observable(!0),F=M.observable("");let se="inbox";try{se=Oe.includes(S.get(Z))?S.get(Z):"inbox"}catch{}const v=M.observable(se),oe=M.observable(""),j=M.observable(""),P=M.observable("");b.add(M.effect(()=>{const e=v.value;try{S.set(Z,e)}catch{}}));const $=M.computed(()=>qe(w.value||{})),B=M.observable([]);b.add(M.effect(()=>{const e=Date.now(),t=Ue(l.value,{filter:v.value,query:oe.value,from:j.value,to:P.value,now:e}),n=$.value;B.value=t.map(a=>{const s=L(a,n,e);return{...s,when:s.at?D(s.at).fromNow():"",whenFull:te(s.at)}})}));const he=M.computed(()=>Fe(l.value)),be=M.computed(()=>{const e=B.value.length,t=l.value.length;return t?e===t?U(t,"submission"):`${e} of ${U(t,"submission")}`:""}),ye=M.computed(()=>l.value.length?v.value==="spam"?"Nothing marked as spam.":"No submissions match.":"No submissions yet - they appear here as people send the form."),we=M.computed(()=>w.value?`What people sent through "${w.value.title||u}".`:"What people sent through this form."),R=e=>l.peek().find(t=>String(t.id)===String(e));async function Ee(){try{const[e,t]=await Le(r.querySelector(".dm-loading-slot"),Promise.all([A(`/forms/${k}`),A(`/forms/${k}/submissions`)]),De.rows);if(!y())return;w.value=e||null,l.value=Array.isArray(t)?t:[],F.value="",ve(e?.title||u)}catch(e){if(!y())return;F.value=`Could not load the submissions: ${e.message||e}`}finally{ne.value=!1}}function ve(e){const t=i.get(0).querySelector(".dm-plugin-banner-name"),n=t?.lastChild;n&&n.nodeType===Node.TEXT_NODE&&t.querySelector(".dm-plugin-banner-sep")&&(n.textContent=`${e} submissions`),document.title=`${e} - Submissions - Domma CMS`}const K=(e,t)=>ce(e.id,()=>xe(e,t)),W=e=>ce(e.id,()=>ge(e));async function xe(e,t){try{const n=await A(`/forms/${k}/submissions/${encodeURIComponent(e.id)}/spam`,{method:"POST",body:JSON.stringify({spam:t})});return l.value=l.peek().map(a=>String(a.id)===String(e.id)?{...a,data:n?.entry?.data||{...a.data,spam:t}}:a),E.toast(t?"Marked as spam - it is under Spam now.":"Back in the inbox.",{type:"success"}),Q("#/forms"),!0}catch(n){return E.toast(`Not changed: ${n.message||n}`,{type:"error"}),!1}}async function ge(e){const t=L(e,$.peek()).title;if(!await E.confirm(`Delete the submission from "${ee(t)}"? This cannot be undone.`,{confirmText:"Delete"}))return!1;try{return await A(`/forms/${k}/submissions/${encodeURIComponent(e.id)}`,{method:"DELETE"}),l.value=l.peek().filter(n=>String(n.id)!==String(e.id)),E.toast("Submission deleted.",{type:"success"}),Q("#/forms"),!0}catch(n){return E.toast(`Not deleted: ${n.message||n}`,{type:"error"}),!1}}let _=!1;async function Ce(){if(!_){_=!0;try{await ke()}finally{_=!1}}}async function ke(){const e=l.peek().length;if(!e){E.toast("There are no submissions to clear.",{type:"info"});return}const t=w.peek()?.title||u;if(await E.confirm(`Delete all ${U(e,"submission")} to "${ee(t)}", spam included? This cannot be undone - Export them first if you may need them.`))try{await A(`/forms/${k}/submissions`,{method:"DELETE"}),l.value=[],E.toast("All submissions cleared.",{type:"success"}),Q("#/forms")}catch(n){E.toast(`Not cleared: ${n.message||n}`,{type:"error"})}}const ae=async e=>{const t=je(e,$.peek(),{formTitle:w.peek()?.title||u,when:te(e.meta?.createdAt)});E.toast(await fe(t)?"Copied.":"Copy failed.",{type:"info"})},Se=async e=>{E.toast(await fe(e)?`Copied ${e}.`:"Copy failed.",{type:"info"})},ie=e=>{const t=L(e,$.peek()).email;t&&Be({to:[t],subject:`Re: ${w.peek()?.title||u}`})};async function $e(e,t){try{const n=await fetch(`/api/forms/${k}/submissions/${e}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);const a=await n.blob(),s=document.createElement("a");return s.href=URL.createObjectURL(a),s.download=t,document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(s.href),1e3),!0}catch(n){return E.toast(`Export failed: ${n.message||n}`,{type:"error"}),!1}}const H=new Set,O=new Set;function re(e,t,n,a="md"){if(O.has(e))return null;O.add(e);let s;try{s=E.slideover({title:ee(t),size:a,position:"right"}),s.element.appendChild(n);const m=me(n);return H.add(s),Re(s,()=>{O.delete(e),H.delete(s),m()}),I.scan(n),s.open(),s}catch(m){O.delete(e);try{s?.destroy?.(),s?.element?.remove()}catch{}return E.toast(`Could not open it: ${m.message||m}`,{type:"error"}),null}}const J=new Set;async function ce(e,t){const n=String(e);if(J.has(n))return!1;J.add(n);try{return await t()}finally{J.delete(n)}}function Ne(){const e=l.peek().length,t=document.createElement("div");t.className="fsx-export";const n=document.createElement("p");n.className="fsx-note",n.textContent=e?`All ${U(e,"submission")}, spam included.`:"There are no submissions yet - the file will be empty.",t.appendChild(n);let a;const s=(m,x,z,G,N)=>{const c=document.createElement("button");c.type="button",c.className="btn btn-ghost";const g=document.createElement("span");g.setAttribute("data-icon",x),c.append(g,document.createTextNode(` ${m}`));const T=document.createElement("span");T.setAttribute("data-help",N),c.addEventListener("click",async()=>{if(!c.disabled){c.disabled=!0;try{await $e(z,`${u}-submissions.${G}`)&&a?.close()}finally{c.disabled=!1}}});const o=document.createElement("div");o.append(c,T),t.appendChild(o)};s("Export as CSV","file-text","export","csv","One row per submission, one column per question - opens in Excel, Numbers or Google Sheets."),s("Export as JSON","code","export/json","json","Every submission exactly as stored, with its id and dates - for importing elsewhere or keeping a full copy."),a=re("export","Export submissions",t,"sm")}function V(e){if(!e)return;const t=$.peek(),n=L(e,t),a=document.createElement("div");a.className="fsx-detail";const s=document.createElement("div");s.className="fsx-facts";const m=o=>{const d=document.createElement("span");d.textContent=o,s.appendChild(d)};e.meta?.createdAt&&m(`Sent ${te(e.meta.createdAt)} (${D(e.meta.createdAt).fromNow()})`),e.meta?.ip&&m(`IP ${e.meta.ip}`),n.spam&&m("Marked as spam"),a.appendChild(s);const x=document.createElement("dl");x.className="fsx-answers";const z=t.filter(o=>q(e.data?.[o.name])!=="");for(const o of z){const d=document.createElement("div");d.className="fsx-answer";const h=document.createElement("dt");h.textContent=o.label;const C=document.createElement("dd");C.textContent=q(e.data[o.name]),d.append(h,C),x.appendChild(d)}const G=new Set(t.map(o=>o.name));for(const[o,d]of Object.entries(e.data||{})){if(G.has(o)||o==="spam"||q(d)==="")continue;const h=document.createElement("div");h.className="fsx-answer";const C=document.createElement("dt");C.textContent=`${o} (no longer on the form)`;const p=document.createElement("dd");p.textContent=q(d),h.append(C,p),x.appendChild(h)}if(x.children.length)a.appendChild(x);else{const o=document.createElement("p");o.className="fsx-note",o.textContent="Nothing was filled in.",a.appendChild(o)}const N=document.createElement("div");N.className="fsx-actions";const c=(o,d,h,C)=>{const p=document.createElement("button");p.type="button",p.className=`btn btn-sm ${h}`;const X=document.createElement("span");return X.setAttribute("data-icon",d),X.setAttribute("data-icon-size","13"),p.append(X,document.createTextNode(` ${o}`)),p.addEventListener("click",C),N.appendChild(p),p};let g;c("Copy","copy","btn-ghost",()=>ae(e)),n.email&&c("Email them","mail","btn-ghost",()=>ie(e)),c(n.spam?"Not spam":"Mark as spam",n.spam?"inbox":"alert-triangle","btn-ghost",async()=>{await K(e,!n.spam)&&g?.close()});const T=document.createElement("span");T.className="fsx-spacer",N.appendChild(T),c("Delete","trash","btn-ghost text-danger",async()=>{await W(e)&&g?.close()}),c("Close","x","btn-ghost",()=>g?.close()),a.appendChild(N),g=re(`sub:${e.id}`,n.title,a)}function Te(e){const t=R(e);if(!t)return[];const n=L(t,$.peek());return[{header:n.title},{label:"Open\u2026",icon:"eye",action:()=>V(t)},{label:"Copy submission",icon:"copy",action:()=>ae(t)},{type:"divider"},{label:n.email?`Copy ${n.email}`:"Copy email",icon:"at-sign",disabled:!n.email,action:()=>Se(n.email)},{label:"Email them\u2026",icon:"mail",disabled:!n.email,action:()=>ie(t)},{type:"divider"},n.spam?{label:"Not spam - back to the inbox",icon:"inbox",action:()=>K(t,!1)}:{label:"Mark as spam",icon:"alert-triangle",action:()=>K(t,!0)},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>W(t)}]}const f=r.querySelector(".fsx-list");f.innerHTML=Ke;const Me=M.applyBindings({loading:ne,loadError:F,filter:v,query:oe,from:j,to:P,rows:B,counts:he,summary:be,emptyText:ye,intro:we},r,{methods:{showInbox:()=>{v.value="inbox"},showNew:()=>{v.value="new"},showSpam:()=>{v.value="spam"},clearDates:()=>{j.value="",P.value=""}}});b.add(()=>Me.dispose()),b.add(me(r,{position:"bottom"}));const Ae=E.contextMenu(f,{cursor:"glyph",match:".fsx-row",inherit:!1,density:"compact",items:e=>Te(e.dataset.id)});b.add(()=>Ae?.destroy?.());const le=e=>{const t=e.target.closest(".fsx-row");if(!t)return;const n=e.target.closest(".fsx-kebab");if(n){pe(n);return}V(R(t.dataset.id))},de=e=>{const t=e.target.closest(".fsx-row");if(!(!t||e.target!==t)){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),pe(t);else if(e.key==="Enter")V(R(t.dataset.id));else if(e.key==="Delete"){e.preventDefault();const n=R(t.dataset.id);n&&W(n)}}};f.addEventListener("click",le),f.addEventListener("keydown",de),b.add(()=>{f.removeEventListener("click",le),f.removeEventListener("keydown",de)}),Y(i,{id:"form-submissions-back",icon:"arrow-left",title:"All forms",onClick:()=>{location.hash="#/forms"}}),Y(i,{id:"form-submissions-export",icon:"download",title:"Export (CSV or JSON)",onClick:Ne}),Y(i,{id:"form-submissions-clear",icon:"trash",title:"Clear all submissions",onClick:Ce}),b.add(()=>{[...H].forEach(e=>{try{e.close()}catch{}})}),I.scan(r),await Ee(),y()&&requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}};
11
+ </div>`,te=a=>String(a??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;"),j=(a,i,p=`${i}s`)=>`${a} ${a===1?i:p}`,ne=a=>a?Ke(a):"";async function fe(a){try{return await navigator.clipboard.writeText(a),!0}catch{}try{const i=document.createElement("textarea");i.value=a,i.style.cssText="position:fixed;opacity:0;",document.body.appendChild(i),i.select();const p=document.execCommand("copy");return i.remove(),p}catch{return!1}}function he(a,i){const p=je(i);if(!p.length||p.length!==(Array.isArray(i)?i.length:1)){a.textContent=F(i);return}const m=document.createElement("ul");m.className="fsx-files";for(const d of p){const u=document.createElement("li"),c=ze(d.url),y=document.createElement(c?"a":"span");if(c&&(y.href=c,y.target="_blank",y.rel="noopener"),y.textContent=F(d),u.appendChild(y),Number(d.size)>0){const N=document.createElement("small");N.textContent=` ${Number(d.size)<1024?`${d.size} B`:`${Math.round(Number(d.size)/1024)} KB`}`,u.appendChild(N)}m.appendChild(u)}a.appendChild(m)}const x=Ie(ue);export const formSubmissionsView={templateUrl:"/admin/js/templates/form-submissions.html",async onMount(a){x.reset();const i=a.get(0).querySelector(".fsx")||a.get(0),p=()=>ue.test(location.hash)&&i.isConnected,m=decodeURIComponent((location.hash.match(/^#\/forms\/([^/?#]+)\/submissions/)||[])[1]||"");if(!m){E.toast("No form selected.",{type:"error"});return}const d=encodeURIComponent(m),u=M.observable(null),c=M.observable([]),y=M.observable(!0),N=M.observable("");let se="inbox";try{se=qe.includes(S.get(ee))?S.get(ee):"inbox"}catch{}const v=M.observable(se),oe=M.observable(""),z=M.observable(""),B=M.observable("");x.add(M.effect(()=>{const e=v.value;try{S.set(ee,e)}catch{}}));const T=M.computed(()=>Ue(u.value||{})),P=M.observable([]);x.add(M.effect(()=>{const e=Date.now(),t=Fe(c.value,{filter:v.value,query:oe.value,from:z.value,to:B.value,now:e}),n=T.value;P.value=t.map(r=>{const s=O(r,n,e);return{...s,when:s.at?D(s.at).fromNow():"",whenFull:ne(s.at)}})}));const be=M.computed(()=>Be(c.value)),ye=M.computed(()=>{const e=P.value.length,t=c.value.length;return t?e===t?j(t,"submission"):`${e} of ${j(t,"submission")}`:""}),Ee=M.computed(()=>c.value.length?v.value==="spam"?"Nothing marked as spam.":"No submissions match.":"No submissions yet - they appear here as people send the form."),we=M.computed(()=>u.value?`What people sent through "${u.value.title||m}".`:"What people sent through this form."),q=e=>c.peek().find(t=>String(t.id)===String(e));async function ge(){try{const[e,t]=await Re(i.querySelector(".dm-loading-slot"),Promise.all([R(`/forms/${d}`),R(`/forms/${d}/submissions`)]),Le.rows);if(!p())return;u.value=e||null,c.value=Array.isArray(t)?t:[],N.value="",xe(e?.title||m)}catch(e){if(!p())return;N.value=`Could not load the submissions: ${e.message||e}`}finally{y.value=!1}}function xe(e){const t=a.get(0).querySelector(".dm-plugin-banner-name"),n=t?.lastChild;n&&n.nodeType===Node.TEXT_NODE&&t.querySelector(".dm-plugin-banner-sep")&&(n.textContent=`${e} submissions`),document.title=`${e} - Submissions - Domma CMS`}const K=(e,t)=>ce(e.id,()=>ve(e,t)),_=e=>ce(e.id,()=>Ce(e));async function ve(e,t){try{const n=await R(`/forms/${d}/submissions/${encodeURIComponent(e.id)}/spam`,{method:"POST",body:JSON.stringify({spam:t})});return c.value=c.peek().map(r=>String(r.id)===String(e.id)?{...r,data:n?.entry?.data||{...r.data,spam:t}}:r),E.toast(t?"Marked as spam - it is under Spam now.":"Back in the inbox.",{type:"success"}),Z("#/forms"),!0}catch(n){return E.toast(`Not changed: ${n.message||n}`,{type:"error"}),!1}}async function Ce(e){const t=O(e,T.peek()).title;if(!await E.confirm(`Delete the submission from "${te(t)}"? This cannot be undone.`,{confirmText:"Delete"}))return!1;try{return await R(`/forms/${d}/submissions/${encodeURIComponent(e.id)}`,{method:"DELETE"}),c.value=c.peek().filter(n=>String(n.id)!==String(e.id)),E.toast("Submission deleted.",{type:"success"}),Z("#/forms"),!0}catch(n){return E.toast(`Not deleted: ${n.message||n}`,{type:"error"}),!1}}let H=!1;async function ke(){if(!H){H=!0;try{await Se()}finally{H=!1}}}async function Se(){const e=c.peek().length;if(!e){E.toast("There are no submissions to clear.",{type:"info"});return}const t=u.peek()?.title||m;if(await E.confirm(`Delete all ${j(e,"submission")} to "${te(t)}", spam included? This cannot be undone - Export them first if you may need them.`))try{await R(`/forms/${d}/submissions`,{method:"DELETE"}),c.value=[],E.toast("All submissions cleared.",{type:"success"}),Z("#/forms")}catch(n){E.toast(`Not cleared: ${n.message||n}`,{type:"error"})}}const ae=async e=>{const t=Pe(e,T.peek(),{formTitle:u.peek()?.title||m,when:ne(e.meta?.createdAt)});E.toast(await fe(t)?"Copied.":"Copy failed.",{type:"info"})},$e=async e=>{E.toast(await fe(e)?`Copied ${e}.`:"Copy failed.",{type:"info"})},ie=e=>{const t=O(e,T.peek()).email;t&&_e({to:[t],subject:`Re: ${u.peek()?.title||m}`})};async function Ne(e,t){try{const n=await fetch(`/api/forms/${d}/submissions/${e}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);const r=await n.blob(),s=document.createElement("a");return s.href=URL.createObjectURL(r),s.download=t,document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(s.href),1e3),!0}catch(n){return E.toast(`Export failed: ${n.message||n}`,{type:"error"}),!1}}const W=new Set,U=new Set;function re(e,t,n,r="md"){if(U.has(e))return null;U.add(e);let s;try{s=E.slideover({title:te(t),size:r,position:"right"}),s.element.appendChild(n);const h=me(n);return W.add(s),Oe(s,()=>{U.delete(e),W.delete(s),h()}),I.scan(n),s.open(),s}catch(h){U.delete(e);try{s?.destroy?.(),s?.element?.remove()}catch{}return E.toast(`Could not open it: ${h.message||h}`,{type:"error"}),null}}const J=new Set;async function ce(e,t){const n=String(e);if(J.has(n))return!1;J.add(n);try{return await t()}finally{J.delete(n)}}function Te(){const e=c.peek().length,t=document.createElement("div");t.className="fsx-export";const n=document.createElement("p");n.className="fsx-note",n.textContent=e?`All ${j(e,"submission")}, spam included.`:"There are no submissions yet - the file will be empty.",t.appendChild(n);let r;const s=(h,C,G,X,A)=>{const l=document.createElement("button");l.type="button",l.className="btn btn-ghost";const k=document.createElement("span");k.setAttribute("data-icon",C),l.append(k,document.createTextNode(` ${h}`));const L=document.createElement("span");L.setAttribute("data-help",A),l.addEventListener("click",async()=>{if(!l.disabled){l.disabled=!0;try{await Ne(G,`${m}-submissions.${X}`)&&r?.close()}finally{l.disabled=!1}}});const o=document.createElement("div");o.append(l,L),t.appendChild(o)};s("Export as CSV","file-text","export","csv","One row per submission, one column per question - opens in Excel, Numbers or Google Sheets."),s("Export as JSON","code","export/json","json","Every submission exactly as stored, with its id and dates - for importing elsewhere or keeping a full copy."),r=re("export","Export submissions",t,"sm")}function V(e){if(!e)return;const t=T.peek(),n=O(e,t),r=document.createElement("div");r.className="fsx-detail";const s=document.createElement("div");s.className="fsx-facts";const h=o=>{const f=document.createElement("span");f.textContent=o,s.appendChild(f)};e.meta?.createdAt&&h(`Sent ${ne(e.meta.createdAt)} (${D(e.meta.createdAt).fromNow()})`),e.meta?.ip&&h(`IP ${e.meta.ip}`),n.spam&&h("Marked as spam"),r.appendChild(s);const C=document.createElement("dl");C.className="fsx-answers";const G=t.filter(o=>F(e.data?.[o.name])!=="");for(const o of G){const f=document.createElement("div");f.className="fsx-answer";const g=document.createElement("dt");g.textContent=o.label;const $=document.createElement("dd");he($,e.data[o.name]),f.append(g,$),C.appendChild(f)}const X=new Set(t.map(o=>o.name));for(const[o,f]of Object.entries(e.data||{})){if(X.has(o)||o==="spam"||F(f)==="")continue;const g=document.createElement("div");g.className="fsx-answer";const $=document.createElement("dt");$.textContent=`${o} (no longer on the form)`;const b=document.createElement("dd");he(b,f),g.append($,b),C.appendChild(g)}if(C.children.length)r.appendChild(C);else{const o=document.createElement("p");o.className="fsx-note",o.textContent="Nothing was filled in.",r.appendChild(o)}const A=document.createElement("div");A.className="fsx-actions";const l=(o,f,g,$)=>{const b=document.createElement("button");b.type="button",b.className=`btn btn-sm ${g}`;const Y=document.createElement("span");return Y.setAttribute("data-icon",f),Y.setAttribute("data-icon-size","13"),b.append(Y,document.createTextNode(` ${o}`)),b.addEventListener("click",$),A.appendChild(b),b};let k;l("Copy","copy","btn-ghost",()=>ae(e)),n.email&&l("Email them","mail","btn-ghost",()=>ie(e)),l(n.spam?"Not spam":"Mark as spam",n.spam?"inbox":"alert-triangle","btn-ghost",async()=>{await K(e,!n.spam)&&k?.close()});const L=document.createElement("span");L.className="fsx-spacer",A.appendChild(L),l("Delete","trash","btn-ghost text-danger",async()=>{await _(e)&&k?.close()}),l("Close","x","btn-ghost",()=>k?.close()),r.appendChild(A),k=re(`sub:${e.id}`,n.title,r)}function Me(e){const t=q(e);if(!t)return[];const n=O(t,T.peek());return[{header:n.title},{label:"Open\u2026",icon:"eye",action:()=>V(t)},{label:"Copy submission",icon:"copy",action:()=>ae(t)},{type:"divider"},{label:n.email?`Copy ${n.email}`:"Copy email",icon:"at-sign",disabled:!n.email,action:()=>$e(n.email)},{label:"Email them\u2026",icon:"mail",disabled:!n.email,action:()=>ie(t)},{type:"divider"},n.spam?{label:"Not spam - back to the inbox",icon:"inbox",action:()=>K(t,!1)}:{label:"Mark as spam",icon:"alert-triangle",action:()=>K(t,!0)},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>_(t)}]}const w=i.querySelector(".fsx-list");w.innerHTML=He;const Ae=M.applyBindings({loading:y,loadError:N,filter:v,query:oe,from:z,to:B,rows:P,counts:be,summary:ye,emptyText:Ee,intro:we},i,{methods:{showInbox:()=>{v.value="inbox"},showNew:()=>{v.value="new"},showSpam:()=>{v.value="spam"},clearDates:()=>{z.value="",B.value=""}}});x.add(()=>Ae.dispose()),x.add(me(i,{position:"bottom"}));const De=E.contextMenu(w,{cursor:"glyph",match:".fsx-row",inherit:!1,density:"compact",items:e=>Me(e.dataset.id)});x.add(()=>De?.destroy?.());const le=e=>{const t=e.target.closest(".fsx-row");if(!t)return;const n=e.target.closest(".fsx-kebab");if(n){pe(n);return}V(q(t.dataset.id))},de=e=>{const t=e.target.closest(".fsx-row");if(!(!t||e.target!==t)){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),pe(t);else if(e.key==="Enter")V(q(t.dataset.id));else if(e.key==="Delete"){e.preventDefault();const n=q(t.dataset.id);n&&_(n)}}};w.addEventListener("click",le),w.addEventListener("keydown",de),x.add(()=>{w.removeEventListener("click",le),w.removeEventListener("keydown",de)}),Q(a,{id:"form-submissions-back",icon:"arrow-left",title:"All forms",onClick:()=>{location.hash="#/forms"}}),Q(a,{id:"form-submissions-export",icon:"download",title:"Export (CSV or JSON)",onClick:Te}),Q(a,{id:"form-submissions-clear",icon:"trash",title:"Clear all submissions",onClick:ke}),x.add(()=>{[...W].forEach(e=>{try{e.close()}catch{}})}),I.scan(i),await ge(),p()&&requestAnimationFrame(()=>{w.isConnected&&I.scan(w)})}};
@@ -1,4 +1,4 @@
1
- import{api as N,getToken as ot,refreshAccessToken as it}from"../api.js";import{addBannerAction as rt}from"../lib/plugin-chrome.js";import{SHAPES as lt,whileLoading as dt}from"../lib/loading.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{onceClosed as ct,openMenuAt as Fe,settingsStore as mt,sidebarCount as le,viewLifetime as ut}from"../lib/tool-kit.js";import{openImageEditor as pt}from"../lib/image-editor.js";import{KIND_LABEL as Ue,arrangeMedia as ft,formatBytes as Pe,isEditable as de,isPreviewable as vt,kindCounts as ht,kindOf as bt,markdownFor as ce,mediaTile as gt,nameRefusal as yt,normaliseLimits as _e,normalisePrefs as wt,dropzoneRules as Et,rejectText as kt,safeName as w,shortName as Be,uploadErrorText as xt,uploadHint as $t}from"../lib/media-arrange.js";import{fmtDateTime as Mt}from"../lib/dates.js";const He=/^#\/media\/?(\?.*)?$/,me="#/media",K="media.draft",Oe=mt("media.prefs",wt),St=`
1
+ import{api as L}from"../api.js";import{catchFiles as qe,mediaUploader as Ne}from"../lib/media-upload.js";import{addBannerAction as ze}from"../lib/plugin-chrome.js";import{SHAPES as Ie,whileLoading as Re}from"../lib/loading.js";import{helpPopovers as be}from"../lib/help-popover.js";import{onceClosed as Ue,openMenuAt as ve,settingsStore as Be,sidebarCount as j,viewLifetime as Fe}from"../lib/tool-kit.js";import{openImageEditor as He}from"../lib/image-editor.js";import{KIND_LABEL as ye,arrangeMedia as Pe,formatBytes as he,isEditable as G,isPreviewable as Ke,kindCounts as Oe,kindOf as _e,markdownFor as J,mediaTile as Ve,normalisePrefs as Ye,safeName as T}from"../lib/media-arrange.js";import{fmtDateTime as je}from"../lib/dates.js";const ge=/^#\/media\/?(\?.*)?$/,Q="#/media",q="media.draft",we=Be("media.prefs",Ye),Ge=`
2
2
  <div class="mdx-tile" data-name="{{name}}" tabindex="0" title="{{name}}">
3
3
  <div class="mdx-thumb">
4
4
  <img data-bind-hidden="!preview" data-bind-src="thumb" alt="" loading="lazy" decoding="async">
@@ -7,7 +7,7 @@ import{api as N,getToken as ot,refreshAccessToken as it}from"../api.js";import{a
7
7
  <span class="mdx-chip">{{kindLabel}}</span>
8
8
  <button type="button" class="mdx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
9
9
  <div class="mdx-info"><span class="mdx-name">{{name}}</span><span class="mdx-meta">{{meta}}</span></div>
10
- </div>`,q=p=>String(p??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Dt(p){const r=document.createElement("template");return r.innerHTML=p.trim(),r.content.firstElementChild}const d=ut(He);let ue=null;async function j(p,r){try{await navigator.clipboard.writeText(p),E.toast(`${r} copied.`,{type:"success"})}catch{E.toast("Copy failed - the browser refused the clipboard.",{type:"error"})}}export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(p){d.reset();const r=p.get(0).querySelector(".mdx")||p.get(0),m=()=>He.test(location.hash)&&r.isConnected,pe=Oe.read(),v=M.observable([]),fe=M.observable(!0),V=M.observable(""),b=M.observable(pe.kind),Y=M.observable(pe.sort),ve=M.observable(""),F=M.observable(!1),he=M.observable("");let k=_e(null),G=String(Date.now());d.add(M.effect(()=>Oe.write({kind:b.value,sort:Y.value})));const U=M.observable([]);d.add(M.effect(()=>{const e=Date.now();U.value=ft(v.value,{kind:b.value,query:ve.value,sort:Y.value}).map(t=>gt(t,{now:e,stamp:G}))}));const be=M.computed(()=>ht(v.value));d.add(M.effect(()=>{b.value!=="all"&&!be.value[b.value]&&v.value.length&&(b.value="all")}));const Ke=M.computed(()=>{const e=v.value,t=U.value.length,s=Pe(e.reduce((n,o)=>n+(Number(o.size)||0),0));return t===e.length?`${e.length} file${e.length===1?"":"s"} \xB7 ${s}`:`${t} of ${e.length} \xB7 ${s} in all`}),je=M.computed(()=>v.value.length?"No files match.":"Nothing uploaded yet - drop files here, or Upload in the banner."),x=e=>v.peek().find(t=>t.name===e);async function $(){try{const e=await dt(r.querySelector(".dm-loading-slot"),N.media.list(),lt.tiles);if(!m())return;G=String(Date.now()),v.value=Array.isArray(e)?e:[],V.value=""}catch(e){V.value=`Could not load the media: ${e.message||e}`}finally{fe.value=!1}}async function W(e){if(!await E.confirm(`Delete "${q(e.name)}"? Pages that show it will show a broken image or link. This cannot be undone.`))return!1;try{return await N.media.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),le(me),m()&&await $(),!0}catch(t){return E.toast(`Not deleted: ${t.message||t}`,{type:"error"}),!1}}async function ge(e){const t=await pt(e);return t&&m()&&(le(me),await $()),t}const J=e=>window.location.origin+e.url;let g=null,ye=!1;const Q=new Set;function D(e,{focusRename:t=!1,draftName:s=null}={}){const n=x(e);if(!n){E.toast(`"${e}" is not here any more.`,{type:"warning"});return}const o=M.observable(s??n.name),H=M.observable(!1),Ce=M.computed(()=>{const a=o.value.trim();if(!a)return"A file needs a name.";const i=w(a);return i!==n.name&&x(i)?`"${i}" is already here.`:""}),Ze=M.computed(()=>{const a=o.value.trim(),i=w(a);return a&&i!==a?`Saved as "${i}" - only letters, numbers, dots, hyphens and underscores are kept.`:""}),Te=()=>o.peek().trim()!==n.name,ze={newName:o,saving:H,renameErr:Ce,renameHint:Ze,cannotRename:M.computed(()=>H.value||!!Ce.value||w(o.value.trim())===n.name),editable:de(n.name)},u=E.slideover({title:q(n.name),size:"md",position:"right"}),c=Dt(`
10
+ </div>`,N=c=>String(c??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Je(c){const s=document.createElement("template");return s.innerHTML=c.trim(),s.content.firstElementChild}const l=Fe(ge);let W=null;async function z(c,s){try{await navigator.clipboard.writeText(c),E.toast(`${s} copied.`,{type:"success"})}catch{E.toast("Copy failed - the browser refused the clipboard.",{type:"error"})}}export const mediaView={templateUrl:"/admin/js/templates/media.html",async onMount(c){l.reset();const s=c.get(0).querySelector(".mdx")||c.get(0),u=()=>ge.test(location.hash)&&s.isConnected,X=we.read(),p=M.observable([]),Z=M.observable(!0),R=M.observable(""),b=M.observable(X.kind),U=M.observable(X.sort),ee=M.observable(""),te=M.observable(!1),ae=M.observable("");let B=String(Date.now());l.add(M.effect(()=>we.write({kind:b.value,sort:U.value})));const C=M.observable([]);l.add(M.effect(()=>{const e=Date.now();C.value=Pe(p.value,{kind:b.value,query:ee.value,sort:U.value}).map(a=>Ve(a,{now:e,stamp:B}))}));const ne=M.computed(()=>Oe(p.value));l.add(M.effect(()=>{b.value!=="all"&&!ne.value[b.value]&&p.value.length&&(b.value="all")}));const ke=M.computed(()=>{const e=p.value,a=C.value.length,i=he(e.reduce((n,m)=>n+(Number(m.size)||0),0));return a===e.length?`${e.length} file${e.length===1?"":"s"} \xB7 ${i}`:`${a} of ${e.length} \xB7 ${i} in all`}),xe=M.computed(()=>p.value.length?"No files match.":"Nothing uploaded yet - drop files here, or Upload in the banner."),h=e=>p.peek().find(a=>a.name===e);async function g(){try{const e=await Re(s.querySelector(".dm-loading-slot"),L.media.list(),Ie.tiles);if(!u())return;B=String(Date.now()),p.value=Array.isArray(e)?e:[],R.value=""}catch(e){R.value=`Could not load the media: ${e.message||e}`}finally{Z.value=!1}}async function F(e){if(!await E.confirm(`Delete "${N(e.name)}"? Pages that show it will show a broken image or link. This cannot be undone.`))return!1;try{return await L.media.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),j(Q),u()&&await g(),!0}catch(a){return E.toast(`Not deleted: ${a.message||a}`,{type:"error"}),!1}}async function oe(e){const a=await He(e);return a&&u()&&(j(Q),await g()),a}const H=e=>window.location.origin+e.url;let v=null,se=!1;const P=new Set;function w(e,{focusRename:a=!1,draftName:i=null}={}){const n=h(e);if(!n){E.toast(`"${e}" is not here any more.`,{type:"warning"});return}const m=M.observable(i??n.name),D=M.observable(!1),de=M.computed(()=>{const t=m.value.trim();if(!t)return"A file needs a name.";const o=T(t);return o!==n.name&&h(o)?`"${o}" is already here.`:""}),Ae=M.computed(()=>{const t=m.value.trim(),o=T(t);return t&&o!==t?`Saved as "${o}" - only letters, numbers, dots, hyphens and underscores are kept.`:""}),ce=()=>m.peek().trim()!==n.name,me={newName:m,saving:D,renameErr:de,renameHint:Ae,cannotRename:M.computed(()=>D.value||!!de.value||T(m.value.trim())===n.name),editable:G(n.name)},d=E.slideover({title:N(n.name),size:"md",position:"right"}),r=Je(`
11
11
  <div class="mdx-panel">
12
12
  <div class="mdx-preview"></div>
13
13
  <dl class="mdx-facts"></dl>
@@ -37,4 +37,4 @@ import{api as N,getToken as ot,refreshAccessToken as it}from"../api.js";import{a
37
37
  <a class="btn btn-sm btn-ghost mdx-open" target="_blank" rel="noopener"><span data-icon="external-link" data-icon-size="13"></span> Open</a>
38
38
  <button type="button" class="btn btn-sm btn-outline" data-on-click="edit" data-bind-hidden="!editable"><span data-icon="edit" data-icon-size="13"></span> Edit image</button>
39
39
  </div>
40
- </div>`),se=c.querySelector(".mdx-preview"),A=bt(n.name);if(vt(n.name)){const a=document.createElement("img");a.src=`${n.url}?t=${encodeURIComponent(G)}`,a.alt=n.name,se.appendChild(a)}else if(A==="video"||A==="audio"){const a=document.createElement(A);a.src=n.url,a.controls=!0,a.preload="metadata",se.appendChild(a)}else{const a=document.createElement("span");a.setAttribute("data-icon",A==="document"?"file-text":"paperclip"),a.setAttribute("data-icon-size","40"),se.appendChild(a)}const et=c.querySelector(".mdx-facts"),O=(a,i)=>{const qe=document.createElement("dt");qe.textContent=a;const re=document.createElement("dd");return re.textContent=i,et.append(qe,re),re};if(O("Type",`${Ue[A]==="Images"?"Image":Ue[A].replace(/s$/,"")} (${(n.name.split(".").pop()||"").toUpperCase()})`),O("Size",Pe(n.size)),O("Uploaded",n.createdAt?Mt(n.createdAt):"-"),de(n.name)){const a=O("Dimensions","\u2026");N.media.info(n.name).then(i=>{a.textContent=i?.width?`${i.width} \xD7 ${i.height} px`:"-"}).catch(()=>{a.textContent="-"})}c.querySelector(".mdx-url").value=J(n),c.querySelector(".mdx-md").value=ce(n),c.querySelector(".mdx-open").href=n.url;let R=!1;async function Re(){if(!ze.cannotRename.peek()){H.value=!0;try{const a=await N.media.rename(n.name,o.peek().trim());E.toast(`Renamed to "${a?.name||w(o.peek().trim())}".`,{type:"success"}),R=!0,u.close(),m()&&await $()}catch(a){E.toast(`Not renamed: ${a.message||a}`,{type:"error"})}finally{H.value=!1}}}u.element.appendChild(c);const tt=Ie(c),nt=M.applyBindings(ze,c,{methods:{rename:Re,copyUrl:()=>j(J(n),"Address"),copyMd:()=>j(ce(n),"Markdown"),async del(){await W(n)&&(R=!0,u.close())},async edit(){R=!0,u.close(),await ge(n)}}}),oe=c.querySelector("#mdx-rename"),at=a=>{a.key==="Enter"&&a.target===oe&&(a.preventDefault(),Re())};c.addEventListener("keydown",at);const Ne={name:n.name,dirty:Te,newName:()=>o.peek()};g=Ne,Q.add(u),ct(u,()=>{nt.dispose(),tt(),c.querySelectorAll("video, audio").forEach(a=>{try{a.pause()}catch{}}),Q.delete(u),g===Ne&&(g=null)});const st=u.close.bind(u);let ie=!1;u.close=async a=>{if(!R&&!ye&&a!==!0&&Te()){if(ie)return;ie=!0;const i=await E.confirm("Discard the new name you typed?");if(ie=!1,!i)return}return R=!0,st()},I.scan(c),u.open(),(t||s)&&(oe.focus(),oe.select())}function Ve(e){const t=x(e);return t?[{header:t.name},{label:"Details\u2026",icon:"info",action:()=>D(t.name)},{label:"Copy address",icon:"link",action:()=>j(J(t),"Address")},{label:"Copy for a page",icon:"copy",action:()=>j(ce(t),"Markdown")},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(t.url,"_blank","noopener")},{type:"divider"},{label:"Rename\u2026",icon:"type",action:()=>D(t.name,{focusRename:!0})},...de(t.name)?[{label:"Edit image\u2026",icon:"edit",action:()=>ge(t)}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>W(t)}]:[]}const h=r.querySelector(".mdx-grid");h.innerHTML=St;const L=e=>()=>{b.value=e},Ye=M.applyBindings({files:v,loading:fe,loadError:V,kind:b,sort:Y,query:ve,tiles:U,counts:be,totalText:Ke,emptyText:je,hintText:he,dragging:F},r,{methods:{kindAll:L("all"),kindImage:L("image"),kindVideo:L("video"),kindAudio:L("audio"),kindDocument:L("document"),kindOther:L("other")}});d.add(()=>Ye.dispose()),d.add(Ie(r,{position:"bottom"})),d.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{h.isConnected&&I.scan(h)})}));const Ge=E.contextMenu(h,{cursor:"glyph",match:".mdx-tile",inherit:!1,density:"compact",items:e=>Ve(e.dataset.name)});d.add(()=>Ge?.destroy?.());const we=e=>{const t=e.target.closest(".mdx-tile");if(!t)return;const s=e.target.closest(".mdx-kebab");if(s){Fe(s);return}D(t.dataset.name)},Ee=e=>{const t=e.target.closest(".mdx-tile");if(!t||e.target!==t)return;const s=x(t.dataset.name);s&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Fe(t)):e.key==="Enter"?D(s.name):e.key==="F2"?(e.preventDefault(),D(s.name,{focusRename:!0})):e.key==="Delete"&&(e.preventDefault(),W(s)))};h.addEventListener("click",we),h.addEventListener("keydown",Ee),d.add(()=>{h.removeEventListener("click",we),h.removeEventListener("keydown",Ee)});const X=new WeakSet;let P=[],C=[],Z=0,ee=0;const ke=e=>setTimeout(e,0),We=()=>{if(!C.length)return;const e=C;C=[],E.toast(`Not added - ${e.slice(0,4).join("; ")}${e.length>4?` and ${e.length-4} more`:""}`,{type:"error",duration:9e3})},te=e=>{C.length||ke(We),C.push(e)};async function Je(){const e=P;if(P=[],!e.length||!m())return;const t=[...new Set(e.map(o=>w(o.name)))],s=t.slice(0,5).map(o=>`"${q(o)}"`).join(", ")+(t.length>5?` and ${t.length-5} more`:""),n=t.length===1;await E.confirm(`${n?"A file":`${t.length} files`} with the same name ${n?"is":"are"} already here: ${s}. Replace ${n?"it":"them"}?`)&&(T||(e.forEach(o=>X.add(o)),l.addFiles(e)))}const Qe=r.querySelector(".mdx-file-input"),Xe=r.querySelector(".mdx-zone");let T=!1,_=0,ne=null;const l=E.dropzone(Xe,{input:Qe,multiple:!0,previews:!0,upload:{url:"/api/media",field:"file",concurrency:3,headers:()=>{const e=ot();return e?{Authorization:`Bearer ${e}`}:{}}},labels:{cancel:"Cancel the upload of {name}",remove:"Remove {name} from the list"},onBeforeAdd(e){const t=yt(e.name,k);if(t)return te(`${Be(e.name)}: ${t}`),!1;if(X.has(e.file))return X.delete(e.file),!0;const s=w(e.name),n=l.getEntries().filter(o=>w(o.name)===s);return n.some(o=>o.status==="uploading"||o.status==="queued")?(te(`${Be(e.name)}: a file of that name is uploading already`),!1):(n.forEach(o=>{o.status!=="done"&&l.removeFile(o.id)}),x(s)?(P.length||ke(Je),P.push(e.file),!1):!0)},onReject(e,t){te(kt(e.name,t,k))},onSuccess(){Z++},onError(e,t){if(t.status===401&&!e._mdxRefreshed&&S.get("auth_refresh_token")){e._mdxRefreshed=!0,t.message="signing in again\u2026",_++,it().then(()=>{T||l.retry(e.id)}).catch(()=>{}).finally(()=>{_--,ne?.()});return}ee++,t.message=xt(t,k)},onComplete(){const e=Z,t=ee;if(Z=0,ee=0,e&&E.toast(`Uploaded ${e} file${e===1?"":"s"}.`,{type:"success"}),t){const s=`${t} file${t===1?"":"s"} not uploaded`;E.toast(m()?`${s} - Retry or remove ${t===1?"it":"them"} in the list.`:`${s}.`,{type:"error",duration:9e3})}l.getEntries().filter(s=>s.status==="done").forEach(s=>l.removeFile(s.id)),e&&(le(me),m()?$():ue?.())}}),ae=()=>l.getEntries().some(e=>e.status==="uploading"||e.status==="queued");ue=()=>{m()&&$()};const xe=()=>{T||(T=!0,l.destroy())};d.add(()=>{ue=null,ne=()=>{!ae()&&!_&&xe()},ae()||_?l.on("complete",ne):xe()}),N.media.limits().then(e=>{T||!m()||(k=_e(e),l.setOptions(Et(k)),he.value=$t(k))}).catch(()=>{}),rt(p,{id:"media-upload",icon:"upload",title:"Upload files",onClick:()=>l.browse()});const B=e=>[...e.dataTransfer?.types||[]].includes("Files");let z=0;const $e=e=>{B(e)&&(e.preventDefault(),z++,F.value=!0)},Me=e=>{B(e)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")},Se=e=>{B(e)&&(z=Math.max(0,z-1),z||(F.value=!1))},De=e=>{B(e)&&(z=0,F.value=!1,!e.defaultPrevented&&(e.preventDefault(),l.addFiles(e.dataTransfer.files)))},Le=e=>{const t=[...e.clipboardData?.files||[]];e.defaultPrevented||!t.length||e.target?.closest?.('input, textarea, [contenteditable=""], [contenteditable="true"]')&&[...e.clipboardData.types||[]].includes("text/plain")||(e.preventDefault(),l.addFiles(t))},f=p.get(0);f.addEventListener("dragenter",$e),f.addEventListener("dragover",Me),f.addEventListener("dragleave",Se),f.addEventListener("drop",De),f.addEventListener("paste",Le),d.add(()=>{f.removeEventListener("dragenter",$e),f.removeEventListener("dragover",Me),f.removeEventListener("dragleave",Se),f.removeEventListener("drop",De),f.removeEventListener("paste",Le)});const Ae=e=>{(ae()||g?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Ae),d.add(()=>window.removeEventListener("beforeunload",Ae)),d.add(()=>{if(g?.dirty())try{S.set(K,{name:g.name,newName:g.newName(),at:new Date().toISOString()})}catch{}ye=!0,[...Q].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(r),await $(),!m())return;let y=null;try{y=S.get(K)}catch{}if(y?.name){try{S.remove?S.remove(K):S.set(K,null)}catch{}x(y.name)&&await E.confirm(`You were renaming "${q(y.name)}" to "${q(y.newName)}" and left before saving. Pick up where you left off?`)&&m()&&D(y.name,{draftName:y.newName})}}};
40
+ </div>`),O=r.querySelector(".mdx-preview"),x=_e(n.name);if(Ke(n.name)){const t=document.createElement("img");t.src=`${n.url}?t=${encodeURIComponent(B)}`,t.alt=n.name,O.appendChild(t)}else if(x==="video"||x==="audio"){const t=document.createElement(x);t.src=n.url,t.controls=!0,t.preload="metadata",O.appendChild(t)}else{const t=document.createElement("span");t.setAttribute("data-icon",x==="document"?"file-text":"paperclip"),t.setAttribute("data-icon-size","40"),O.appendChild(t)}const Ce=r.querySelector(".mdx-facts"),$=(t,o)=>{const fe=document.createElement("dt");fe.textContent=t;const Y=document.createElement("dd");return Y.textContent=o,Ce.append(fe,Y),Y};if($("Type",`${ye[x]==="Images"?"Image":ye[x].replace(/s$/,"")} (${(n.name.split(".").pop()||"").toUpperCase()})`),$("Size",he(n.size)),$("Uploaded",n.createdAt?je(n.createdAt):"-"),G(n.name)){const t=$("Dimensions","\u2026");L.media.info(n.name).then(o=>{t.textContent=o?.width?`${o.width} \xD7 ${o.height} px`:"-"}).catch(()=>{t.textContent="-"})}r.querySelector(".mdx-url").value=H(n),r.querySelector(".mdx-md").value=J(n),r.querySelector(".mdx-open").href=n.url;let A=!1;async function ue(){if(!me.cannotRename.peek()){D.value=!0;try{const t=await L.media.rename(n.name,m.peek().trim());E.toast(`Renamed to "${t?.name||T(m.peek().trim())}".`,{type:"success"}),A=!0,d.close(),u()&&await g()}catch(t){E.toast(`Not renamed: ${t.message||t}`,{type:"error"})}finally{D.value=!1}}}d.element.appendChild(r);const De=be(r),$e=M.applyBindings(me,r,{methods:{rename:ue,copyUrl:()=>z(H(n),"Address"),copyMd:()=>z(J(n),"Markdown"),async del(){await F(n)&&(A=!0,d.close())},async edit(){A=!0,d.close(),await oe(n)}}}),_=r.querySelector("#mdx-rename"),Le=t=>{t.key==="Enter"&&t.target===_&&(t.preventDefault(),ue())};r.addEventListener("keydown",Le);const pe={name:n.name,dirty:ce,newName:()=>m.peek()};v=pe,P.add(d),Ue(d,()=>{$e.dispose(),De(),r.querySelectorAll("video, audio").forEach(t=>{try{t.pause()}catch{}}),P.delete(d),v===pe&&(v=null)});const Te=d.close.bind(d);let V=!1;d.close=async t=>{if(!A&&!se&&t!==!0&&ce()){if(V)return;V=!0;const o=await E.confirm("Discard the new name you typed?");if(V=!1,!o)return}return A=!0,Te()},I.scan(r),d.open(),(a||i)&&(_.focus(),_.select())}function Ee(e){const a=h(e);return a?[{header:a.name},{label:"Details\u2026",icon:"info",action:()=>w(a.name)},{label:"Copy address",icon:"link",action:()=>z(H(a),"Address")},{label:"Copy for a page",icon:"copy",action:()=>z(J(a),"Markdown")},{label:"Open in a new tab",icon:"external-link",action:()=>window.open(a.url,"_blank","noopener")},{type:"divider"},{label:"Rename\u2026",icon:"type",action:()=>w(a.name,{focusRename:!0})},...G(a.name)?[{label:"Edit image\u2026",icon:"edit",action:()=>oe(a)}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>F(a)}]:[]}const f=s.querySelector(".mdx-grid");f.innerHTML=Ge;const k=e=>()=>{b.value=e},Me=M.applyBindings({files:p,loading:Z,loadError:R,kind:b,sort:U,query:ee,tiles:C,counts:ne,totalText:ke,emptyText:xe,hintText:ae,dragging:te},s,{methods:{kindAll:k("all"),kindImage:k("image"),kindVideo:k("video"),kindAudio:k("audio"),kindDocument:k("document"),kindOther:k("other")}});l.add(()=>Me.dispose()),l.add(be(s,{position:"bottom"})),l.add(M.effect(()=>{C.value,requestAnimationFrame(()=>{f.isConnected&&I.scan(f)})}));const Se=E.contextMenu(f,{cursor:"glyph",match:".mdx-tile",inherit:!1,density:"compact",items:e=>Ee(e.dataset.name)});l.add(()=>Se?.destroy?.());const ie=e=>{const a=e.target.closest(".mdx-tile");if(!a)return;const i=e.target.closest(".mdx-kebab");if(i){ve(i);return}w(a.dataset.name)},le=e=>{const a=e.target.closest(".mdx-tile");if(!a||e.target!==a)return;const i=h(a.dataset.name);i&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ve(a)):e.key==="Enter"?w(i.name):e.key==="F2"?(e.preventDefault(),w(i.name,{focusRename:!0})):e.key==="Delete"&&(e.preventDefault(),F(i)))};f.addEventListener("click",ie),f.addEventListener("keydown",le),l.add(()=>{f.removeEventListener("click",ie),f.removeEventListener("keydown",le)});const K=Ne(s.querySelector(".mdx-zone"),{input:s.querySelector(".mdx-file-input"),exists:e=>!!h(e),showing:u,onHint:e=>{ae.value=e},onUploaded(){j(Q),u()?g():W?.()}});W=()=>{u()&&g()},l.add(()=>{W=null,K.release()}),ze(c,{id:"media-upload",icon:"upload",title:"Upload files",onClick:()=>K.browse()}),l.add(qe(c.get(0),K.addFiles,{onDragging:e=>{te.value=e}}));const re=e=>{v?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",re),l.add(()=>window.removeEventListener("beforeunload",re)),l.add(()=>{if(v?.dirty())try{S.set(q,{name:v.name,newName:v.newName(),at:new Date().toISOString()})}catch{}se=!0,[...P].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(s),await g(),!u())return;let y=null;try{y=S.get(q)}catch{}if(y?.name){try{S.remove?S.remove(q):S.set(q,null)}catch{}h(y.name)&&await E.confirm(`You were renaming "${N(y.name)}" to "${N(y.newName)}" and left before saving. Pick up where you left off?`)&&u()&&w(y.name,{draftName:y.newName})}}};
@@ -461,7 +461,7 @@ Any other properties stored on a field, such as `validation`, `logic`, `referenc
461
461
 
462
462
  **Types the server and displays understand, but that can only be set in `schema.json`, recipes or plugins:**
463
463
  - `reference`. A relational field (see below). The server validates it and the display, form and API code resolve it.
464
- - `file`. An uploaded-file reference `{url, name, size, mime}` with a `file: {accept, maxSize}` config. Displays show an image thumbnail for image MIME types and a download link for anything else.
464
+ - `file`. An uploaded-file reference `{url, name, size, mime}` (a list of them with `multiple: true`) with a `file: {accept, maxSize, maxFiles}` config. The collection editor offers it as "Files (uploaded with a form)"; the entry editor shows the files and keeps them. Displays show an image thumbnail for image MIME types and a download link for anything else.
465
465
  - `boolean`, `datetime`, `multiselect` / `multi-select`, `tags`, `chooser`, `json`, `object`, `array`, `markdown`, `text`. These are used by presets, plugins and recipes. The Collection Browser builds a filter control from `text`/`textarea`/`email`, `number`, `date`/`datetime`, `select`/`chooser`, `multiselect`/`tags`/`tag` and `boolean`/`checkbox`.
466
466
 
467
467
  Examples of where they appear: `projects` uses `boolean`, `notifications` uses `datetime` and `array`, `api-endpoints` uses `object`, the calendar plugin uses `markdown`, the onboarding recipe uses `multiselect` and `file`.
@@ -494,8 +494,9 @@ Examples of where they appear: `projects` uses `boolean`, `notifications` uses `
494
494
 
495
495
  - **File uploads**. A form `file` field switches the submission to `multipart/form-data`.
496
496
  - The server checks each file against the field's `file.accept` (MIME list, wildcards such as `image/*` allowed) and `file.maxSize` (default 5 MB).
497
- - The filename is cleaned and given a uuid prefix, and the file is saved to `content/media/`.
498
- - The entry stores `{url, name, size, mime}`.
497
+ - `multiple: true` takes several files, up to `file.maxFiles` (default 5, at most 20; the form editor's Several files / Most files). One bad file or one too many refuses the submission.
498
+ - The filename is cleaned and given a uuid prefix, and the file is saved to `content/media/` - only after every other check has passed.
499
+ - The entry stores `{url, name, size, mime}`, or a list of them for a `multiple` field.
499
500
  - Files are publicly reachable under `/media/`.
500
501
  Where: `parseMultipartForm()` in `server/routes/api/forms.js`; `docs/configuration.md`. Since: 0.20.0.
501
502
 
@@ -2827,7 +2828,7 @@ entries are genuine code issues.
2827
2828
  - The collection editor offers only 13 types. It cannot create `reference`, `file`, `boolean`, `datetime`, `multiselect` and others, which need hand-edited JSON even though the display, filter and API code supports them.
2828
2829
  - `docs/configuration.md` lists `text`, `boolean`, `multiselect`, `chooser` and `tags` as schema types but not the editor's own `string`/`tel`/`radio`/`checkbox-group`/`time`/`url`/`hidden`.
2829
2830
  - Collection-to-form mapping turns every unknown type into a plain text input.
2830
- - The form editor has no UI for a file field's `accept`/`maxSize`.
2831
+ - The form editor has no UI for a file field's `accept`/`maxSize` (it has Several files / Most files).
2831
2832
  - **Collection editor offers role names that do not exist by default.** It hard-codes `subscriber`, `editor` and `manager` for API and export access, while the base roles are `super-admin`, `admin` and `user`. (fixed in 0.92.1)
2832
2833
  - For the API, an unknown role resolves to level Infinity, so any signed-in user passes.
2833
2834
  - For export, an unknown role fails closed.
@@ -1313,7 +1313,12 @@ Order of checks:
1313
1313
  `redirect`) run after the store; their failures are logged and notified, never returned.
1314
1314
 
1315
1315
  File parts are checked against the field's `file.maxSize` (default 5 MB) and `file.accept` (MIME list,
1316
- `image/*` wildcards), saved to the media library and stored as `{url, name, size, mime}`. There is no CAPTCHA.
1316
+ `image/*` wildcards), saved to the media library and stored as `{url, name, size, mime}` - a list of them for
1317
+ a `multiple: true` field, which takes up to `file.maxFiles` (default 5, at most 20; one more is a `400`). All the files of
1318
+ one submission together may hold at most server.json `uploads.maxSubmissionBytes` (default 50 MB, or `uploads.maxFileSize`
1319
+ if larger); past it the
1320
+ submission is a `400` and nothing is written. A
1321
+ file field's value only comes from file parts. There is no CAPTCHA.
1317
1322
  `{{entryId}}` in `settings.successRedirect` (or a trigger redirect, which wins) is replaced with the new id.
1318
1323
 
1319
1324
  ---
@@ -29,6 +29,7 @@ Controls the HTTP server.
29
29
  | `csp.connectHosts` | string[] | `[]` | Other sites a public page may read from in the browser (`fetch`), added to the Content-Security-Policy's `connect-src` as `https://<host>`. The site itself and cdn.jsdelivr.net are always allowed. Bare hostnames only (`dcm.dommajs.org`); a scheme, path, port or wildcard is ignored. Read at boot, so restart after changing it |
30
30
  | `csp.imgHosts` | string[] | `[]` | Other sites a public page may show images from (an `<img>` hosted elsewhere, such as a badge or partner logo), added to the Content-Security-Policy's `img-src` as `https://<host>`. The site itself, `data:` and `blob:` are always allowed; without this an image from another site is blocked - upload it to Media instead if you can. Bare hostnames only (`statics.startupbase.io`); a scheme, path, port or wildcard is ignored. Read at boot, so restart after changing it |
31
31
  | `uploads.maxFileSize` | number | `10485760` (10 MB) | Maximum allowed upload size in bytes |
32
+ | `uploads.maxSubmissionBytes` | number | 50 MB, or `uploads.maxFileSize` when that is larger | The most a public form submission's files may add up to, all file fields together. The server holds a submission's files in memory until every check has passed, so it stops reading and refuses the submission (`400`, nothing written) the moment the running total passes this. Read fresh on every submission. A form with a file field is sent to the page with `fileLimits.maxTotalBytes`, so the page refuses a file that would pass it as it is added, and checks again before posting. A page with a `[form]` that is already in the response cache keeps the cap it was drawn with until the site restarts or the form is saved |
32
33
  | `logging.level` | string | Log verbosity: `fatal`, `error`, `warn`, `info`, `debug`, `trace`, `silent`. Defaults to `info` in development, `warn` otherwise. `warn` hides 4xx responses and plugin `log.info` output, so a failed admin action leaves no trace - raise it to `info` if you need to diagnose one. Overridden by the `LOG_LEVEL` env var. |
33
34
  | `restartOnPluginToggle` | boolean | `true` | Restart the server automatically when an admin enables or disables a plugin. See **Plugin toggles restart the server** below |
34
35
  | `requireSignedPlugins` | boolean | `false` | When `true`, the site runs only plugin code the Marketplace signed: an unsigned `.dcmsplugin` (e.g. one exported from another site) is refused even if the installer confirms it, New plugin is refused, a plugin's files cannot be changed in the code editor (it opens read only), licensed source cannot be read even by a Super Admin, and MongoDB connection options that start a program or read a file (`autoEncryption`, `tls…File`, …) cannot be saved. A bundle with a signature that does not verify is always refused, whatever this is set to. Read fresh on every check, and also as it was at boot, so changing the file on a running site can switch it on but not off. `DOMMA_SIGNED_ONLY=1` in the environment turns it on whatever the file says - Domma CMS Manager sets it on the sites it hosts. Nothing in the admin writes `server.json`. See `server/services/codePolicy.js` |
@@ -1680,8 +1681,9 @@ options when a reference field is in `filterable="…"`.
1680
1681
 
1681
1682
  ### `type: "file"` - uploaded file field
1682
1683
 
1683
- Stores an uploaded file's metadata: `{ url, name, size, mime }`. The actual
1684
- bytes live in `content/media/` (under a uuid-prefixed filename so concurrent
1684
+ Stores an uploaded file's metadata: `{ url, name, size, mime }` - or, for a field
1685
+ with `multiple: true`, a list of them (always a list, even of one file). An empty
1686
+ field stores `""`. The actual bytes live in `content/media/` (under a uuid-prefixed filename so concurrent
1685
1687
  uploads of the same name don't collide).
1686
1688
 
1687
1689
  ```json
@@ -1700,29 +1702,59 @@ uploads of the same name don't collide).
1700
1702
  | Sub-field | Required | Description |
1701
1703
  |-----------|----------|--------------------------------------------------------------------------------------|
1702
1704
  | `accept` | no | Comma-separated mime types or wildcards (`image/*`, `application/pdf,image/jpeg`) |
1703
- | `maxSize` | no | Maximum bytes (default 5 MB) |
1705
+ | `maxSize` | no | Maximum bytes per file (default 5 MB) |
1706
+ | `maxFiles`| no | With `multiple: true`: how many files the field takes (default 5, never more than 20) |
1707
+
1708
+ `multiple: true` (on the field, beside `file`) lets the visitor add several files;
1709
+ the form editor's **Several files** and **Most files** set it and `maxFiles`.
1704
1710
 
1705
1711
  **Form rendering** - a drop area (`E.dropzone`) over the field's file input: drop,
1706
1712
  browse (click, Enter or Space) or paste, with image previews. `accept`, `maxSize`
1707
- and the count (one file unless `multiple: true`) are checked as files are added,
1708
- and a refused file is named under the area and announced. The rules are
1709
- `public/js/form-files.mjs`, which the server imports for its own check. Setting
1710
- `multiple: true` lets several files be added (the server keeps the last one per
1711
- field).
1713
+ and the count (one file, or up to `maxFiles` with `multiple: true`) are checked as
1714
+ files are added, and a refused file is named under the area and announced. The
1715
+ rules are `public/js/form-files.mjs`, which the server imports for its own check,
1716
+ so both say the same words. In a multi-step form, going back to a step keeps every
1717
+ file chosen there.
1712
1718
 
1713
1719
  **Submission** - the form switches automatically to `multipart/form-data`
1714
1720
  when any file input has a selected file. The server-side handler:
1715
1721
 
1716
- 1. Validates each file against its field's `accept` and `maxSize`
1717
- 2. Sanitises the filename (lowercases, strips path chars, prepends a uuid)
1718
- 3. Saves to `content/media/<uuid>-<safe-name>`
1719
- 4. Replaces the entry's field value with the reference object
1722
+ 1. Validates each file against its field's `accept` and `maxSize` (and refuses web
1723
+ pages, SVG, XML and script whatever `accept` says). One refused file, or more
1724
+ files than `maxFiles`, refuses the whole submission with a `400`; a second file
1725
+ sent for a single field is ignored. All of a submission's files together may
1726
+ hold at most `uploads.maxSubmissionBytes` (server.json; by default 50 MB, or
1727
+ `uploads.maxFileSize` when that is larger) - past that the submission is
1728
+ refused as soon as the total is reached
1729
+ 2. Holds the files in memory until the honeypot, timing, trigger, validation and
1730
+ rate-limit checks pass - nothing a refused or bot submission sent is written.
1731
+ If a file cannot be written, or the entry is then refused (a collection rule),
1732
+ the files already written for it are removed again
1733
+ 3. Sanitises each filename (lowercases, strips path chars, prepends a uuid) and
1734
+ saves it to `content/media/<uuid>-<safe-name>`
1735
+ 4. Stores the reference object (a list of them for a `multiple` field) as the
1736
+ field's value. A file field's value only ever comes from uploaded files: a
1737
+ JSON body claiming one holds a reference is stored as empty
1738
+
1739
+ **Where the value goes** - the submissions screen and the collection's entries
1740
+ screen list the files as links (the entry editor shows them and keeps them as they
1741
+ are); the CSV export writes `name (url)`, several joined with `; `; the JSON
1742
+ export, the webhook body (`{form, data}`), the `form:submitted` hook's `data` and
1743
+ Actions' `{{entry.data.<field>}}` carry the stored value itself (so a `multiple`
1744
+ field is an array - `{{entry.data.photos.0.url}}` is the first file's address);
1745
+ the notification email links each file (absolute: `site.json` `baseUrl`, else an
1746
+ address an admin has signed in at). CSV cells that start with `=`, `+`, `-`, `@`,
1747
+ TAB or CR are prefixed with `'` so a spreadsheet does not run them as formulas.
1748
+ A collection's `file` field is never `required` there (a form made collection
1749
+ leaves it off): the form enforces it, and the entry editor, which cannot upload,
1750
+ keeps the stored files as they are.
1720
1751
 
1721
1752
  **Display** - `[collection]`, the Collection Browser, and the public read API
1722
1753
  all render file references appropriately:
1723
1754
 
1724
1755
  - Image mimes (`image/png`, `image/jpeg`, etc.) → clickable `<img>` thumbnail
1725
1756
  - Everything else → `<a href download>filename</a>`
1757
+ - A `multiple` field's list → one of the above per file (a table cell lists the names)
1726
1758
 
1727
1759
  Stored reference shape:
1728
1760
 
@@ -1735,6 +1767,17 @@ Stored reference shape:
1735
1767
  }
1736
1768
  ```
1737
1769
 
1770
+ and for `"multiple": true`:
1771
+
1772
+ ```json
1773
+ [
1774
+ {"url": "/media/1f2e3d4c-front.jpg", "name": "front.jpg", "size": 81234, "mime": "image/jpeg"},
1775
+ {"url": "/media/9a8b7c6d-back.jpg", "name": "back.jpg", "size": 79012, "mime": "image/jpeg"}
1776
+ ]
1777
+ ```
1778
+
1779
+ `fileRefs(value)` in `public/js/form-files.mjs` reads either shape as a list.
1780
+
1738
1781
  **Security note** - the `/media/` URL is publicly served (the file is in
1739
1782
  `content/media/` which the CMS exposes statically). If your use case needs
1740
1783
  gated downloads (e.g. CVs only visible to the candidate + recruiters),
@@ -60,7 +60,7 @@ Everything the single-page form does is applied to each page as the visitor reac
60
60
  | **Date fields** | Render as native date pickers on every page |
61
61
  | **Icon / image fields**| Keep their Browse buttons and previews on every page |
62
62
  | **Spacers** | Rendered in place on the page they belong to |
63
- | **File uploads** | A file chosen on page one is still attached when page three submits |
63
+ | **File uploads** | Files chosen on page one (all of them, for a multiple field) are still attached when page three submits |
64
64
  | **Conditional logic** | A rule on page two can test an answer given on page one |
65
65
  | **Spam protection** | The honeypot and timing check cover the submission, not just page one |
66
66
 
@@ -409,6 +409,9 @@ options.hooks.on('content:pageCreated', ({ page, urlPath }) => {
409
409
  |------------------|--------------------------------------|------------------------------------------------------------------------------------------------------------------|
410
410
  | `form:submitted` | `{ slug, entryId, data, formTitle }` | A public form submission passes validation and is stored. `entryId` is `null` if collection storage is disabled. |
411
411
 
412
+ In `data`, a file field is `{url, name, size, mime}`, a list of them when the field is `multiple`, or `''`;
413
+ `fileRefs(value)` from `public/js/form-files.mjs` reads any of the three as a list.
414
+
412
415
  #### Auth events
413
416
 
414
417
  | Event | Payload | Fired when |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.10.16",
3
+ "version": "1.10.18",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -1 +1 @@
1
- export const DEFAULT_MAX_SIZE=5242880;export function fileRules(t){const e=t&&t.file||{};return{accept:String(e.accept||"").split(",").map(i=>i.trim()).filter(Boolean),maxSize:Number(e.maxSize)||5242880,multiple:!!(t&&t.multiple)}}export function mimeAccepted(t,e){if(!t||!t.length)return!0;const i=String(e||"");return t.some(o=>o.endsWith("/*")?i.startsWith(o.slice(0,-1)):o===i)}export function cleanUploadName(t){const e=i=>i.replace(/^[-.]+|[-.]+$/g,"");return e(e(String(t||"upload").toLowerCase().replace(/[^\w.-]+/g,"-").replace(/-+/g,"-")).slice(0,200))||"upload"}const u=/^(text\/html|text\/xml|text\/xsl|text\/javascript|text\/ecmascript|application\/(x-)?javascript|application\/ecmascript|application\/xml|application\/[\w.-]+\+xml|image\/svg\+xml)\b/i,d=/\.(html?|xht(ml)?|shtml?|svgz?|xml|xsd|xslt?|xul|rdf|owl|atom|rss|wsdl|smil?|kml|opf|xlf|gpx|ttml|mathml|mml|xop|m?js|mht(ml)?)$/i;function p(t){const e=/\.[^.]+$/.exec(cleanUploadName(t&&(t.filename??t.name)||""));return e?e[0]:""}export function isActiveFile(t){const e=String(t&&(t.mimetype??t.type)||""),i=t&&(t.filename??t.name)||"";return u.test(e)||d.test(cleanUploadName(i))}export function fileRefusal(t,e){const i=fileRules(t),o=t&&(t.label||t.name)||"File";if(Number(e&&e.size)>i.maxSize)return`"${o}" file is too large (max ${Math.round(i.maxSize/1024)} KB).`;const n=e&&(e.mimetype??e.type)||"";return isActiveFile(e)?`"${o}" file type "${n||p(e)}" not allowed (it could run as a page on this site).`:i.accept.length&&!mimeAccepted(i.accept,n)?`"${o}" file type "${n}" not allowed (expected: ${String(t.file.accept).trim()}).`:null}const m=[[/wordprocessingml|^msword$/,"Word"],[/spreadsheetml|ms-excel/,"Excel"],[/presentationml|ms-powerpoint/,"PowerPoint"],[/^plain$/,"text"]];export function describeAccept(t){const e=[];return(t||[]).forEach(i=>{const[o,n]=String(i).split("/");let r;n==="*"?r={image:"images",audio:"audio",video:"video",text:"text files"}[o]||`${o} files`:n?r=m.find(([s])=>s.test(n))?.[1]||n.replace(/^(x-|vnd\.)/,"").split(/[.+]/)[0].toUpperCase():r=String(i),r&&!e.includes(r)&&e.push(r)}),e.length<2?e[0]||"":`${e.slice(0,-1).join(", ")} or ${e[e.length-1]}`}export function formatSize(t){const e=Number(t)||0;if(e<1024)return`${e} B`;const i=["KB","MB","GB"];let o=e,n=-1;do o/=1024,n++;while(o>=1024&&n<i.length-1);return`${o>=10?Math.round(o):Math.round(o*10)/10} ${i[n]}`}export function fileHint(t){const e=fileRules(t),i=describeAccept(e.accept),o=`up to ${formatSize(e.maxSize)}`;return i?`${i[0].toUpperCase()}${i.slice(1)} - ${o}`:`Any file - ${o}`}export function dropzoneOptions(t){const e=fileRules(t);return{accept:e.accept.length?e.accept.join(","):null,maxSize:e.maxSize,multiple:e.multiple,labels:{prompt:e.multiple?"Drop files here, paste, or click to browse":"Drop a file here, paste, or click to browse",hint:fileHint(t),required:"Please add a file",type:refusalMessage(t,{name:"{name}"},"type"),size:refusalMessage(t,{name:"{name}"},"size"),count:refusalMessage(t,{name:"{name}"},"count")}}}export function fileBlueprint(t,e){return{...e,type:"dropzone",formConfig:{...e.formConfig||{},...dropzoneOptions(t)}}}export function refusalMessage(t,e,i){const o=fileRules(t),n=e&&e.name||"That file";if(i==="size")return`${n} was not added: it is larger than ${formatSize(o.maxSize)}.`;if(i==="type"){const r=describeAccept(o.accept);return`${n} was not added: ${r?`only ${r} can be uploaded here`:"that type of file is not accepted"}.`}return i==="active"?`${n} was not added: web pages, SVG images and scripts cannot be uploaded.`:`${n} was not added: only one file can be added here.`}function h(t){return Array.isArray(t)&&t.length>0&&typeof File<"u"&&t.every(e=>e instanceof File)}export function splitFiles(t,e,i){const o={...t||{}},n=new Map,r=new Set((e||[]).filter(s=>s&&s.type==="file"&&s.name).map(s=>s.name));for(const[s,l]of Object.entries(o))h(l)?(n.set(s,l),delete o[s]):r.has(s)&&(Array.isArray(l)||l==null)&&(o[s]="");for(const[s,l]of i||[])n.set(s,l||[]);for(const[s,l]of n)l.length?delete o[s]:(n.delete(s),(r.has(s)||s in o)&&(o[s]=""));return{values:o,files:n}}export function submitBody(t,e){if(![...(e||new Map).values()].some(n=>n&&n.length))return{body:JSON.stringify(t),headers:{"Content-Type":"application/json"},multipart:!1};const o=new FormData;for(const[n,r]of Object.entries(t))r==null||e.has(n)||o.append(n,typeof r=="object"?JSON.stringify(r):String(r));for(const[n,r]of e)for(const s of r||[])o.append(n,s,s.name);return{body:o,headers:{},multipart:!0}}export function captureFileFields(t,e){!t||!e||t.querySelectorAll('input[type="file"]').forEach(i=>{i.name&&e.set(i.name,Array.from(i.files||[]))})}export function restoreFileFields(t,e){!t||!e||!e.size||t.querySelectorAll('input[type="file"]').forEach(i=>{const o=e.get(i.name);if(!o||!o.length||i.files&&i.files.length)return;const n=f(i);if(n){n.addFiles(o);return}try{const r=new DataTransfer;o.forEach(s=>r.items.add(s)),i.files=r.files}catch{}})}function f(t){const e=globalThis.E;if(!e||typeof e.get!="function")return null;const i=t.closest("[data-dropzone-field]"),o=i&&e.get(i,"dropzone")||e.get(t,"dropzone");return o&&typeof o.addFiles=="function"?o:null}function c(t,e){const i=String(e&&e.name||"").replace(/\[\]$/,"");return(t||[]).find(o=>o&&o.name===i)||{name:i}}function x(t){let e=t.nextElementSibling;return(!e||!e.classList.contains("fb-file-refusal"))&&(e=document.createElement("p"),e.className="fb-file-refusal",e.hidden=!0,t.after(e)),e}export function wireFileFields(t,e){if(!t||t._fbFilesWired)return;t._fbFilesWired=!0;const i=new WeakMap,o=(n,r,s)=>{const l=x(n.zone),a=i.get(n.zone)||[];a.push(refusalMessage(c(e,n.input),r,s)),i.set(n.zone,a),l.textContent=a.join(" "),l.hidden=!1,n.zone.classList.add("is-invalid"),queueMicrotask(()=>i.delete(n.zone))};t.addEventListener("dm:dropzone:reject",n=>{const r=n.detail&&n.detail.instance,[s,l]=n.detail&&n.detail.args||[];r&&r.zone&&o(r,s,l)}),t.addEventListener("dm:dropzone:beforeadd",n=>{const r=n.detail&&n.detail.instance,[s]=n.detail&&n.detail.args||[];!r||!r.zone||!s||!isActiveFile(s.file||s)||(n.preventDefault(),o(r,s,"active"))}),t.addEventListener("dm:dropzone:add",n=>{const r=n.detail&&n.detail.instance;if(!r||!r.zone||i.get(r.zone))return;const s=r.zone.nextElementSibling;s&&s.classList.contains("fb-file-refusal")&&(s.hidden=!0,s.textContent="")}),t.addEventListener("dm:dropzone:change",n=>{const r=n.detail&&n.detail.instance;if(!r||!r.input)return;r.input.dispatchEvent(new Event("input",{bubbles:!0}));const s=r.input.closest(".fb-field-invalid");s&&r.getFiles().length&&s.classList.remove("fb-field-invalid")})}export function enhanceFileInputs(t,e){const i=globalThis.E;!t||!i||typeof i.dropzone!="function"||(t.querySelectorAll('input[type="file"]').forEach(o=>{f(o)||i.dropzone(o,dropzoneOptions(c(e,o)))}),wireFileFields(t,e))}export function checkFileInputs(t,e){const i=[];return t&&t.querySelectorAll('input[type="file"]').forEach(o=>{const n=c(e,o),r=Array.from(o.files||[]);!fileRules(n).multiple&&r.length>1&&i.push(`"${n.label||n.name}" takes one file.`),r.forEach(s=>{const l=fileRefusal(n,s);l&&i.push(l)})}),i}export function destroyDropzones(t){const e=globalThis.E;!t||!e||typeof e.get!="function"||t.querySelectorAll('.dm-dropzone, input[type="file"]').forEach(i=>{const o=e.get(i,"dropzone");o&&typeof o.destroy=="function"&&o.destroy()})}
1
+ export const DEFAULT_MAX_SIZE=5242880,DEFAULT_MAX_FILES=5,MAX_FILES_LIMIT=20;export function fileRules(e){const t=e&&e.file||{},n=!!(e&&e.multiple),o=Math.floor(Number(t.maxFiles));return{accept:String(t.accept||"").split(",").map(r=>r.trim()).filter(Boolean),maxSize:Number(t.maxSize)||5242880,multiple:n,maxFiles:n?Math.min(o>=1?o:5,20):1}}function d(e){return e===1?"one file":`${e} files`}export function countRefusal(e,t){const{maxFiles:n,multiple:o}=fileRules(e);if(Number(t)<=n)return null;const r=e&&(e.label||e.name)||"File";return o?`"${r}" takes at most ${d(n)}.`:`"${r}" takes one file.`}export function fileRefs(e){return(Array.isArray(e)?e:[e]).filter(n=>n&&typeof n=="object"&&!Array.isArray(n)&&typeof n.url=="string"&&(typeof n.name=="string"||n.name===void 0))}export function isFileValue(e){const t=fileRefs(e);return t.length>0&&t.length===(Array.isArray(e)?e.length:1)}export function fileValueText(e,{base:t="",urls:n=!0}={}){return fileRefs(e).map(o=>{const r=o.name||o.url.split("/").pop()||"file";return!n||!o.url?r:`${r} (${fileUrl(o,t)})`}).join("; ")}export function fileUrl(e,t=""){const n=String(e&&e.url||""),o=String(t||"").replace(/\/+$/,"");return o&&n.startsWith("/")?`${o}${n}`:n}export function mimeAccepted(e,t){if(!e||!e.length)return!0;const n=String(t||"");return e.some(o=>o.endsWith("/*")?n.startsWith(o.slice(0,-1)):o===n)}export function cleanUploadName(e){const t=n=>n.replace(/^[-.]+|[-.]+$/g,"");return t(t(String(e||"upload").toLowerCase().replace(/[^\w.-]+/g,"-").replace(/-+/g,"-")).slice(0,200))||"upload"}const y=/^(text\/html|text\/xml|text\/xsl|text\/javascript|text\/ecmascript|application\/(x-)?javascript|application\/ecmascript|application\/xml|application\/[\w.-]+\+xml|image\/svg\+xml)\b/i,b=/\.(html?|xht(ml)?|shtml?|svgz?|xml|xsd|xslt?|xul|rdf|owl|atom|rss|wsdl|smil?|kml|opf|xlf|gpx|ttml|mathml|mml|xop|m?js|mht(ml)?)$/i;function z(e){const t=/\.[^.]+$/.exec(cleanUploadName(e&&(e.filename??e.name)||""));return t?t[0]:""}export function isActiveFile(e){const t=String(e&&(e.mimetype??e.type)||""),n=e&&(e.filename??e.name)||"";return y.test(t)||b.test(cleanUploadName(n))}export function fileRefusal(e,t){const n=fileRules(e),o=e&&(e.label||e.name)||"File";if(Number(t&&t.size)>n.maxSize)return`"${o}" file is too large (max ${Math.round(n.maxSize/1024)} KB).`;const r=t&&(t.mimetype??t.type)||"";return isActiveFile(t)?`"${o}" file type "${r||z(t)}" not allowed (it could run as a page on this site).`:n.accept.length&&!mimeAccepted(n.accept,r)?`"${o}" file type "${r}" not allowed (expected: ${String(e.file.accept).trim()}).`:null}const $=[[/wordprocessingml|^msword$/,"Word"],[/spreadsheetml|ms-excel/,"Excel"],[/presentationml|ms-powerpoint/,"PowerPoint"],[/^plain$/,"text"]];export function describeAccept(e){const t=[];return(e||[]).forEach(n=>{const[o,r]=String(n).split("/");let s;r==="*"?s={image:"images",audio:"audio",video:"video",text:"text files"}[o]||`${o} files`:r?s=$.find(([i])=>i.test(r))?.[1]||r.replace(/^(x-|vnd\.)/,"").split(/[.+]/)[0].toUpperCase():s=String(n),s&&!t.includes(s)&&t.push(s)}),t.length<2?t[0]||"":`${t.slice(0,-1).join(", ")} or ${t[t.length-1]}`}export function formatSize(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const n=["KB","MB","GB"];let o=t,r=-1;do o/=1024,r++;while(o>=1024&&r<n.length-1);return`${o>=10?Math.round(o):Math.round(o*10)/10} ${n[r]}`}export function fileHint(e){const t=fileRules(e),n=describeAccept(t.accept),o=t.multiple?`up to ${d(t.maxFiles)}, ${formatSize(t.maxSize)} each`:`up to ${formatSize(t.maxSize)}`;return n?`${n[0].toUpperCase()}${n.slice(1)} - ${o}`:`Any file - ${o}`}export function dropzoneOptions(e){const t=fileRules(e);return{accept:t.accept.length?t.accept.join(","):null,maxSize:t.maxSize,multiple:t.multiple,maxFiles:t.multiple?t.maxFiles:null,labels:{prompt:t.multiple?"Drop files here, paste, or click to browse":"Drop a file here, paste, or click to browse",hint:fileHint(e),required:"Please add a file",type:refusalMessage(e,{name:"{name}"},"type"),size:refusalMessage(e,{name:"{name}"},"size"),count:refusalMessage(e,{name:"{name}"},"count")}}}export function fileBlueprint(e,t){return{...t,type:"dropzone",formConfig:{...t.formConfig||{},...dropzoneOptions(e)}}}export function refusalMessage(e,t,n,o){const r=fileRules(e),s=t&&t.name||"That file";if(n==="total")return`${s} was not added: the files together would be more than ${formatSize(o)}.`;if(n==="size")return`${s} was not added: it is larger than ${formatSize(r.maxSize)}.`;if(n==="type"){const i=describeAccept(r.accept);return`${s} was not added: ${i?`only ${i} can be uploaded here`:"that type of file is not accepted"}.`}return n==="active"?`${s} was not added: web pages, SVG images and scripts cannot be uploaded.`:`${s} was not added: only ${d(r.maxFiles)} can be added here.`}function S(e){return Array.isArray(e)&&e.length>0&&typeof File<"u"&&e.every(t=>t instanceof File)}export function splitFiles(e,t,n){const o={...e||{}},r=new Map,s=new Set((t||[]).filter(i=>i&&i.type==="file"&&i.name).map(i=>i.name));for(const[i,l]of Object.entries(o))S(l)?(r.set(i,l),delete o[i]):s.has(i)&&(Array.isArray(l)||l==null)&&(o[i]="");for(const[i,l]of n||[])r.set(i,l||[]);for(const[i,l]of r)l.length?delete o[i]:(r.delete(i),(s.has(i)||i in o)&&(o[i]=""));return{values:o,files:r}}export function submitBody(e,t){if(![...(t||new Map).values()].some(r=>r&&r.length))return{body:JSON.stringify(e),headers:{"Content-Type":"application/json"},multipart:!1};const o=new FormData;for(const[r,s]of Object.entries(e))s==null||t.has(r)||o.append(r,typeof s=="object"?JSON.stringify(s):String(s));for(const[r,s]of t)for(const i of s||[])o.append(r,i,i.name);return{body:o,headers:{},multipart:!0}}export function captureFileFields(e,t){!e||!t||e.querySelectorAll('input[type="file"]').forEach(n=>{n.name&&t.set(n.name,Array.from(n.files||[]))})}export function restoreFileFields(e,t){!e||!t||!t.size||e.querySelectorAll('input[type="file"]').forEach(n=>{const o=t.get(n.name);if(!o||!o.length||n.files&&n.files.length)return;const r=g(n);if(r){r.addFiles(o);return}try{const s=new DataTransfer;o.forEach(i=>s.items.add(i)),n.files=s.files}catch{}})}function g(e){const t=globalThis.E;if(!t||typeof t.get!="function")return null;const n=e.closest("[data-dropzone-field]"),o=n&&t.get(n,"dropzone")||t.get(e,"dropzone");return o&&typeof o.addFiles=="function"?o:null}function m(e,t){const n=String(t&&t.name||"").replace(/\[\]$/,"");return(e||[]).find(o=>o&&o.name===n)||{name:n}}function E(e){let t=e.nextElementSibling;return(!t||!t.classList.contains("fb-file-refusal"))&&(t=document.createElement("p"),t.className="fb-file-refusal",t.hidden=!0,e.after(t)),t}export function submissionTooLarge(e){const t=e/1048576;return`The files sent are too large together (at most ${t>=1?`${Math.round(t*10)/10} MB`:`${Math.round(e/1024)} KB`} in one submission). Send fewer or smaller files.`}export function setFileLimits(e,t,n){e&&(e._fbFileLimits=t||null,n&&(e._fbRetained=n))}function p(e){return Number(e&&e._fbFileLimits&&e._fbFileLimits.maxTotalBytes)||0}function f(e){let t=0;for(const n of e||[])t+=Number(n&&n.size)||0;return t}export function totalTooLarge(e,t){const n=p(e);if(!n||!t)return null;let o=0;for(const r of t.values())o+=f(r);return o>n?submissionTooLarge(n):null}function F(e,t){const n=globalThis.E,o=a=>n&&typeof n.get=="function"?n.get(a,"dropzone"):null,r=new Set;e.querySelectorAll('.dm-dropzone, [data-dropzone-field], input[type="file"]').forEach(a=>{const c=o(a);c&&r.add(c)});const s=new Set,i=new Set;let l=0;for(const a of r)a.input&&(s.add(a.input),a.input.name&&i.add(a.input.name)),a.zone&&a.zone.querySelectorAll('input[type="file"]').forEach(c=>s.add(c)),a!==t&&(l+=f(a.getFiles()));e.querySelectorAll('input[type="file"]').forEach(a=>{s.has(a)||(a.name&&i.add(a.name),l+=f(a.files))});for(const[a,c]of e._fbRetained||[])i.has(a)||(l+=f(c));return l}export function wireFileFields(e,t){if(!e||e._fbFilesWired)return;e._fbFilesWired=!0;const n=new WeakMap,o=(s,i,l)=>{const a=E(s.zone),c=n.get(s.zone)||[];c.push(refusalMessage(m(t,s.input),i,l,p(e))),n.set(s.zone,c),a.textContent=c.join(" "),a.hidden=!1,s.zone.classList.add("is-invalid"),queueMicrotask(()=>n.delete(s.zone))};e.addEventListener("dm:dropzone:reject",s=>{const i=s.detail&&s.detail.instance,[l,a]=s.detail&&s.detail.args||[];i&&i.zone&&o(i,l,a)});const r=new Map;e.addEventListener("dm:dropzone:beforeadd",s=>{const i=s.detail&&s.detail.instance,[l]=s.detail&&s.detail.args||[];if(!i||!i.zone||!l)return;const a=l.file||l;if(isActiveFile(a)){s.preventDefault(),o(i,l,"active");return}const c=p(e);if(!c)return;const u=i.options&&i.options.multiple===!1,h=Number(l.size??a.size)||0,x=u?0:r.get(i)||0;if(F(e,u?i:null)+x+h>c){s.preventDefault(),o(i,l,"total");return}u||(r.has(i)||queueMicrotask(()=>r.delete(i)),r.set(i,x+h))}),e.addEventListener("dm:dropzone:change",s=>{const i=s.detail&&s.detail.instance;i&&r.delete(i)}),e.addEventListener("dm:dropzone:add",s=>{const i=s.detail&&s.detail.instance;if(!i||!i.zone||n.get(i.zone))return;const l=i.zone.nextElementSibling;l&&l.classList.contains("fb-file-refusal")&&(l.hidden=!0,l.textContent="")}),e.addEventListener("dm:dropzone:change",s=>{const i=s.detail&&s.detail.instance;if(!i||!i.input)return;i.input.dispatchEvent(new Event("input",{bubbles:!0}));const l=i.input.closest(".fb-field-invalid");l&&i.getFiles().length&&l.classList.remove("fb-field-invalid")})}export function enhanceFileInputs(e,t){const n=globalThis.E;!e||!n||typeof n.dropzone!="function"||(e.querySelectorAll('input[type="file"]').forEach(o=>{g(o)||n.dropzone(o,dropzoneOptions(m(t,o)))}),wireFileFields(e,t))}export function checkFileInputs(e,t){const n=[];return e&&e.querySelectorAll('input[type="file"]').forEach(o=>{const r=m(t,o),s=Array.from(o.files||[]),i=countRefusal(r,s.length);i&&n.push(i),s.forEach(l=>{const a=fileRefusal(r,l);a&&n.push(a)})}),n}export function destroyDropzones(e){const t=globalThis.E;!e||!t||typeof t.get!="function"||e.querySelectorAll('.dm-dropzone, input[type="file"]').forEach(n=>{const o=t.get(n,"dropzone");o&&typeof o.destroy=="function"&&o.destroy()})}
@@ -1,2 +1,2 @@
1
- import{installSmoothCarets as H}from"/public/js/smooth-caret.js";import{buildWizardSteps as K}from"/public/js/form-steps.mjs";import{applyFormClasses as F,applySubmitButtons as _,applyWizardButtons as J,resolveSecondaryButton as Y,submitWrapperClasses as $}from"/public/js/form-submit.mjs";import{captureFileFields as L,checkFileInputs as G,destroyDropzones as Q,enhanceFileInputs as X,fileBlueprint as Z,restoreFileFields as ee,splitFiles as te,submitBody as ne,wireFileFields as j}from"/public/js/form-files.mjs";H(document);const B=document.querySelectorAll("[data-form]");B.length&&B.forEach(me);function T(t){return(t||[]).some(o=>o.logic||Array.isArray(o.triggers)&&o.triggers.length)}function oe(t,o){const n=t.querySelector('[name="'+o+'"], [name="'+o+'[]"]')||t.querySelector('[data-chooser-field="'+o+'"]');if(!n)return null;const e=n.closest("form")||t;let i=n;for(;i.parentNode&&i.parentNode!==e;)i=i.parentNode;return i}export function markInvalidFields(t,o){t.querySelectorAll(".fb-field-invalid").forEach(e=>e.classList.remove("fb-field-invalid"));let n=null;(o||[]).forEach(function(e){const i=oe(t,e.name);i&&(i.classList.add("fb-field-invalid"),n||(n=i))}),n&&n.scrollIntoView&&n.scrollIntoView({behavior:"smooth",block:"center"})}export function showValidationPopup(t){const o=(t.missing||[]).map(e=>"<strong>"+R(e.label)+"</strong> is required.").concat((t.errors||[]).map(R)),n=o.join("<br>");if(typeof E<"u"&&E.alert){E.alert(n,{title:"Please complete this step"});return}window.alert(o.join(`
2
- `).replace(/<[^>]+>/g,""))}function R(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function q(t,o){let n=!1;t.addEventListener("dm-form-end",function(e){const i=e.detail&&e.detail.validation;if(i&&!i.valid){markInvalidFields(t,i.missing),showValidationPopup(i);return}n||(n=!0,t.classList.add("fb-form-loading"),Promise.resolve(o(e.detail&&e.detail.values||{})).catch(function(){}).finally(function(){n=!1,t.classList.remove("fb-form-loading")}))})}function v(t,o,n){const e=t.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const i=document.createElement("div");i.className=n==="success"?"fb-form-success":"fb-form-error",i.textContent=o,t.appendChild(i)}function A(t,o){if(t._formLogicRuntime=o,t._formLogicObserver&&(t._formLogicObserver.disconnect(),t._formLogicObserver=null),!t.parentNode||typeof MutationObserver>"u"||t._formLogicObserved)return;t._formLogicObserved=!0;const n=new MutationObserver(function(e){for(const i of e)for(const r of i.removedNodes)if(r===t||r.nodeType===1&&r.contains&&r.contains(t)){t._formLogicRuntime&&t._formLogicRuntime.destroy(),n.disconnect();return}});n.observe(t.parentNode,{childList:!0,subtree:!1}),t._formLogicObserver=n}function re(t){const o=t&&t.logic;return!!(o&&(o.visibility||o.requirement))}function z(t,o){const n={};return t.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const i=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),r=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":i?"chooser":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const s={type:r,label:e.label||e.name,required:re(e)?!1:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{helperText:e.helper},...e.tooltip&&{tooltip:e.tooltip},...d}};e.minLength!==void 0&&(s.minLength=e.minLength),e.maxLength!==void 0&&(s.maxLength=e.maxLength),e.min!==void 0&&(s.min=e.min),e.max!==void 0&&(s.max=e.max),i&&(e.variant&&(s.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(s.multiple=!0),e.density&&(s.density=e.density),e.columns&&(s.columns=e.columns),e.accent&&(s.accent=e.accent),e.accentStyle&&(s.accentStyle=e.accentStyle),e.glow&&(s.glow=!0),e.glowColour&&(s.glowColour=e.glowColour),e.shadow&&(s.shadow=e.shadow),e.shadowColour&&(s.shadowColour=e.shadowColour)),n[e.name]=e.type==="file"?Z(e,s):s}),n}function ie(t){const o={};return t.forEach(function(n){if(!(!n.name||n.type==="page-break"||n.type==="spacer")&&(n.type==="select"||n.type==="multiselect")&&n.required){const e=(n.options||[])[0];e&&(o[n.name]=typeof e=="object"?e.value:e)}}),o}function P(t,o){(o||[]).forEach(function(n){if(n.type!=="date"||!n.name)return;const e=t.querySelector('[name="'+n.name+'"]');e&&e.type!=="date"&&(e.type="date")})}const O="padding:.45rem .7rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:.85em;flex-shrink:0;line-height:1.2;";function D(t){const o=document.createElement("div");o.style.cssText="position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:20px;";const n=document.createElement("div");n.style.cssText="background:var(--dm-surface-overlay,var(--dm-surface));color:var(--dm-text);border:1px solid var(--dm-border);border-radius:10px;width:100%;max-width:560px;max-height:75vh;display:flex;flex-direction:column;gap:.6rem;padding:1rem;box-shadow:var(--dm-shadow-lg,0 12px 40px rgba(0,0,0,.35));";const e=document.createElement("div");e.style.cssText="display:flex;align-items:center;justify-content:space-between;gap:.5rem;";const i=document.createElement("strong");i.textContent=t;const r=document.createElement("button");r.type="button",r.textContent="\xD7",r.setAttribute("aria-label","Close"),r.style.cssText=O+"font-size:1rem;padding:.15rem .55rem;",e.appendChild(i),e.appendChild(r),n.appendChild(e);const d=document.createElement("div");d.style.cssText="display:flex;flex-direction:column;gap:.6rem;overflow:hidden;min-height:0;",n.appendChild(d),o.appendChild(n);const s=l=>{l.key==="Escape"&&a()};function a(){document.removeEventListener("keydown",s),o.remove()}return r.addEventListener("click",a),o.addEventListener("click",l=>{l.target===o&&a()}),document.addEventListener("keydown",s),document.body.appendChild(o),{body:d,close:a}}function ae(t){const{body:o,close:n}=D("Choose an icon"),e=document.createElement("input");e.type="text",e.placeholder="Search icons\u2026",e.className="form-input",e.style.cssText="padding:.45rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);background:var(--dm-input-bg,transparent);color:var(--dm-text);",o.appendChild(e);const i=document.createElement("div");i.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(46px,1fr));gap:4px;overflow-y:auto;min-height:0;",o.appendChild(i);const r=window.I&&typeof I.list=="function"?I.list():[],d=r.map(s=>{const a=document.createElement("button");a.type="button",a.title=s,a.dataset.iconName=s,a.style.cssText="display:flex;align-items:center;justify-content:center;padding:.55rem 0;border:1px solid transparent;border-radius:6px;background:transparent;color:inherit;cursor:pointer;";const l=document.createElement("span");return l.setAttribute("data-icon",s),l.setAttribute("data-icon-size","18"),a.appendChild(l),a.addEventListener("mouseenter",()=>{a.style.borderColor="var(--dm-border)"}),a.addEventListener("mouseleave",()=>{a.style.borderColor="transparent"}),a.addEventListener("click",()=>{n(),t(s)}),i.appendChild(a),a});if(!r.length){const s=document.createElement("p");s.textContent="Icon engine unavailable - type an icon name into the field instead.",i.appendChild(s)}window.I?.scan?.(i),e.addEventListener("input",()=>{const s=e.value.trim().toLowerCase();d.forEach(a=>{a.style.display=!s||a.dataset.iconName.includes(s)?"":"none"})}),e.focus()}async function se(){const t=typeof S<"u"&&S.get?S.get("auth_token"):null;if(!t)return{error:"auth"};const o=i=>fetch("/api/media",{headers:{Authorization:"Bearer "+i}});let n;try{if(n=await o(t),n.status===401&&S.get("auth_refresh_token")){const i=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:S.get("auth_refresh_token")})});if(i.ok){const{token:r}=await i.json();S.set("auth_token",r),n=await o(r)}}}catch{return{error:"load"}}if(!n.ok)return{error:n.status===401||n.status===403?"auth":"load"};const e=await n.json().catch(()=>[]);return{files:(Array.isArray(e)?e:[]).filter(i=>/\.(png|jpe?g|gif|webp|svg|avif)$/i.test(i.name||i.url||""))}}function ce(t){const{body:o,close:n}=D("Choose an image"),e=document.createElement("p");e.textContent="Loading media library\u2026",e.style.cssText="margin:0;font-size:.85em;color:var(--dm-text-muted);",o.appendChild(e),se().then(({files:i,error:r})=>{if(r){e.textContent=r==="auth"?"Sign in with media access to browse the library - or paste an image URL into the field.":"Could not load the media library - paste an image URL into the field instead.";return}if(!i.length){e.textContent="No images in the media library yet.";return}e.remove();const d=document.createElement("div");d.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;overflow-y:auto;min-height:0;",i.forEach(s=>{const a=document.createElement("button");a.type="button",a.title=s.name||s.url,a.style.cssText="display:flex;flex-direction:column;gap:.25rem;padding:.35rem;border:1px solid var(--dm-border);border-radius:8px;background:transparent;color:inherit;cursor:pointer;min-width:0;";const l=document.createElement("img");l.src=s.url,l.alt=s.name||"",l.loading="lazy",l.style.cssText="width:100%;height:72px;object-fit:cover;border-radius:5px;";const f=document.createElement("span");f.textContent=s.name||s.url,f.style.cssText="font-size:.68em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",a.appendChild(l),a.appendChild(f),a.addEventListener("click",()=>{n(),t(s.url)}),d.appendChild(a)}),o.appendChild(d)})}function M(t,o){t.value=o,t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0}))}function V(t){const o=document.createElement("div");return o.style.cssText="display:flex;align-items:center;gap:.5rem;",t.parentNode.insertBefore(o,t),o.appendChild(t),t.style.flex="1",t.style.minWidth="0",o}function W(t){const o=document.createElement("button");return o.type="button",o.textContent=t,o.style.cssText=O,o}function le(t){if(!window.I||typeof I.list!="function")return;const o=V(t),n=document.createElement("span");n.style.cssText="display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;flex-shrink:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);";const e=()=>{n.textContent="";const r=t.value.trim();if(!r)return;const d=document.createElement("span");d.setAttribute("data-icon",r),d.setAttribute("data-icon-size","16"),n.appendChild(d),window.I?.scan?.(n)};t.addEventListener("input",e),e();const i=W("Browse");i.addEventListener("click",()=>ae(r=>{M(t,r),e()})),o.appendChild(n),o.appendChild(i)}function ue(t){const o=V(t),n=document.createElement("img");n.alt="",n.style.cssText="width:2rem;height:2rem;flex-shrink:0;object-fit:cover;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);display:none;";const e=()=>{const r=t.value.trim();n.style.display=r?"":"none",r&&(n.src=r)};t.addEventListener("input",e),e();const i=W("Browse");i.addEventListener("click",()=>ce(r=>{M(t,r),e()})),o.appendChild(n),o.appendChild(i)}export function enhancePickerFields(t,o){const n=typeof t=="string"?document.querySelector(t):t;n&&(o||[]).forEach(function(e){if(!e.name||e.type!=="icon"&&e.type!=="image")return;const i=n.querySelector('[name="'+e.name+'"]');!i||i.dataset.pickerEnhanced||(i.dataset.pickerEnhanced="1",e.type==="icon"?le(i):ue(i))})}function N(t,o){const n=document.createElement("div");n.className="fb-form-honeypot",n.setAttribute("aria-hidden","true");const e=document.createElement("input");e.name=t.querySelector('[name="website"]')?"_website":"website",n.dataset.hpName=e.name,e.type="text",e.tabIndex=-1,e.autocomplete="url",e.placeholder="https://",n.appendChild(e);const i=document.createElement("input");i.name="_t",i.type="hidden",i.value=o||Date.now(),n.appendChild(i),t.appendChild(n)}function U(t,o){const n=t.querySelector("form");if(!n)return;const e=Array.from(n.querySelectorAll(".form-group"));let i=0;o.forEach(function(r){if(r.type==="spacer"){const d=document.createElement("div");d.className="fb-spacer";const s=e[i];if(s)n.insertBefore(d,s);else{const a=n.querySelector('[type="submit"]');a?n.insertBefore(d,a):n.appendChild(d)}}else r.type!=="page-break"&&i++})}function x(t,o,n,e,i,r,d){const s=i||e,a=s.querySelector(".fb-form-honeypot"),l=a?.querySelector('input[type="text"]')?.value||"",f=a?.querySelector('[name="_t"]')?.value||"",u=Object.assign({},o);delete u._t,a?.dataset.hpName&&delete u[a.dataset.hpName];const c=new Map(r||[]);L(s,c);const h=te(u,d,c),{body:p,headers:w}=ne(Object.assign(h.values,{_hp:l,_t:f}),h.files);return fetch("/api/forms/submit/"+t,{method:"POST",headers:w,body:p}).then(g=>g.json().then(b=>({ok:g.ok,body:b}))).then(g=>{g.ok&&g.body.ok?(Q(e),e.textContent="",v(e,g.body.message||n.successMessage||"Thank you.","success")):v(e,g.body.error||"Something went wrong.","error")}).catch(()=>{v(e,"Unable to submit. Please check your connection.","error")})}function de(t,o,n,e,i){const r=document.createElement("form");r.noValidate=!0,o.forEach(function(a){const l=document.createElement("div");l.className="form-group",l.style.marginBottom="1.25rem",a.name&&(l.dataset.field=a.name);const f=document.createElement("label");if(f.className="form-label",f.textContent=a.label||a.name,a.required){const c=document.createElement("span");c.textContent=" *",c.style.color="var(--dm-danger)",f.appendChild(c)}let u;a.type==="textarea"?(u=document.createElement("textarea"),u.rows=a.formConfig?.rows||4,u.className="form-input"):a.type==="select"?(u=document.createElement("select"),u.className="form-input",(a.options||[]).forEach(function(c){const h=document.createElement("option");h.value=typeof c=="string"?c:c.value??"",h.textContent=typeof c=="string"?c:c.label||h.value,u.appendChild(h)})):(u=document.createElement("input"),u.type=a.type==="icon"||a.type==="image"?"text":a.type||"text",u.className="form-input",a.placeholder&&(u.placeholder=a.placeholder),a.type==="file"&&(a.file?.accept&&(u.accept=a.file.accept),a.multiple&&(u.multiple=!0))),u.name=a.name,u.required=a.required||!1,l.appendChild(f),l.appendChild(u),r.appendChild(l)}),n.honeypot&&N(r);const d=document.createElement("button");if(d.type="submit",d.className="btn btn-primary",d.textContent=n.submitText||"Submit",$(n,"stacked").length||Y(n)){const a=document.createElement("div");a.className="form-group form-buttons",a.appendChild(d),r.appendChild(a)}else r.appendChild(d);if(r.addEventListener("submit",function(a){a.preventDefault();const l={};if(o.forEach(function(u){const c=r.querySelector('[name="'+u.name+'"]');c&&(l[u.name]=c.value)}),window.FormLogicEngine&&i){const u=window.FormLogicEngine,c=[],h=[];if(o.forEach(function(p){if(u.evaluateFieldVisibility(p,l)==="hidden"){delete l[p.name];return}const g=u.evaluateFieldRequirement(p,l),b=l[p.name];g&&(!b||!String(b).trim())&&c.push(p.label||p.name);const C=u.validateField(p,b||"",l);C.length&&h.push(C[0].message)}),c.length||h.length){const p=[];c.length&&p.push("Required: "+c.join(", ")),h.length&&p.push(h.join("; ")),v(t,p.join(". "),"error");return}}const f=G(r,o);if(f.length){v(t,f.join(" "),"error");return}if(t.dataset.triggerBlocked==="true"){const u=t._formLogicRuntime,c=u&&u.submitBlock?u.submitBlock().reasons:[];v(t,c.join(" ")||"This form cannot be submitted yet.","error");return}t.classList.add("fb-form-loading"),d.disabled=!0,x(e,l,n,t,r,null,o).finally(function(){t.classList.remove("fb-form-loading"),d.disabled=!1})}),t.appendChild(r),_(r,n,"stacked"),F(r,n,o),enhancePickerFields(t,o),X(t,o),q(t,function(a){return x(e,a,n,t,r,null,o)}),window.FormLogicEngine&&i&&T(o)){const a=new window.FormLogicEngine.FormLogicRuntime(i,t);a.init(),t._formLogicRuntime=a}}function me(t){const o=t.getAttribute("data-form");o&&fetch("/api/forms/"+o+"/public").then(n=>{if(!n.ok)throw new Error("Form not found: "+o);return n.json()}).then(n=>{const e=n.fields||[],i=n.settings||{},r=document.createElement("div");r.className="fb-form-wrapper",t.appendChild(r);const d=e.some(s=>s.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const s=Number(i.columns)===3?3:2,a=i.layout||"grid";if(d&&Domma.forms.wizard){const l=K(e,n.title),f=l.map(m=>m.fields),u=l.map(function(m){return{title:m.title,description:m.description,fields:z(m.fields,s)}});let c=0,h=null,p=null;const w=Date.now(),g=new Map;j(r,e);const b=function(m){const y=window.FormLogicEngine;if(!y||!y.validateFields)return;const k=y.validateFields(r,f[c]||[]);k.valid||(m.preventDefault(),m.stopImmediatePropagation(),markInvalidFields(r,k.missing),showValidationPopup(k))},C=function(){const m=f[c]||e;if(J(r,i),F(r,i,m),P(r,m),enhancePickerFields(r,m),ee(r,g),m.some(y=>y.type==="spacer")&&U(r,m),i.honeypot){const y=r.querySelector("form");y&&N(y,w)}window.FormLogicEngine&&T(e)&&(h&&h.destroy(),h=new window.FormLogicEngine.FormLogicRuntime(n,r,{baseValues:p&&p.getStepData?p.getStepData():{}}),h.init(),A(r,h)),r.querySelectorAll(".wizard-next, .wizard-submit").forEach(y=>y.addEventListener("click",b,!0))};p=Domma.forms.wizard(r,{schema:{steps:u},layout:a,columns:s,finishText:i.submitText||"Finish",onStepChange:function(m){L(r,g),c=m,setTimeout(C,0)},onSubmit:function(m){return L(r,g),x(o,m,i,r,null,g,e)}}),q(r,function(m){return L(r,g),x(o,m,i,r,null,g,e)}),r.addEventListener("dm-form-trigger",function(m){if(!m.detail||m.detail.type!=="goto-step"||!p||!p.goToStep)return;const y=Number(m.detail.action.step);!isFinite(y)||y<0||y>=u.length||y!==c&&p.goToStep(y)}),Promise.resolve(p).then(C)}else if(Domma.forms.render){const l=z(e,s),f=ie(e);j(r,e),q(r,function(c){return x(o,c,i,r,null,null,e)});const u=Domma.forms.render(r,l,f,{submitText:i.submitText||"Submit",layout:a,columns:s,onSubmit:function(c){return x(o,c,i,r,null,null,e)}});Promise.resolve(u).then(function(){if(_(r,i,a),F(r,i,e),P(r,e),enhancePickerFields(r,e),window.FormLogicEngine&&T(e)){const c=new window.FormLogicEngine.FormLogicRuntime(n,r);c.init(),A(r,c)}if(e.some(c=>c.type==="spacer")&&U(r,e),i.honeypot){const c=r.querySelector("form");c&&N(c)}})}}else de(r,e.filter(s=>s.type!=="page-break"&&s.type!=="spacer"),i,o,n)}).catch(n=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:var(--dm-danger);font-style:italic;",t.appendChild(e),console.warn("[forms]",n.message)})}
1
+ import{installSmoothCarets as K}from"/public/js/smooth-caret.js";import{buildWizardSteps as J}from"/public/js/form-steps.mjs";import{applyFormClasses as F,applySubmitButtons as _,applyWizardButtons as Y,resolveSecondaryButton as $,submitWrapperClasses as G}from"/public/js/form-submit.mjs";import{captureFileFields as L,checkFileInputs as Q,destroyDropzones as X,enhanceFileInputs as Z,fileBlueprint as ee,restoreFileFields as te,setFileLimits as j,splitFiles as ne,submitBody as oe,totalTooLarge as re,wireFileFields as B}from"/public/js/form-files.mjs";K(document);const R=document.querySelectorAll("[data-form]");R.length&&R.forEach(fe);function T(n){return(n||[]).some(o=>o.logic||Array.isArray(o.triggers)&&o.triggers.length)}function ie(n,o){const t=n.querySelector('[name="'+o+'"], [name="'+o+'[]"]')||n.querySelector('[data-chooser-field="'+o+'"]');if(!t)return null;const e=t.closest("form")||n;let i=t;for(;i.parentNode&&i.parentNode!==e;)i=i.parentNode;return i}export function markInvalidFields(n,o){n.querySelectorAll(".fb-field-invalid").forEach(e=>e.classList.remove("fb-field-invalid"));let t=null;(o||[]).forEach(function(e){const i=ie(n,e.name);i&&(i.classList.add("fb-field-invalid"),t||(t=i))}),t&&t.scrollIntoView&&t.scrollIntoView({behavior:"smooth",block:"center"})}export function showValidationPopup(n){const o=(n.missing||[]).map(e=>"<strong>"+A(e.label)+"</strong> is required.").concat((n.errors||[]).map(A)),t=o.join("<br>");if(typeof E<"u"&&E.alert){E.alert(t,{title:"Please complete this step"});return}window.alert(o.join(`
2
+ `).replace(/<[^>]+>/g,""))}function A(n){return String(n).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function q(n,o){let t=!1;n.addEventListener("dm-form-end",function(e){const i=e.detail&&e.detail.validation;if(i&&!i.valid){markInvalidFields(n,i.missing),showValidationPopup(i);return}t||(t=!0,n.classList.add("fb-form-loading"),Promise.resolve(o(e.detail&&e.detail.values||{})).catch(function(){}).finally(function(){t=!1,n.classList.remove("fb-form-loading")}))})}function x(n,o,t){const e=n.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const i=document.createElement("div");i.className=t==="success"?"fb-form-success":"fb-form-error",i.textContent=o,n.appendChild(i)}function P(n,o){if(n._formLogicRuntime=o,n._formLogicObserver&&(n._formLogicObserver.disconnect(),n._formLogicObserver=null),!n.parentNode||typeof MutationObserver>"u"||n._formLogicObserved)return;n._formLogicObserved=!0;const t=new MutationObserver(function(e){for(const i of e)for(const r of i.removedNodes)if(r===n||r.nodeType===1&&r.contains&&r.contains(n)){n._formLogicRuntime&&n._formLogicRuntime.destroy(),t.disconnect();return}});t.observe(n.parentNode,{childList:!0,subtree:!1}),n._formLogicObserver=t}function ae(n){const o=n&&n.logic;return!!(o&&(o.visibility||o.requirement))}function z(n,o){const t={};return n.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const i=e.type==="chooser"||!!(e.variant&&["select","radio","checkbox-group"].includes(e.type)),r=e.type==="checkbox"?"boolean":e.type==="date"||e.type==="icon"||e.type==="image"?"string":i?"chooser":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const s={type:r,label:e.label||e.name,required:ae(e)?!1:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{helperText:e.helper},...e.tooltip&&{tooltip:e.tooltip},...d}};e.minLength!==void 0&&(s.minLength=e.minLength),e.maxLength!==void 0&&(s.maxLength=e.maxLength),e.min!==void 0&&(s.min=e.min),e.max!==void 0&&(s.max=e.max),i&&(e.variant&&(s.variant=e.variant),(e.multiple||e.type==="checkbox-group")&&(s.multiple=!0),e.density&&(s.density=e.density),e.columns&&(s.columns=e.columns),e.accent&&(s.accent=e.accent),e.accentStyle&&(s.accentStyle=e.accentStyle),e.glow&&(s.glow=!0),e.glowColour&&(s.glowColour=e.glowColour),e.shadow&&(s.shadow=e.shadow),e.shadowColour&&(s.shadowColour=e.shadowColour)),t[e.name]=e.type==="file"?ee(e,s):s}),t}function se(n){const o={};return n.forEach(function(t){if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(o[t.name]=typeof e=="object"?e.value:e)}}),o}function O(n,o){(o||[]).forEach(function(t){if(t.type!=="date"||!t.name)return;const e=n.querySelector('[name="'+t.name+'"]');e&&e.type!=="date"&&(e.type="date")})}const D="padding:.45rem .7rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:.85em;flex-shrink:0;line-height:1.2;";function M(n){const o=document.createElement("div");o.style.cssText="position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;padding:20px;";const t=document.createElement("div");t.style.cssText="background:var(--dm-surface-overlay,var(--dm-surface));color:var(--dm-text);border:1px solid var(--dm-border);border-radius:10px;width:100%;max-width:560px;max-height:75vh;display:flex;flex-direction:column;gap:.6rem;padding:1rem;box-shadow:var(--dm-shadow-lg,0 12px 40px rgba(0,0,0,.35));";const e=document.createElement("div");e.style.cssText="display:flex;align-items:center;justify-content:space-between;gap:.5rem;";const i=document.createElement("strong");i.textContent=n;const r=document.createElement("button");r.type="button",r.textContent="\xD7",r.setAttribute("aria-label","Close"),r.style.cssText=D+"font-size:1rem;padding:.15rem .55rem;",e.appendChild(i),e.appendChild(r),t.appendChild(e);const d=document.createElement("div");d.style.cssText="display:flex;flex-direction:column;gap:.6rem;overflow:hidden;min-height:0;",t.appendChild(d),o.appendChild(t);const s=l=>{l.key==="Escape"&&a()};function a(){document.removeEventListener("keydown",s),o.remove()}return r.addEventListener("click",a),o.addEventListener("click",l=>{l.target===o&&a()}),document.addEventListener("keydown",s),document.body.appendChild(o),{body:d,close:a}}function ce(n){const{body:o,close:t}=M("Choose an icon"),e=document.createElement("input");e.type="text",e.placeholder="Search icons\u2026",e.className="form-input",e.style.cssText="padding:.45rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);background:var(--dm-input-bg,transparent);color:var(--dm-text);",o.appendChild(e);const i=document.createElement("div");i.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(46px,1fr));gap:4px;overflow-y:auto;min-height:0;",o.appendChild(i);const r=window.I&&typeof I.list=="function"?I.list():[],d=r.map(s=>{const a=document.createElement("button");a.type="button",a.title=s,a.dataset.iconName=s,a.style.cssText="display:flex;align-items:center;justify-content:center;padding:.55rem 0;border:1px solid transparent;border-radius:6px;background:transparent;color:inherit;cursor:pointer;";const l=document.createElement("span");return l.setAttribute("data-icon",s),l.setAttribute("data-icon-size","18"),a.appendChild(l),a.addEventListener("mouseenter",()=>{a.style.borderColor="var(--dm-border)"}),a.addEventListener("mouseleave",()=>{a.style.borderColor="transparent"}),a.addEventListener("click",()=>{t(),n(s)}),i.appendChild(a),a});if(!r.length){const s=document.createElement("p");s.textContent="Icon engine unavailable - type an icon name into the field instead.",i.appendChild(s)}window.I?.scan?.(i),e.addEventListener("input",()=>{const s=e.value.trim().toLowerCase();d.forEach(a=>{a.style.display=!s||a.dataset.iconName.includes(s)?"":"none"})}),e.focus()}async function le(){const n=typeof S<"u"&&S.get?S.get("auth_token"):null;if(!n)return{error:"auth"};const o=i=>fetch("/api/media",{headers:{Authorization:"Bearer "+i}});let t;try{if(t=await o(n),t.status===401&&S.get("auth_refresh_token")){const i=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:S.get("auth_refresh_token")})});if(i.ok){const{token:r}=await i.json();S.set("auth_token",r),t=await o(r)}}}catch{return{error:"load"}}if(!t.ok)return{error:t.status===401||t.status===403?"auth":"load"};const e=await t.json().catch(()=>[]);return{files:(Array.isArray(e)?e:[]).filter(i=>/\.(png|jpe?g|gif|webp|svg|avif)$/i.test(i.name||i.url||""))}}function ue(n){const{body:o,close:t}=M("Choose an image"),e=document.createElement("p");e.textContent="Loading media library\u2026",e.style.cssText="margin:0;font-size:.85em;color:var(--dm-text-muted);",o.appendChild(e),le().then(({files:i,error:r})=>{if(r){e.textContent=r==="auth"?"Sign in with media access to browse the library - or paste an image URL into the field.":"Could not load the media library - paste an image URL into the field instead.";return}if(!i.length){e.textContent="No images in the media library yet.";return}e.remove();const d=document.createElement("div");d.style.cssText="display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;overflow-y:auto;min-height:0;",i.forEach(s=>{const a=document.createElement("button");a.type="button",a.title=s.name||s.url,a.style.cssText="display:flex;flex-direction:column;gap:.25rem;padding:.35rem;border:1px solid var(--dm-border);border-radius:8px;background:transparent;color:inherit;cursor:pointer;min-width:0;";const l=document.createElement("img");l.src=s.url,l.alt=s.name||"",l.loading="lazy",l.style.cssText="width:100%;height:72px;object-fit:cover;border-radius:5px;";const h=document.createElement("span");h.textContent=s.name||s.url,h.style.cssText="font-size:.68em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",a.appendChild(l),a.appendChild(h),a.addEventListener("click",()=>{t(),n(s.url)}),d.appendChild(a)}),o.appendChild(d)})}function V(n,o){n.value=o,n.dispatchEvent(new Event("input",{bubbles:!0})),n.dispatchEvent(new Event("change",{bubbles:!0}))}function W(n){const o=document.createElement("div");return o.style.cssText="display:flex;align-items:center;gap:.5rem;",n.parentNode.insertBefore(o,n),o.appendChild(n),n.style.flex="1",n.style.minWidth="0",o}function U(n){const o=document.createElement("button");return o.type="button",o.textContent=n,o.style.cssText=D,o}function de(n){if(!window.I||typeof I.list!="function")return;const o=W(n),t=document.createElement("span");t.style.cssText="display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;flex-shrink:0;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);";const e=()=>{t.textContent="";const r=n.value.trim();if(!r)return;const d=document.createElement("span");d.setAttribute("data-icon",r),d.setAttribute("data-icon-size","16"),t.appendChild(d),window.I?.scan?.(t)};n.addEventListener("input",e),e();const i=U("Browse");i.addEventListener("click",()=>ce(r=>{V(n,r),e()})),o.appendChild(t),o.appendChild(i)}function me(n){const o=W(n),t=document.createElement("img");t.alt="",t.style.cssText="width:2rem;height:2rem;flex-shrink:0;object-fit:cover;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md,6px);display:none;";const e=()=>{const r=n.value.trim();t.style.display=r?"":"none",r&&(t.src=r)};n.addEventListener("input",e),e();const i=U("Browse");i.addEventListener("click",()=>ue(r=>{V(n,r),e()})),o.appendChild(t),o.appendChild(i)}export function enhancePickerFields(n,o){const t=typeof n=="string"?document.querySelector(n):n;t&&(o||[]).forEach(function(e){if(!e.name||e.type!=="icon"&&e.type!=="image")return;const i=t.querySelector('[name="'+e.name+'"]');!i||i.dataset.pickerEnhanced||(i.dataset.pickerEnhanced="1",e.type==="icon"?de(i):me(i))})}function N(n,o){const t=document.createElement("div");t.className="fb-form-honeypot",t.setAttribute("aria-hidden","true");const e=document.createElement("input");e.name=n.querySelector('[name="website"]')?"_website":"website",t.dataset.hpName=e.name,e.type="text",e.tabIndex=-1,e.autocomplete="url",e.placeholder="https://",t.appendChild(e);const i=document.createElement("input");i.name="_t",i.type="hidden",i.value=o||Date.now(),t.appendChild(i),n.appendChild(t)}function H(n,o){const t=n.querySelector("form");if(!t)return;const e=Array.from(t.querySelectorAll(".form-group"));let i=0;o.forEach(function(r){if(r.type==="spacer"){const d=document.createElement("div");d.className="fb-spacer";const s=e[i];if(s)t.insertBefore(d,s);else{const a=t.querySelector('[type="submit"]');a?t.insertBefore(d,a):t.appendChild(d)}}else r.type!=="page-break"&&i++})}function C(n,o,t,e,i,r,d){const s=i||e,a=s.querySelector(".fb-form-honeypot"),l=a?.querySelector('input[type="text"]')?.value||"",h=a?.querySelector('[name="_t"]')?.value||"",u=Object.assign({},o);delete u._t,a?.dataset.hpName&&delete u[a.dataset.hpName];const c=new Map(r||[]);L(s,c);const f=ne(u,d,c),p=re(e,f.files);if(p)return x(e,p,"error"),Promise.resolve();const{body:w,headers:b}=oe(Object.assign(f.values,{_hp:l,_t:h}),f.files);return fetch("/api/forms/submit/"+n,{method:"POST",headers:b,body:w}).then(y=>y.json().then(v=>({ok:y.ok,body:v}))).then(y=>{y.ok&&y.body.ok?(X(e),e.textContent="",x(e,y.body.message||t.successMessage||"Thank you.","success")):x(e,y.body.error||"Something went wrong.","error")}).catch(()=>{x(e,"Unable to submit. Please check your connection.","error")})}function pe(n,o,t,e,i){const r=document.createElement("form");r.noValidate=!0,o.forEach(function(a){const l=document.createElement("div");l.className="form-group",l.style.marginBottom="1.25rem",a.name&&(l.dataset.field=a.name);const h=document.createElement("label");if(h.className="form-label",h.textContent=a.label||a.name,a.required){const c=document.createElement("span");c.textContent=" *",c.style.color="var(--dm-danger)",h.appendChild(c)}let u;a.type==="textarea"?(u=document.createElement("textarea"),u.rows=a.formConfig?.rows||4,u.className="form-input"):a.type==="select"?(u=document.createElement("select"),u.className="form-input",(a.options||[]).forEach(function(c){const f=document.createElement("option");f.value=typeof c=="string"?c:c.value??"",f.textContent=typeof c=="string"?c:c.label||f.value,u.appendChild(f)})):(u=document.createElement("input"),u.type=a.type==="icon"||a.type==="image"?"text":a.type||"text",u.className="form-input",a.placeholder&&(u.placeholder=a.placeholder),a.type==="file"&&(a.file?.accept&&(u.accept=a.file.accept),a.multiple&&(u.multiple=!0))),u.name=a.name,u.required=a.required||!1,l.appendChild(h),l.appendChild(u),r.appendChild(l)}),t.honeypot&&N(r);const d=document.createElement("button");if(d.type="submit",d.className="btn btn-primary",d.textContent=t.submitText||"Submit",G(t,"stacked").length||$(t)){const a=document.createElement("div");a.className="form-group form-buttons",a.appendChild(d),r.appendChild(a)}else r.appendChild(d);if(r.addEventListener("submit",function(a){a.preventDefault();const l={};if(o.forEach(function(u){const c=r.querySelector('[name="'+u.name+'"]');c&&(l[u.name]=c.value)}),window.FormLogicEngine&&i){const u=window.FormLogicEngine,c=[],f=[];if(o.forEach(function(p){if(u.evaluateFieldVisibility(p,l)==="hidden"){delete l[p.name];return}const b=u.evaluateFieldRequirement(p,l),y=l[p.name];b&&(!y||!String(y).trim())&&c.push(p.label||p.name);const v=u.validateField(p,y||"",l);v.length&&f.push(v[0].message)}),c.length||f.length){const p=[];c.length&&p.push("Required: "+c.join(", ")),f.length&&p.push(f.join("; ")),x(n,p.join(". "),"error");return}}const h=Q(r,o);if(h.length){x(n,h.join(" "),"error");return}if(n.dataset.triggerBlocked==="true"){const u=n._formLogicRuntime,c=u&&u.submitBlock?u.submitBlock().reasons:[];x(n,c.join(" ")||"This form cannot be submitted yet.","error");return}n.classList.add("fb-form-loading"),d.disabled=!0,C(e,l,t,n,r,null,o).finally(function(){n.classList.remove("fb-form-loading"),d.disabled=!1})}),n.appendChild(r),_(r,t,"stacked"),F(r,t,o),enhancePickerFields(n,o),Z(n,o),q(n,function(a){return C(e,a,t,n,r,null,o)}),window.FormLogicEngine&&i&&T(o)){const a=new window.FormLogicEngine.FormLogicRuntime(i,n);a.init(),n._formLogicRuntime=a}}function fe(n){const o=n.getAttribute("data-form");o&&fetch("/api/forms/"+o+"/public").then(t=>{if(!t.ok)throw new Error("Form not found: "+o);return t.json()}).then(t=>{const e=t.fields||[],i=t.settings||{},r=document.createElement("div");r.className="fb-form-wrapper",n.appendChild(r),j(r,t.fileLimits);const d=e.some(s=>s.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const s=Number(i.columns)===3?3:2,a=i.layout||"grid";if(d&&Domma.forms.wizard){const l=J(e,t.title),h=l.map(m=>m.fields),u=l.map(function(m){return{title:m.title,description:m.description,fields:z(m.fields,s)}});let c=0,f=null,p=null;const w=Date.now(),b=new Map;j(r,t.fileLimits,b),B(r,e);const y=function(m){const g=window.FormLogicEngine;if(!g||!g.validateFields)return;const k=g.validateFields(r,h[c]||[]);k.valid||(m.preventDefault(),m.stopImmediatePropagation(),markInvalidFields(r,k.missing),showValidationPopup(k))},v=function(){const m=h[c]||e;if(Y(r,i),F(r,i,m),O(r,m),enhancePickerFields(r,m),te(r,b),m.some(g=>g.type==="spacer")&&H(r,m),i.honeypot){const g=r.querySelector("form");g&&N(g,w)}window.FormLogicEngine&&T(e)&&(f&&f.destroy(),f=new window.FormLogicEngine.FormLogicRuntime(t,r,{baseValues:p&&p.getStepData?p.getStepData():{}}),f.init(),P(r,f)),r.querySelectorAll(".wizard-next, .wizard-submit").forEach(g=>g.addEventListener("click",y,!0))};p=Domma.forms.wizard(r,{schema:{steps:u},layout:a,columns:s,finishText:i.submitText||"Finish",onStepChange:function(m){L(r,b),c=m,setTimeout(v,0)},onSubmit:function(m){return L(r,b),C(o,m,i,r,null,b,e)}}),q(r,function(m){return L(r,b),C(o,m,i,r,null,b,e)}),r.addEventListener("dm-form-trigger",function(m){if(!m.detail||m.detail.type!=="goto-step"||!p||!p.goToStep)return;const g=Number(m.detail.action.step);!isFinite(g)||g<0||g>=u.length||g!==c&&p.goToStep(g)}),Promise.resolve(p).then(v)}else if(Domma.forms.render){const l=z(e,s),h=se(e);B(r,e),q(r,function(c){return C(o,c,i,r,null,null,e)});const u=Domma.forms.render(r,l,h,{submitText:i.submitText||"Submit",layout:a,columns:s,onSubmit:function(c){return C(o,c,i,r,null,null,e)}});Promise.resolve(u).then(function(){if(_(r,i,a),F(r,i,e),O(r,e),enhancePickerFields(r,e),window.FormLogicEngine&&T(e)){const c=new window.FormLogicEngine.FormLogicRuntime(t,r);c.init(),P(r,c)}if(e.some(c=>c.type==="spacer")&&H(r,e),i.honeypot){const c=r.querySelector("form");c&&N(c)}})}}else pe(r,e.filter(s=>s.type!=="page-break"&&s.type!=="spacer"),i,o,t)}).catch(t=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:var(--dm-danger);font-style:italic;",n.appendChild(e),console.warn("[forms]",t.message)})}