From 413f69cf434209ef19a759c77ce7222b273ddf1e Mon Sep 17 00:00:00 2001 From: prismiwi2015 Date: Sun, 16 Aug 2026 19:07:40 +0200 Subject: [PATCH] Fix everything a full review of the plugin found A complete review pass before submission: WordPress.org compliance, live end-to-end verification in wp-env, and a code review across every PHP include and TypeScript module, with each confirmed defect fixed and locked in by a regression test. 535 PHPUnit + 242 vitest tests pass; Plugin Check reports zero errors on the shipped tree. The headline fixes: a tampered client total could survive a formula failure; scientific notation broke formula substitution in both engines; the "Other" choice option was rejected by its own whitelist; every query-carrying REST path 404ed on plain-permalink sites, taking the Entries and Analytics screens down on a default install; and Analytics was only reachable inside OpenStation despite the readme's promise, so it is now an ordinary Forms submenu page too. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0119U4sRRWGcTQdwYreTwpAp --- assets/js/analytics.js | 55 +++- assets/js/analytics.min.js | 2 +- assets/js/builder.js | 320 +++++++++++++++----- assets/js/builder.min.js | 6 +- assets/js/entries.js | 40 ++- assets/js/entries.min.js | 2 +- assets/js/form.js | 26 +- assets/js/form.min.js | 2 +- assets/js/widget.js | 7 +- assets/js/widget.min.js | 2 +- docs/architecture.md | 9 +- docs/hooks-reference.md | 31 +- includes/admin-page.php | 35 ++- includes/availability.php | 13 +- includes/calc.php | 12 +- includes/confirmations.php | 17 +- includes/entries.php | 9 + includes/field-types.php | 25 +- includes/merge-tags.php | 9 +- includes/render-controls.php | 11 +- includes/render.php | 20 +- includes/rest.php | 8 + includes/resume.php | 65 +++- includes/schema.php | 102 ++++++- includes/spam.php | 57 +++- includes/submission.php | 30 +- includes/uploads.php | 43 +++ includes/validation.php | 9 +- readme.txt | 4 +- src/analytics.ts | 94 +++++- src/api.ts | 18 +- src/builder.ts | 462 ++++++++++++++++++++++++----- src/entries.ts | 67 ++++- src/form.ts | 46 ++- src/merge-tags.ts | 12 +- src/preview-button.ts | 4 +- src/shared/calc.ts | 15 +- src/theme-studio.ts | 28 +- tests/fixtures/calc-cases.json | 4 +- tests/phpunit/tests/calc.php | 37 +++ tests/phpunit/tests/entries.php | 71 +++++ tests/phpunit/tests/mergeTags.php | 16 + tests/phpunit/tests/render.php | 35 +++ tests/phpunit/tests/resume.php | 121 ++++++++ tests/phpunit/tests/schema.php | 43 +++ tests/phpunit/tests/submission.php | 268 +++++++++++++++++ tests/phpunit/tests/validation.php | 64 ++++ tests/vitest/calc.test.ts | 6 +- tests/vitest/field-move.test.ts | 158 ++++++++++ tests/vitest/form-enhance.test.ts | 169 +++++++++++ tests/vitest/histogram.test.ts | 126 ++++++++ tests/vitest/rest-url.test.ts | 53 ++++ uninstall.php | 254 +++++++++------- 53 files changed, 2765 insertions(+), 377 deletions(-) create mode 100644 tests/vitest/field-move.test.ts create mode 100644 tests/vitest/form-enhance.test.ts create mode 100644 tests/vitest/histogram.test.ts create mode 100644 tests/vitest/rest-url.test.ts diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 199f1ab..e91ecb3 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -13,11 +13,14 @@ var allTerrainFormsAnalytics = function(exports) { this.code = code; } } + function joinPath(base, path) { + return base.includes("?") ? `${base}${path.replace("?", "&")}` : `${base}${path}`; + } async function request(path, init = {}) { if (!config?.restUrl) { throw new ApiError("AllTerrain Forms is not configured on this page.", 0); } - const url = `${config.restUrl}${path}`; + const url = joinPath(config.restUrl, path); const headers = { "Content-Type": "application/json", ...init.headers ?? {} @@ -55,7 +58,7 @@ var allTerrainFormsAnalytics = function(exports) { } const headers = config.nonce ? { "X-WP-Nonce": config.nonce } : {}; const shell = getShell(); - const url = `${config.wpRestUrl}${route}`; + const url = joinPath(config.wpRestUrl, route); const response = shell?.fetch ? await shell.fetch(url, { credentials: "same-origin", headers }, { source: "allterrain-forms" }) : await fetch(url, { credentials: "same-origin", headers }); if (!response.ok) { throw new ApiError(`Request failed with status ${response.status}.`, response.status); @@ -278,6 +281,40 @@ var allTerrainFormsAnalytics = function(exports) { { key: "passives", label: "Passives", hint: "7–8" }, { key: "promoters", label: "Promoters", hint: "9–10" } ]; + const MAX_HISTOGRAM_BUCKETS = 50; + function histogramBuckets(numbers) { + const lo = Math.floor(numbers.min); + const hi = Math.ceil(numbers.max); + if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi < lo) { + return []; + } + const span = hi - lo + 1; + const size = Math.max(1, Math.ceil(span / MAX_HISTOGRAM_BUCKETS)); + const total = Math.ceil(span / size); + const indexOf = (value) => Math.min(total - 1, Math.max(0, Math.floor((value - lo) / size))); + const buckets = []; + for (let index = 0; index < total; index++) { + const start = lo + index * size; + const end = Math.min(hi, start + size - 1); + buckets.push({ + label: size === 1 ? String(start) : `${start}–${end}`, + count: 0, + holdsMean: false + }); + } + for (const [key, count] of Object.entries(numbers.distribution)) { + const value = Math.round(Number(key)); + if (!Number.isFinite(value)) { + continue; + } + buckets[indexOf(value)].count += count; + } + const mean = Math.round(numbers.mean); + if (Number.isFinite(mean)) { + buckets[indexOf(mean)].holdsMean = true; + } + return buckets; + } class AnalyticsWindow { constructor(root) { this.forms = []; @@ -658,25 +695,21 @@ var allTerrainFormsAnalytics = function(exports) { if (!numbers) { return el("div"); } - const buckets = []; - for (let value = Math.floor(numbers.min); value <= Math.ceil(numbers.max); value++) { - buckets.push({ value, count: numbers.distribution[String(value)] ?? 0 }); - } + const buckets = histogramBuckets(numbers); const peak = buckets.reduce((most, bucket) => Math.max(most, bucket.count), 0); - const meanAt = Math.round(numbers.mean); return el("div", { class: "atfa-hist", children: buckets.map( (bucket) => el("div", { - class: `atfa-hist__col${bucket.value === meanAt ? " is-mean" : ""}`, - attrs: { title: `${bucket.value}: ${bucket.count}` }, + class: `atfa-hist__col${bucket.holdsMean ? " is-mean" : ""}`, + attrs: { title: `${bucket.label}: ${bucket.count}` }, children: [ el("span", { class: "atfa-hist__count", text: bucket.count ? String(bucket.count) : "" }), el("div", { class: "atfa-hist__bar", attrs: { style: `height:${peak ? Math.max(2, bucket.count / peak * 100) : 0}%` } }), - el("span", { class: "atfa-hist__tick", text: String(bucket.value) }) + el("span", { class: "atfa-hist__tick", text: bucket.label }) ] }) ) @@ -834,6 +867,8 @@ var allTerrainFormsAnalytics = function(exports) { } else { mountAnalytics(); } + exports.MAX_HISTOGRAM_BUCKETS = MAX_HISTOGRAM_BUCKETS; + exports.histogramBuckets = histogramBuckets; exports.mountAnalytics = mountAnalytics; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); return exports; diff --git a/assets/js/analytics.min.js b/assets/js/analytics.min.js index 842bc43..1539378 100644 --- a/assets/js/analytics.min.js +++ b/assets/js/analytics.min.js @@ -1 +1 @@ -var allTerrainFormsAnalytics=function($){"use strict";function k(){return window.wp?.os??null}const c=window.allTerrainForms;class f extends Error{constructor(e,t,a=""){super(e),this.name="ApiError",this.status=t,this.code=a}}async function _(s,e={}){if(!c?.restUrl)throw new f("AllTerrain Forms is not configured on this page.",0);const t=`${c.restUrl}${s}`,a={"Content-Type":"application/json",...e.headers??{}};c.nonce&&(a["X-WP-Nonce"]=c.nonce);const i=k(),o=await(i?.fetch?(l,u)=>i.fetch(l,u,{source:"allterrain-forms"}):(l,u)=>fetch(l,u))(t,{credentials:"same-origin",...e,headers:a});if(!o.ok){let l=`Request failed with status ${o.status}.`,u="";try{const T=await o.json();l=T.message??l,u=T.code??""}catch{}throw new f(l,o.status,u)}if(o.status!==204)return await o.json()}const d=s=>_(s);async function M(s){if(!c?.wpRestUrl)throw new f("AllTerrain Forms is not configured on this page.",0);const e=c.nonce?{"X-WP-Nonce":c.nonce}:{},t=k(),a=`${c.wpRestUrl}${s}`,i=t?.fetch?await t.fetch(a,{credentials:"same-origin",headers:e},{source:"allterrain-forms"}):await fetch(a,{credentials:"same-origin",headers:e});if(!i.ok)throw new f(`Request failed with status ${i.status}.`,i.status);return await i.json()}function F(s){if(!s||!s.includes("&"))return s;const e=document.createElement("textarea");return e.innerHTML=s,e.value}const h=(s,e)=>_(s,{method:"POST",body:JSON.stringify(e)}),p=s=>_(s,{method:"DELETE"});function E(s){const e=new URLSearchParams;for(const[a,i]of Object.entries(s))i===void 0||i===""||i===!1||e.set(a,String(i));const t=e.toString();return t?`?${t}`:""}const m={config:()=>d("/config"),listForms:()=>d("/forms"),getForm:s=>d(`/forms/${s}`),createForm:s=>h("/forms",s),updateForm:(s,e)=>h(`/forms/${s}`,e),duplicateForm:s=>h(`/forms/${s}/duplicate`,{}),deleteForm:s=>p(`/forms/${s}`),preview:(s,e)=>h(`/forms/${s}/preview`,e),pages:()=>M("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(s=>s.map(e=>({id:e.id,title:F(e.title?.rendered??"")||`#${e.id}`}))),mergeTags:s=>d(`/forms/${s}/merge-tags`).then(e=>e.groups),analytics:(s,e="")=>d(`/forms/${s}/analytics${e?`?dimension=${encodeURIComponent(e)}`:""}`),demoStatus:()=>d("/demo"),demoSeed:s=>h("/demo",s?{count:s}:{}),demoRemove:()=>p("/demo"),listEntries:s=>d(`/entries${E(s)}`),getEntry:s=>d(`/entries/${s}`),updateEntry:(s,e)=>h(`/entries/${s}`,e),deleteEntry:s=>p(`/entries/${s}`),exportEntries:s=>d(`/entries/export${E(s)}`),listThemes:()=>d("/themes"),saveTheme:s=>h("/themes",s),deleteTheme:s=>p(`/themes/${s}`)};function n(s,e={}){const t=document.createElement(s);e.class&&(t.className=e.class),e.text!==void 0&&(t.textContent=e.text),e.html!==void 0&&(t.innerHTML=e.html),e.title&&(t.title=e.title),e.type&&"type"in t&&(t.type=e.type),e.value!==void 0&&"value"in t&&(t.value=e.value),e.placeholder!==void 0&&"placeholder"in t&&(t.placeholder=e.placeholder),e.href&&"href"in t&&(t.href=e.href);for(const[a,i]of Object.entries(e.attrs??{}))i===void 0||i===!1||t.setAttribute(a,i===!0?"":String(i));Object.assign(t.style,e.style??{});for(const[a,i]of Object.entries(e.on??{}))t.addEventListener(a,i);for(const a of e.children??[])a==null||a===!1||t.append(a);return t}function b(s){s.replaceChildren()}function A(s,e,t){if(x("os-select")&&x("os-option")){const a=document.createElement("os-select");a.setAttribute("value",s),a.classList.add("atfb-field");for(const i of e){const r=document.createElement("os-option");r.setAttribute("value",i.value),r.textContent=i.label,a.append(r)}return a.addEventListener("os-pick",i=>{t(String(i.detail?.value??""))}),a}return n("select",{class:"atfb-input atfb-select",on:{change:a=>t(a.target.value)},children:e.map(a=>n("option",{value:a.value,text:a.label,attrs:{selected:a.value===s}}))})}function P(s,e,t="secondary",a){const i=[null,n("span",{text:s})];if(x("os-button")){const r=document.createElement("os-button");r.setAttribute("variant",t),r.setAttribute("type","button"),r.classList.add("atfb-button",`atfb-button--${t}`),r.addEventListener("click",e);for(const o of i)o&&r.append(o);return r}return n("button",{class:`atfb-button atfb-button--${t}`,type:"button",on:{click:e},children:i})}function x(s){return typeof customElements<"u"&&!!customElements.get(s)}const q=["os-button","os-checkbox-label","os-number-field","os-select","os-option","os-segmented","os-segment","os-color-field","os-empty-state"];let v=null;function D(){if(v)return v;const s=window.wp?.os;return v=s?.loadComponents?s.loadComponents(q).catch(()=>{}):Promise.resolve(),v}async function L(s,e=""){const t=window.wp?.os;return t?.confirm?t.confirm({title:e,message:s,danger:!0}):window.confirm(s)}function y(s,e="",t="info"){const a=window.wp?.os;if(a?.notify){a.notify({title:s,body:e,type:t});return}console.info(`[AllTerrain Forms] ${s}${e?`: ${e}`:""}`)}const C=[{key:"detractors",label:"Detractors",hint:"0–6"},{key:"passives",label:"Passives",hint:"7–8"},{key:"promoters",label:"Promoters",hint:"9–10"}];class N{constructor(e){this.forms=[],this.formId=0,this.dimension="",this.report=null,this.demo=null,this.demoAvailable=!1,this.bar=e.querySelector("[data-atfa-bar]")??n("div"),this.body=e.querySelector("[data-atfa-body]")??n("div")}async start(){await D();try{this.forms=await m.listForms()}catch{this.fail("Could not load your forms.");return}if(!this.forms.length){this.fail("There are no forms yet. Build one, collect a few submissions, and this fills in.");return}if(c?.devMode&&c?.canEdit)try{this.demo=await m.demoStatus(),this.demoAvailable=!0}catch{this.demoAvailable=!1}this.formId=this.forms[0].id,await this.load()}async load(){try{this.report=await m.analytics(this.formId,this.dimension)}catch{this.fail("Could not load that report.");return}this.dimension=this.report.breakdown?.id??"",this.renderBar(),this.render()}fail(e){b(this.bar),b(this.body),this.body.append(n("p",{class:"atfa__empty",text:e}))}renderBar(){b(this.bar),this.bar.append(A(String(this.formId),this.forms.map(t=>({value:String(t.id),label:t.title||`#${t.id}`})),t=>{this.formId=Number(t),this.dimension="",this.load()}));const e=this.report?.dimensions??[];e.length>1&&this.bar.append(n("span",{class:"atfa__bar-label",text:"Group by"}),A(this.dimension,e.map(t=>({value:t.id,label:t.label})),t=>{this.dimension=t,this.load()}))}render(){const e=this.report;if(b(this.body),!!e){if(this.body.append(this.kpis(e)),e.submissions<1&&e.sampled<1)this.body.append(n("p",{class:"atfa__empty",text:"No submissions yet. Everything below fills in as they arrive."}));else{this.body.append(this.timeline(e));for(const t of e.fields)t.nps&&this.body.append(this.npsPanel(t));e.breakdown&&e.breakdown.groups.length&&this.body.append(this.breakdownPanel(e.breakdown)),this.body.append(this.questions(e))}this.demoAvailable&&this.body.append(this.demoPanel())}}kpis(e){const t=[{label:"Submissions",value:String(e.submissions)},{label:"Views",value:String(e.views)},{label:"Conversion",value:`${e.conversion}%`,hint:"of people who saw it"},{label:"Completion",value:`${e.completion}%`,hint:"of people who started"},{label:"Unread",value:String(e.unread)},{label:"Spam",value:String(e.spam)}];return n("div",{class:"atfa-kpis",children:t.map(a=>n("div",{class:"atfa-kpi",children:[n("span",{class:"atfa-kpi__value",text:a.value}),n("span",{class:"atfa-kpi__label",text:a.label}),a.hint?n("span",{class:"atfa-kpi__hint",text:a.hint}):null]}))})}timeline(e){const t=e.timeline,a=t.reduce((r,o)=>Math.max(r,o.count),0),i=t.reduce((r,o)=>r+o.count,0);return this.panel("When they answered",`${i} in the last ${t.length} days · busiest day ${a}`,[n("div",{class:"atfa-timeline",children:t.map(r=>n("div",{class:`atfa-timeline__day${r.count?"":" is-empty"}`,attrs:{style:`height:${a?Math.max(2,r.count/a*100):0}%`,title:`${r.date}: ${r.count}`}}))}),n("div",{class:"atfa-timeline__axis",children:[n("span",{text:t[0]?.date??""}),n("span",{text:t[t.length-1]?.date??""})]})])}npsPanel(e){const t=e.nps;if(!t)return n("div");const a=i=>t.responses?i/t.responses*100:0;return this.panel(e.label,`${t.responses} answers · Net Promoter Score`,[n("div",{class:"atfa-nps",children:[n("div",{class:`atfa-nps__score is-${t.score>=50?"great":t.score>=0?"ok":"poor"}`,children:[n("span",{class:"atfa-nps__number",text:`${t.score>0?"+":""}${t.score}`}),n("span",{class:"atfa-nps__caption",text:"NPS"})]}),n("div",{class:"atfa-nps__detail",children:[n("div",{class:"atfa-nps__bar",children:C.map(i=>n("div",{class:`atfa-nps__band is-${i.key}`,attrs:{style:`width:${a(t[i.key])}%`,title:`${i.label} (${i.hint}): ${t[i.key]}`}}))}),n("div",{class:"atfa-nps__legend",children:C.map(i=>n("span",{class:`atfa-nps__key is-${i.key}`,children:[n("strong",{text:String(t[i.key])}),n("span",{text:` ${i.label} `}),n("small",{text:i.hint})]}))})]})]})])}breakdownPanel(e){const t=e.groups[0]?.metrics??[];if(!t.length)return n("div");const a=new Map;for(const o of e.groups)for(const l of o.metrics)a.set(l.id,Math.max(a.get(l.id)??0,l.mean));const i=n("tr",{children:[n("th",{text:e.label,attrs:{scope:"col"}}),n("th",{text:"Answers",attrs:{scope:"col"}}),...t.map(o=>n("th",{text:o.label,attrs:{scope:"col"}}))]}),r=e.groups.map(o=>n("tr",{children:[n("th",{class:"atfa-table__label",text:o.label,attrs:{scope:"row"}}),n("td",{class:"atfa-table__count",text:String(o.count)}),...o.metrics.map(l=>n("td",{children:[n("div",{class:"atfa-meter",children:[n("div",{class:"atfa-meter__fill",attrs:{style:`width:${l.mean/(a.get(l.id)||1)*100}%`}})]}),n("span",{class:"atfa-meter__value",text:l.nps!==null?`${l.mean.toFixed(1)} · NPS ${l.nps>0?"+":""}${l.nps}`:l.mean.toFixed(2)})]}))]}));return this.panel(`Broken down by ${e.label.toLowerCase()}`,"",[n("table",{class:"atfa-table",children:[n("thead",{children:[i]}),n("tbody",{children:r})]})])}questions(e){return n("div",{class:"atfa-questions",children:e.fields.map(t=>this.question(t,e.sampled))})}question(e,t){const a=[];if(e.choices.length){const i=e.choices.reduce((r,o)=>Math.max(r,o.count),0);a.push(n("ul",{class:"atfa-bars",children:e.choices.map(r=>n("li",{class:"atfa-bars__row",children:[n("span",{class:"atfa-bars__label",text:r.label}),n("div",{class:"atfa-meter",children:[n("div",{class:"atfa-meter__fill",attrs:{style:`width:${i?r.count/i*100:0}%`}})]}),n("span",{class:"atfa-bars__value",text:`${r.count} ${e.answered?Math.round(r.count/e.answered*100):0}%`})]}))}))}return e.numbers&&a.push(this.histogram(e)),a.length||a.push(n("p",{class:"atfa-question__note",text:`${e.answered} of ${t} people answered this.`})),this.panel(e.label,this.questionSummary(e,t),a)}questionSummary(e,t){const a=[`${e.rate}% answered`];return e.numbers&&a.push(`mean ${e.numbers.mean}`,`median ${e.numbers.median}`),a.push(`${e.answered}/${t}`),a.join(" · ")}histogram(e){const t=e.numbers;if(!t)return n("div");const a=[];for(let o=Math.floor(t.min);o<=Math.ceil(t.max);o++)a.push({value:o,count:t.distribution[String(o)]??0});const i=a.reduce((o,l)=>Math.max(o,l.count),0),r=Math.round(t.mean);return n("div",{class:"atfa-hist",children:a.map(o=>n("div",{class:`atfa-hist__col${o.value===r?" is-mean":""}`,attrs:{title:`${o.value}: ${o.count}`},children:[n("span",{class:"atfa-hist__count",text:o.count?String(o.count):""}),n("div",{class:"atfa-hist__bar",attrs:{style:`height:${i?Math.max(2,o.count/i*100):0}%`}}),n("span",{class:"atfa-hist__tick",text:String(o.value)})]}))})}demoPanel(){const e=this.demo,t=n("p",{class:"atfa-demo__status",text:this.demoSummary(e)}),a=P("Fill with demo data",async()=>{await this.seedAll(t)}),i=P("Remove demo data",async()=>{if(await L("Delete the demo survey and every entry it generated?","Remove demo data")){t.textContent="Removing…";try{const r=await m.demoRemove();y("Demo data removed",`${r.entries} entries and ${r.forms} form.`)}catch{y("Could not remove the demo data","","error")}await this.refreshDemo(t)}});return n("div",{class:"atfa-demo",attrs:{"data-atfa-demo":""},children:[n("h3",{class:"atfa-demo__title",text:"Developer"}),n("p",{class:"atfa-demo__note",text:"Generates a team pulse survey and several hundred submissions through the real submission pipeline, so this report has something to report on. Everything it makes is tagged, and removing it deletes exactly that and nothing else."}),t,n("div",{class:"atfa-demo__actions",children:[a,i]})]})}demoSummary(e){return!e||!e.formId?"No demo data on this site.":`${e.title}: ${e.entries} of ${e.target} submissions.`}async seedAll(e){let t=0,a=0;for(;;){try{const i=await m.demoSeed();if(this.demo=i,a=0,e.textContent=`Generating… ${i.entries} of ${i.target}`,i.remaining<1){y("Demo data ready",`${i.entries} submissions.`);break}}catch{if(a+=1,a>=3){e.textContent="Stopped early. Press it again to carry on from here.",y("Could not finish the demo data","Some submissions were made. Try again to continue.","error");break}}if(t+=1,t>200)break}if(await this.refreshDemo(e),this.demo?.formId){try{this.forms=await m.listForms()}catch{}this.formId=this.demo.formId,this.dimension=""}await this.load()}async refreshDemo(e){try{this.demo=await m.demoStatus()}catch{this.demo=null}e.textContent=this.demoSummary(this.demo)}panel(e,t,a){return n("section",{class:"atfa-panel",children:[n("header",{class:"atfa-panel__head",children:[n("h3",{class:"atfa-panel__title",text:e}),t?n("p",{class:"atfa-panel__note",text:t}):null]}),...a]})}revealDemo(){this.body.querySelector("[data-atfa-demo]")?.scrollIntoView({behavior:"smooth",block:"start"})}}let w=null;function g(){const s=document.querySelector("[data-atfa-root]");!s||s.dataset.atfaMounted||(s.dataset.atfaMounted="1",w=new N(s),w.start())}let S=!1;return document.addEventListener("atf-open-demo-panel",()=>{S=!0,window.setTimeout(()=>{w?.revealDemo(),S=!1},600)}),document.addEventListener("os-window-content-loaded",()=>{g(),S&&window.setTimeout(()=>w?.revealDemo(),400)}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",g):g(),$.mountAnalytics=g,Object.defineProperty($,Symbol.toStringTag,{value:"Module"}),$}({}); +var allTerrainFormsAnalytics=function(p){"use strict";function A(){return window.wp?.os??null}const c=window.allTerrainForms;class v extends Error{constructor(e,t,a=""){super(e),this.name="ApiError",this.status=t,this.code=a}}function T(s,e){return s.includes("?")?`${s}${e.replace("?","&")}`:`${s}${e}`}async function M(s,e={}){if(!c?.restUrl)throw new v("AllTerrain Forms is not configured on this page.",0);const t=T(c.restUrl,s),a={"Content-Type":"application/json",...e.headers??{}};c.nonce&&(a["X-WP-Nonce"]=c.nonce);const i=A(),r=await(i?.fetch?(l,u)=>i.fetch(l,u,{source:"allterrain-forms"}):(l,u)=>fetch(l,u))(t,{credentials:"same-origin",...e,headers:a});if(!r.ok){let l=`Request failed with status ${r.status}.`,u="";try{const h=await r.json();l=h.message??l,u=h.code??""}catch{}throw new v(l,r.status,u)}if(r.status!==204)return await r.json()}const d=s=>M(s);async function L(s){if(!c?.wpRestUrl)throw new v("AllTerrain Forms is not configured on this page.",0);const e=c.nonce?{"X-WP-Nonce":c.nonce}:{},t=A(),a=T(c.wpRestUrl,s),i=t?.fetch?await t.fetch(a,{credentials:"same-origin",headers:e},{source:"allterrain-forms"}):await fetch(a,{credentials:"same-origin",headers:e});if(!i.ok)throw new v(`Request failed with status ${i.status}.`,i.status);return await i.json()}function O(s){if(!s||!s.includes("&"))return s;const e=document.createElement("textarea");return e.innerHTML=s,e.value}const m=(s,e)=>M(s,{method:"POST",body:JSON.stringify(e)}),y=s=>M(s,{method:"DELETE"});function C(s){const e=new URLSearchParams;for(const[a,i]of Object.entries(s))i===void 0||i===""||i===!1||e.set(a,String(i));const t=e.toString();return t?`?${t}`:""}const f={config:()=>d("/config"),listForms:()=>d("/forms"),getForm:s=>d(`/forms/${s}`),createForm:s=>m("/forms",s),updateForm:(s,e)=>m(`/forms/${s}`,e),duplicateForm:s=>m(`/forms/${s}/duplicate`,{}),deleteForm:s=>y(`/forms/${s}`),preview:(s,e)=>m(`/forms/${s}/preview`,e),pages:()=>L("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(s=>s.map(e=>({id:e.id,title:O(e.title?.rendered??"")||`#${e.id}`}))),mergeTags:s=>d(`/forms/${s}/merge-tags`).then(e=>e.groups),analytics:(s,e="")=>d(`/forms/${s}/analytics${e?`?dimension=${encodeURIComponent(e)}`:""}`),demoStatus:()=>d("/demo"),demoSeed:s=>m("/demo",s?{count:s}:{}),demoRemove:()=>y("/demo"),listEntries:s=>d(`/entries${C(s)}`),getEntry:s=>d(`/entries/${s}`),updateEntry:(s,e)=>m(`/entries/${s}`,e),deleteEntry:s=>y(`/entries/${s}`),exportEntries:s=>d(`/entries/export${C(s)}`),listThemes:()=>d("/themes"),saveTheme:s=>m("/themes",s),deleteTheme:s=>y(`/themes/${s}`)};function n(s,e={}){const t=document.createElement(s);e.class&&(t.className=e.class),e.text!==void 0&&(t.textContent=e.text),e.html!==void 0&&(t.innerHTML=e.html),e.title&&(t.title=e.title),e.type&&"type"in t&&(t.type=e.type),e.value!==void 0&&"value"in t&&(t.value=e.value),e.placeholder!==void 0&&"placeholder"in t&&(t.placeholder=e.placeholder),e.href&&"href"in t&&(t.href=e.href);for(const[a,i]of Object.entries(e.attrs??{}))i===void 0||i===!1||t.setAttribute(a,i===!0?"":String(i));Object.assign(t.style,e.style??{});for(const[a,i]of Object.entries(e.on??{}))t.addEventListener(a,i);for(const a of e.children??[])a==null||a===!1||t.append(a);return t}function w(s){s.replaceChildren()}function F(s,e,t){if(k("os-select")&&k("os-option")){const a=document.createElement("os-select");a.setAttribute("value",s),a.classList.add("atfb-field");for(const i of e){const o=document.createElement("os-option");o.setAttribute("value",i.value),o.textContent=i.label,a.append(o)}return a.addEventListener("os-pick",i=>{t(String(i.detail?.value??""))}),a}return n("select",{class:"atfb-input atfb-select",on:{change:a=>t(a.target.value)},children:e.map(a=>n("option",{value:a.value,text:a.label,attrs:{selected:a.value===s}}))})}function P(s,e,t="secondary",a){const i=[null,n("span",{text:s})];if(k("os-button")){const o=document.createElement("os-button");o.setAttribute("variant",t),o.setAttribute("type","button"),o.classList.add("atfb-button",`atfb-button--${t}`),o.addEventListener("click",e);for(const r of i)r&&o.append(r);return o}return n("button",{class:`atfb-button atfb-button--${t}`,type:"button",on:{click:e},children:i})}function k(s){return typeof customElements<"u"&&!!customElements.get(s)}const I=["os-button","os-checkbox-label","os-number-field","os-select","os-option","os-segmented","os-segment","os-color-field","os-empty-state"];let g=null;function R(){if(g)return g;const s=window.wp?.os;return g=s?.loadComponents?s.loadComponents(I).catch(()=>{}):Promise.resolve(),g}async function j(s,e=""){const t=window.wp?.os;return t?.confirm?t.confirm({title:e,message:s,danger:!0}):window.confirm(s)}function _(s,e="",t="info"){const a=window.wp?.os;if(a?.notify){a.notify({title:s,body:e,type:t});return}console.info(`[AllTerrain Forms] ${s}${e?`: ${e}`:""}`)}const N=[{key:"detractors",label:"Detractors",hint:"0–6"},{key:"passives",label:"Passives",hint:"7–8"},{key:"promoters",label:"Promoters",hint:"9–10"}],q=50;function D(s){const e=Math.floor(s.min),t=Math.ceil(s.max);if(!Number.isFinite(e)||!Number.isFinite(t)||tMath.min(o-1,Math.max(0,Math.floor((h-e)/i))),l=[];for(let h=0;h({value:String(t.id),label:t.title||`#${t.id}`})),t=>{this.formId=Number(t),this.dimension="",this.load()}));const e=this.report?.dimensions??[];e.length>1&&this.bar.append(n("span",{class:"atfa__bar-label",text:"Group by"}),F(this.dimension,e.map(t=>({value:t.id,label:t.label})),t=>{this.dimension=t,this.load()}))}render(){const e=this.report;if(w(this.body),!!e){if(this.body.append(this.kpis(e)),e.submissions<1&&e.sampled<1)this.body.append(n("p",{class:"atfa__empty",text:"No submissions yet. Everything below fills in as they arrive."}));else{this.body.append(this.timeline(e));for(const t of e.fields)t.nps&&this.body.append(this.npsPanel(t));e.breakdown&&e.breakdown.groups.length&&this.body.append(this.breakdownPanel(e.breakdown)),this.body.append(this.questions(e))}this.demoAvailable&&this.body.append(this.demoPanel())}}kpis(e){const t=[{label:"Submissions",value:String(e.submissions)},{label:"Views",value:String(e.views)},{label:"Conversion",value:`${e.conversion}%`,hint:"of people who saw it"},{label:"Completion",value:`${e.completion}%`,hint:"of people who started"},{label:"Unread",value:String(e.unread)},{label:"Spam",value:String(e.spam)}];return n("div",{class:"atfa-kpis",children:t.map(a=>n("div",{class:"atfa-kpi",children:[n("span",{class:"atfa-kpi__value",text:a.value}),n("span",{class:"atfa-kpi__label",text:a.label}),a.hint?n("span",{class:"atfa-kpi__hint",text:a.hint}):null]}))})}timeline(e){const t=e.timeline,a=t.reduce((o,r)=>Math.max(o,r.count),0),i=t.reduce((o,r)=>o+r.count,0);return this.panel("When they answered",`${i} in the last ${t.length} days · busiest day ${a}`,[n("div",{class:"atfa-timeline",children:t.map(o=>n("div",{class:`atfa-timeline__day${o.count?"":" is-empty"}`,attrs:{style:`height:${a?Math.max(2,o.count/a*100):0}%`,title:`${o.date}: ${o.count}`}}))}),n("div",{class:"atfa-timeline__axis",children:[n("span",{text:t[0]?.date??""}),n("span",{text:t[t.length-1]?.date??""})]})])}npsPanel(e){const t=e.nps;if(!t)return n("div");const a=i=>t.responses?i/t.responses*100:0;return this.panel(e.label,`${t.responses} answers · Net Promoter Score`,[n("div",{class:"atfa-nps",children:[n("div",{class:`atfa-nps__score is-${t.score>=50?"great":t.score>=0?"ok":"poor"}`,children:[n("span",{class:"atfa-nps__number",text:`${t.score>0?"+":""}${t.score}`}),n("span",{class:"atfa-nps__caption",text:"NPS"})]}),n("div",{class:"atfa-nps__detail",children:[n("div",{class:"atfa-nps__bar",children:N.map(i=>n("div",{class:`atfa-nps__band is-${i.key}`,attrs:{style:`width:${a(t[i.key])}%`,title:`${i.label} (${i.hint}): ${t[i.key]}`}}))}),n("div",{class:"atfa-nps__legend",children:N.map(i=>n("span",{class:`atfa-nps__key is-${i.key}`,children:[n("strong",{text:String(t[i.key])}),n("span",{text:` ${i.label} `}),n("small",{text:i.hint})]}))})]})]})])}breakdownPanel(e){const t=e.groups[0]?.metrics??[];if(!t.length)return n("div");const a=new Map;for(const r of e.groups)for(const l of r.metrics)a.set(l.id,Math.max(a.get(l.id)??0,l.mean));const i=n("tr",{children:[n("th",{text:e.label,attrs:{scope:"col"}}),n("th",{text:"Answers",attrs:{scope:"col"}}),...t.map(r=>n("th",{text:r.label,attrs:{scope:"col"}}))]}),o=e.groups.map(r=>n("tr",{children:[n("th",{class:"atfa-table__label",text:r.label,attrs:{scope:"row"}}),n("td",{class:"atfa-table__count",text:String(r.count)}),...r.metrics.map(l=>n("td",{children:[n("div",{class:"atfa-meter",children:[n("div",{class:"atfa-meter__fill",attrs:{style:`width:${l.mean/(a.get(l.id)||1)*100}%`}})]}),n("span",{class:"atfa-meter__value",text:l.nps!==null?`${l.mean.toFixed(1)} · NPS ${l.nps>0?"+":""}${l.nps}`:l.mean.toFixed(2)})]}))]}));return this.panel(`Broken down by ${e.label.toLowerCase()}`,"",[n("table",{class:"atfa-table",children:[n("thead",{children:[i]}),n("tbody",{children:o})]})])}questions(e){return n("div",{class:"atfa-questions",children:e.fields.map(t=>this.question(t,e.sampled))})}question(e,t){const a=[];if(e.choices.length){const i=e.choices.reduce((o,r)=>Math.max(o,r.count),0);a.push(n("ul",{class:"atfa-bars",children:e.choices.map(o=>n("li",{class:"atfa-bars__row",children:[n("span",{class:"atfa-bars__label",text:o.label}),n("div",{class:"atfa-meter",children:[n("div",{class:"atfa-meter__fill",attrs:{style:`width:${i?o.count/i*100:0}%`}})]}),n("span",{class:"atfa-bars__value",text:`${o.count} ${e.answered?Math.round(o.count/e.answered*100):0}%`})]}))}))}return e.numbers&&a.push(this.histogram(e)),a.length||a.push(n("p",{class:"atfa-question__note",text:`${e.answered} of ${t} people answered this.`})),this.panel(e.label,this.questionSummary(e,t),a)}questionSummary(e,t){const a=[`${e.rate}% answered`];return e.numbers&&a.push(`mean ${e.numbers.mean}`,`median ${e.numbers.median}`),a.push(`${e.answered}/${t}`),a.join(" · ")}histogram(e){const t=e.numbers;if(!t)return n("div");const a=D(t),i=a.reduce((o,r)=>Math.max(o,r.count),0);return n("div",{class:"atfa-hist",children:a.map(o=>n("div",{class:`atfa-hist__col${o.holdsMean?" is-mean":""}`,attrs:{title:`${o.label}: ${o.count}`},children:[n("span",{class:"atfa-hist__count",text:o.count?String(o.count):""}),n("div",{class:"atfa-hist__bar",attrs:{style:`height:${i?Math.max(2,o.count/i*100):0}%`}}),n("span",{class:"atfa-hist__tick",text:o.label})]}))})}demoPanel(){const e=this.demo,t=n("p",{class:"atfa-demo__status",text:this.demoSummary(e)}),a=P("Fill with demo data",async()=>{await this.seedAll(t)}),i=P("Remove demo data",async()=>{if(await j("Delete the demo survey and every entry it generated?","Remove demo data")){t.textContent="Removing…";try{const o=await f.demoRemove();_("Demo data removed",`${o.entries} entries and ${o.forms} form.`)}catch{_("Could not remove the demo data","","error")}await this.refreshDemo(t)}});return n("div",{class:"atfa-demo",attrs:{"data-atfa-demo":""},children:[n("h3",{class:"atfa-demo__title",text:"Developer"}),n("p",{class:"atfa-demo__note",text:"Generates a team pulse survey and several hundred submissions through the real submission pipeline, so this report has something to report on. Everything it makes is tagged, and removing it deletes exactly that and nothing else."}),t,n("div",{class:"atfa-demo__actions",children:[a,i]})]})}demoSummary(e){return!e||!e.formId?"No demo data on this site.":`${e.title}: ${e.entries} of ${e.target} submissions.`}async seedAll(e){let t=0,a=0;for(;;){try{const i=await f.demoSeed();if(this.demo=i,a=0,e.textContent=`Generating… ${i.entries} of ${i.target}`,i.remaining<1){_("Demo data ready",`${i.entries} submissions.`);break}}catch{if(a+=1,a>=3){e.textContent="Stopped early. Press it again to carry on from here.",_("Could not finish the demo data","Some submissions were made. Try again to continue.","error");break}}if(t+=1,t>200)break}if(await this.refreshDemo(e),this.demo?.formId){try{this.forms=await f.listForms()}catch{}this.formId=this.demo.formId,this.dimension=""}await this.load()}async refreshDemo(e){try{this.demo=await f.demoStatus()}catch{this.demo=null}e.textContent=this.demoSummary(this.demo)}panel(e,t,a){return n("section",{class:"atfa-panel",children:[n("header",{class:"atfa-panel__head",children:[n("h3",{class:"atfa-panel__title",text:e}),t?n("p",{class:"atfa-panel__note",text:t}):null]}),...a]})}revealDemo(){this.body.querySelector("[data-atfa-demo]")?.scrollIntoView({behavior:"smooth",block:"start"})}}let $=null;function x(){const s=document.querySelector("[data-atfa-root]");!s||s.dataset.atfaMounted||(s.dataset.atfaMounted="1",$=new B(s),$.start())}let E=!1;return document.addEventListener("atf-open-demo-panel",()=>{E=!0,window.setTimeout(()=>{$?.revealDemo(),E=!1},600)}),document.addEventListener("os-window-content-loaded",()=>{x(),E&&window.setTimeout(()=>$?.revealDemo(),400)}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",x):x(),p.MAX_HISTOGRAM_BUCKETS=q,p.histogramBuckets=D,p.mountAnalytics=x,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({}); diff --git a/assets/js/builder.js b/assets/js/builder.js index ebc65f3..a9a62fb 100644 --- a/assets/js/builder.js +++ b/assets/js/builder.js @@ -253,11 +253,14 @@ var allTerrainFormsBuilder = function(exports) { this.code = code; } } + function joinPath(base, path) { + return base.includes("?") ? `${base}${path.replace("?", "&")}` : `${base}${path}`; + } async function request(path, init = {}) { if (!config?.restUrl) { throw new ApiError("AllTerrain Forms is not configured on this page.", 0); } - const url = `${config.restUrl}${path}`; + const url = joinPath(config.restUrl, path); const headers = { "Content-Type": "application/json", ...init.headers ?? {} @@ -295,7 +298,7 @@ var allTerrainFormsBuilder = function(exports) { } const headers = config.nonce ? { "X-WP-Nonce": config.nonce } : {}; const shell2 = getShell(); - const url = `${config.wpRestUrl}${route}`; + const url = joinPath(config.wpRestUrl, route); const response = shell2?.fetch ? await shell2.fetch(url, { credentials: "same-origin", headers }, { source: "allterrain-forms" }) : await fetch(url, { credentials: "same-origin", headers }); if (!response.ok) { throw new ApiError(`Request failed with status ${response.status}.`, response.status); @@ -1383,7 +1386,11 @@ var allTerrainFormsBuilder = function(exports) { } if (typeof document !== "undefined") { document.addEventListener("pointerdown", (event) => { - if (openPicker && !openPicker.contains(event.target)) { + const target = event.target; + if (target?.closest(".atfb-tagpick__open")) { + return; + } + if (openPicker && !openPicker.contains(target)) { closePicker(); } }); @@ -1730,6 +1737,10 @@ var allTerrainFormsBuilder = function(exports) { let active = options.activeSlug; let overrides = { ...options.overrides }; let themes = options.themes; + const replaceOverrides = (next) => { + overrides = next; + options.onOverridesReplaced?.({ ...next }); + }; const preview = el("div", { class: "atfs-preview__frame" }); const controls = el("div", { class: "atfs-controls__body" }); const quick = el("div", { class: "atfs-quick" }); @@ -1788,7 +1799,7 @@ var allTerrainFormsBuilder = function(exports) { on: { click: () => { active = theme.slug; - overrides = {}; + replaceOverrides({}); options.onTheme(active); renderThemes(); renderQuick(); @@ -2013,7 +2024,7 @@ var allTerrainFormsBuilder = function(exports) { themes = [...themes.filter((candidate) => candidate.slug !== saved.slug), saved]; options.onThemesChanged(themes); active = saved.slug; - overrides = {}; + replaceOverrides({}); options.onTheme(active); renderThemes(); renderControls(); @@ -2036,7 +2047,7 @@ var allTerrainFormsBuilder = function(exports) { themes = themes.filter((candidate) => candidate.slug !== theme.slug); options.onThemesChanged(themes); active = "clean"; - overrides = {}; + replaceOverrides({}); options.onTheme(active); renderThemes(); renderControls(); @@ -2196,6 +2207,9 @@ var allTerrainFormsBuilder = function(exports) { overrides[token] = value; } }, + onOverridesReplaced: (next) => { + overrides = { ...next }; + }, previewFor: async (slug, tokens) => { const form = await api.getForm(previewForm.id); form.schema.settings.theme = slug; @@ -2327,7 +2341,7 @@ var allTerrainFormsBuilder = function(exports) { if (!os?.windowManager?.open) { return; } - const open = document.querySelector(`[data-window-id^="${PREVIEW_WINDOW_ID}-${formId}"]`); + const open = document.querySelector(`[data-window-id="${PREVIEW_WINDOW_ID}-${formId}"]`); if (!open) { return; } @@ -2604,6 +2618,14 @@ var allTerrainFormsBuilder = function(exports) { return { key: `r${next}`, label }; }); } + function fieldMove(fields, fieldId, index) { + const from = fields.findIndex((field) => field.id === fieldId); + if (from < 0) { + return null; + } + const to = Math.max(0, Math.min(index, fields.length - 1)); + return to === from ? null : { from, to }; + } const PREFILL_SOURCES = [ { value: "user:email", label: "Their email address", group: "About the person filling it in", tag: "{user:email}" }, { value: "user:display_name", label: "Their name", group: "About the person filling it in", tag: "{user:display_name}" }, @@ -2633,7 +2655,11 @@ var allTerrainFormsBuilder = function(exports) { this.openSections = /* @__PURE__ */ new Map(); this.logicMapOn = "off" !== readSetting(LOGIC_MAP_SETTING); this.dirty = false; + this.editGeneration = 0; + this.saveInFlight = false; + this.queuedSave = null; this.teardowns = []; + this.canvasTarget = null; this.history = []; this.historyAt = -1; this.autosave = debounce(() => { @@ -2667,7 +2693,11 @@ var allTerrainFormsBuilder = function(exports) { const definition = this.config?.fieldTypes.find((candidate) => candidate.type === field.type); const supports = definition?.supports ?? []; const update = (key, value) => { - field[key] = value; + const live = this.liveField(field.id); + if (!live) { + return; + } + live[key] = value; this.markDirty(); this.renderCanvas(); }; @@ -2840,6 +2870,8 @@ var allTerrainFormsBuilder = function(exports) { } /** Releases every listener this instance registered. */ destroy() { + this.canvasTarget?.(); + this.canvasTarget = null; this.teardowns.forEach((teardown) => teardown()); this.teardowns = []; } @@ -3059,6 +3091,7 @@ var allTerrainFormsBuilder = function(exports) { this.history.shift(); } this.historyAt = this.history.length - 1; + this.syncHistoryButtons(); } /** Steps backwards or forwards through the history. */ travel(delta) { @@ -3071,19 +3104,37 @@ var allTerrainFormsBuilder = function(exports) { if (!this.schema.fields.some((field) => field.id === this.selected)) { this.selected = null; } - this.dirty = true; - this.autosave(); + this.markDirty(); this.renderBar(); this.renderCanvas(); this.renderInspector(); } /** An undo or redo button, disabled when there is nowhere to go. */ historyButton(iconSlug, label, delta) { - const target = this.historyAt + delta; const node = button(label, () => this.travel(delta), "secondary", iconSlug); - node.disabled = target < 0 || target >= this.history.length; + node.setAttribute("data-atfb-history", String(delta)); + node.disabled = !this.canTravel(delta); return node; } + /** Whether the history has anywhere to go in this direction. */ + canTravel(delta) { + const target = this.historyAt + delta; + return target >= 0 && target < this.history.length; + } + /** + * Refreshes Undo and Redo's disabled state in place. + * + * The state was computed only in `renderBar()`, which structural edits never + * call — so Undo sat disabled all session while Cmd+Z quietly worked. The + * buttons are updated whenever the history moves instead. + */ + syncHistoryButtons() { + for (const node of this.bar.querySelectorAll( + "[data-atfb-history]" + )) { + node.disabled = !this.canTravel(Number(node.dataset.atfbHistory)); + } + } /** * Downloads the current form as JSON. * @@ -3184,6 +3235,7 @@ var allTerrainFormsBuilder = function(exports) { /** Marks the form as having unsaved changes and schedules an autosave. */ markDirty() { this.dirty = true; + this.editGeneration += 1; const save = this.bar.querySelector("[data-atfb-save]"); if (save) { save.disabled = false; @@ -3195,24 +3247,32 @@ var allTerrainFormsBuilder = function(exports) { if (!this.form || !this.schema) { return; } + if (this.saveInFlight) { + this.queuedSave = { silent: silent && (this.queuedSave?.silent ?? true) }; + return; + } + this.saveInFlight = true; + const generation = this.editGeneration; try { const saved = await api.updateForm(this.form.id, { title: this.form.title, schema: this.schema }); - this.form = saved; - this.schema = saved.schema; - this.dirty = false; - this.rebindCanvas(); + if (generation === this.editGeneration) { + this.form = saved; + this.schema = saved.schema; + this.dirty = false; + this.rebindCanvas(); + const save = this.bar.querySelector("[data-atfb-save]"); + if (save) { + save.disabled = true; + } + } forgetMergeTags(saved.id); const summary = this.forms.find((candidate) => candidate.id === saved.id); if (summary) { summary.title = saved.title; } - const save = this.bar.querySelector("[data-atfb-save]"); - if (save) { - save.disabled = true; - } refreshPreview(saved.id, saved.title, saved.previewUrl); if (!silent) { notify("Form saved", saved.title); @@ -3223,6 +3283,13 @@ var allTerrainFormsBuilder = function(exports) { error instanceof Error ? error.message : "", "error" ); + } finally { + this.saveInFlight = false; + const queued = this.queuedSave; + this.queuedSave = null; + if (queued) { + void this.save(queued.silent); + } } } /* ---------------------------------------------------------------- Forms */ @@ -3267,7 +3334,15 @@ var allTerrainFormsBuilder = function(exports) { return; } const overlay = el("div", { class: "atfb-overlay" }); - const close = () => overlay.remove(); + const onKeydown = (event) => { + if (event.key === "Escape") { + close(); + } + }; + const close = () => { + overlay.remove(); + document.removeEventListener("keydown", onKeydown); + }; const grid = el("div", { class: "atfb-templates", children: this.config.templates.map( @@ -3296,6 +3371,9 @@ var allTerrainFormsBuilder = function(exports) { this.schema = created.schema; this.selected = null; this.dirty = false; + this.history = []; + this.historyAt = -1; + this.snapshot(); this.renderBar(); this.renderCanvas(); this.renderInspector(); @@ -3329,15 +3407,7 @@ var allTerrainFormsBuilder = function(exports) { close(); } }); - document.addEventListener( - "keydown", - (event) => { - if (event.key === "Escape") { - close(); - } - }, - { once: true } - ); + document.addEventListener("keydown", onKeydown); this.root.append(overlay); grid.querySelector("button")?.focus(); } @@ -3941,6 +4011,8 @@ var allTerrainFormsBuilder = function(exports) { * shared drag manager buys and an iframe could not. */ registerCanvasTarget(list) { + this.canvasTarget?.(); + this.canvasTarget = null; const marker = el("div", { class: "atfb-marker", attrs: { "aria-hidden": "true" } }); const teardown = getDragManager().registerDropTarget({ id: `atfb-canvas-${this.form?.id ?? 0}`, @@ -3970,15 +4042,19 @@ var allTerrainFormsBuilder = function(exports) { } } }); - this.teardowns.push(teardown); const onMove = (event) => { const detail = event.detail; if (detail?.payload?.type !== FIELD_PAYLOAD_TYPE || !list.classList.contains("is-dropping")) { return; } + const dragged = detail.payload.data?.fieldId ? this.canvas.querySelector( + `[data-atfb-card="${CSS.escape(detail.payload.data.fieldId)}"]` + ) : null; const y = detail.clientY ?? 0; - const index = insertionIndex(list, ".atfb-card", y); - const cards = list.querySelectorAll(".atfb-card"); + const index = insertionIndex(list, ".atfb-card", y, dragged ?? void 0); + const cards = Array.from(list.querySelectorAll(".atfb-card")).filter( + (card) => card !== dragged + ); if (index >= cards.length) { list.append(marker); } else { @@ -3986,7 +4062,10 @@ var allTerrainFormsBuilder = function(exports) { } }; document.addEventListener("os.drag.move", onMove); - this.teardowns.push(() => document.removeEventListener("os.drag.move", onMove)); + this.canvasTarget = () => { + teardown(); + document.removeEventListener("os.drag.move", onMove); + }; } /* -------------------------------------------------------- Field editing */ /** Adds a field of a type, at an index or at the end. */ @@ -4035,22 +4114,23 @@ var allTerrainFormsBuilder = function(exports) { this.canvas.querySelector(`[data-atfb-card="${CSS.escape(field.id)}"]`)?.focus(); }); } - /** Moves a field to an index. */ + /** + * Moves a field to an index. + * + * `index` counts the list *without* the moved field — see {@link fieldMove} + * for why every caller works in that space. + */ moveField(fieldId, index) { if (!this.schema) { return; } - const from = this.schema.fields.findIndex((field2) => field2.id === fieldId); - if (from < 0) { - return; - } - const clamped = Math.max(0, Math.min(index, this.schema.fields.length - 1)); - if (from === clamped) { + const move = fieldMove(this.schema.fields, fieldId, index); + if (!move) { return; } this.snapshot(); - const [field] = this.schema.fields.splice(from, 1); - this.schema.fields.splice(clamped > from ? clamped - 1 : clamped, 0, field); + const [field] = this.schema.fields.splice(move.from, 1); + this.schema.fields.splice(move.to, 0, field); this.markDirty(); this.renderCanvas(); } @@ -4108,6 +4188,22 @@ var allTerrainFormsBuilder = function(exports) { } return `f${index}`; } + /** + * An id for a new notification or confirmation, not already in use. + * + * Minted against the ids present, like `nextFieldId()`, rather than from + * the list's length: after a delete-then-add, `length + 1` re-issues an id + * the list still contains, and two entries sharing one id share one + * disclosure panel — opening either folds and unfolds both. + */ + nextEntryId(prefix, items) { + const used = new Set(items.map((item) => item.id)); + let index = items.length + 1; + while (used.has(`${prefix}${index}`)) { + index++; + } + return `${prefix}${index}`; + } /** * The settings a field type brings with it. * @@ -4275,6 +4371,7 @@ var allTerrainFormsBuilder = function(exports) { /** The choices editor, with drag-in image support. */ renderChoicesEditor(field, update) { const choices = field.choices ?? []; + const liveChoice = (index) => this.liveField(field.id)?.choices?.[index]; const list = el("div", { class: "atfb-choices" }); choices.forEach((choice, index) => { const rowEl = el("div", { @@ -4285,31 +4382,47 @@ var allTerrainFormsBuilder = function(exports) { // empty boxes for a setting that does nothing. field.type === "image_choice" ? this.choiceImageWell(choice, index, field) : null, bind(textInput(choice.label, (value) => { - const mirroring = !choice.value || choice.value === choice.label; - choice.label = value; + const live = liveChoice(index); + if (!live) { + return; + } + const mirroring = !live.value || live.value === live.label; + live.label = value; if (mirroring) { - choice.value = value; + live.value = value; } this.markDirty(); - this.syncCanvas(field); + const parent = this.liveField(field.id); + if (parent) { + this.syncCanvas(parent); + } }), `choice:${index}:label`), bind( textInput( choice.value, (value) => { - choice.value = value; - this.markDirty(); + const live = liveChoice(index); + if (live) { + live.value = value; + this.markDirty(); + } }, "value" ), `choice:${index}:value` ), field.type === "quiz" || choice.points !== void 0 ? numberInput(String(choice.points ?? ""), (value) => { - choice.points = value === "" ? void 0 : Number(value); - this.markDirty(); + const live = liveChoice(index); + if (live) { + live.points = value === "" ? void 0 : Number(value); + this.markDirty(); + } }) : numberInput(String(choice.price ?? ""), (value) => { - choice.price = value === "" ? void 0 : Number(value); - this.markDirty(); + const live = liveChoice(index); + if (live) { + live.price = value === "" ? void 0 : Number(value); + this.markDirty(); + } }), el("button", { class: "atfb-card__action", @@ -4317,8 +4430,12 @@ var allTerrainFormsBuilder = function(exports) { attrs: { "aria-label": `Remove ${choice.label}` }, on: { click: () => { - choices.splice(index, 1); - update("choices", choices); + const parent = this.liveField(field.id); + if (!parent) { + return; + } + (parent.choices ?? []).splice(index, 1); + update("choices", parent.choices); this.renderInspector(); } }, @@ -4340,8 +4457,13 @@ var allTerrainFormsBuilder = function(exports) { button( "Add choice", () => { - choices.push({ label: "", value: "" }); - update("choices", choices); + const parent = this.liveField(field.id); + if (!parent) { + return; + } + const next = parent.choices ?? []; + next.push({ label: "", value: "" }); + update("choices", next); this.renderInspector(); }, "ghost", @@ -4401,17 +4523,22 @@ var allTerrainFormsBuilder = function(exports) { notify("That is not an image this field can use", "", "error"); return; } - choice.image = id; + const live = this.liveField(field.id)?.choices?.[index]; + if (!live) { + return; + } + live.image = id; this.markDirty(); this.renderInspector(); } }); this.teardowns.push(teardown); well.addEventListener("click", () => { - if (!choice.image) { + const live = this.liveField(field.id)?.choices?.[index]; + if (!live?.image) { return; } - choice.image = void 0; + live.image = void 0; this.markDirty(); this.renderInspector(); }); @@ -4485,6 +4612,7 @@ var allTerrainFormsBuilder = function(exports) { /** The conditional-logic editor. */ renderLogicSection(field, update) { const logic = field.logic; + const liveLogic = () => this.liveField(field.id)?.logic; const others = (this.schema?.fields ?? []).filter( (candidate) => candidate.id !== field.id && candidate.type !== "page_break" ); @@ -4501,21 +4629,30 @@ var allTerrainFormsBuilder = function(exports) { label: candidate.label || candidate.id })), (value) => { - rule.field = value; - this.markDirty(); + const live = liveLogic()?.rules[index]; + if (live) { + live.field = value; + this.markDirty(); + } } ), select( rule.operator, Object.entries(this.config?.operators ?? {}).map(([value, label]) => ({ value, label })), (value) => { - rule.operator = value; - this.markDirty(); + const live = liveLogic()?.rules[index]; + if (live) { + live.operator = value; + this.markDirty(); + } } ), textInput(rule.value, (value) => { - rule.value = value; - this.markDirty(); + const live = liveLogic()?.rules[index]; + if (live) { + live.value = value; + this.markDirty(); + } }), el("button", { class: "atfb-card__action", @@ -4523,8 +4660,12 @@ var allTerrainFormsBuilder = function(exports) { attrs: { "aria-label": "Remove this rule" }, on: { click: () => { - logic.rules.splice(index, 1); - update("logic", logic); + const live = liveLogic(); + if (!live) { + return; + } + live.rules.splice(index, 1); + update("logic", live); this.renderInspector(); } }, @@ -4539,8 +4680,12 @@ var allTerrainFormsBuilder = function(exports) { "Conditional logic", [ checkbox("Only show this field sometimes", logic.enabled, (value) => { - logic.enabled = value; - update("logic", logic); + const live = liveLogic(); + if (!live) { + return; + } + live.enabled = value; + update("logic", live); this.renderInspector(); }), logic.enabled ? el("div", { @@ -4555,8 +4700,11 @@ var allTerrainFormsBuilder = function(exports) { { value: "hide", label: "Hide" } ], (value) => { - logic.action = value; - update("logic", logic); + const live = liveLogic(); + if (live) { + live.action = value; + update("logic", live); + } } ), el("span", { text: "this field when" }), @@ -4567,8 +4715,11 @@ var allTerrainFormsBuilder = function(exports) { { value: "any", label: "any" } ], (value) => { - logic.match = value; - update("logic", logic); + const live = liveLogic(); + if (live) { + live.match = value; + update("logic", live); + } } ), el("span", { text: "of these match:" }) @@ -4578,12 +4729,16 @@ var allTerrainFormsBuilder = function(exports) { button( "Add rule", () => { - logic.rules.push({ + const live = liveLogic(); + if (!live) { + return; + } + live.rules.push({ field: others[0]?.id ?? "", operator: "is", value: "" }); - update("logic", logic); + update("logic", live); this.renderInspector(); }, "ghost", @@ -4622,6 +4777,14 @@ var allTerrainFormsBuilder = function(exports) { } this.markDirty(); }, + // The studio clears the whole set on a theme switch, save or + // delete. Without this the schema keeps the old theme's tuning + // while the preview shows none of it, and the published form + // disagrees with what the Theme tab said it would look like. + onOverridesReplaced: (overrides) => { + this.schema.settings.themeOverrides = { ...overrides }; + this.markDirty(); + }, previewFor: (slug, overrides) => this.previewHtml(slug, overrides), onThemesChanged: (themes) => { this.themes = themes; @@ -5054,7 +5217,7 @@ var allTerrainFormsBuilder = function(exports) { "Add a notification", () => { notifications.push({ - id: `n${notifications.length + 1}`, + id: this.nextEntryId("n", notifications), enabled: true, name: "Notification", to: "{admin_email}", @@ -5227,7 +5390,7 @@ var allTerrainFormsBuilder = function(exports) { "Add a confirmation", () => { confirmations.push({ - id: `c${confirmations.length + 1}`, + id: this.nextEntryId("c", confirmations), enabled: true, name: "Confirmation", type: "message", @@ -5300,6 +5463,7 @@ var allTerrainFormsBuilder = function(exports) { exports.Builder = Builder; exports.SETTINGS_HANDLED_ELSEWHERE = SETTINGS_HANDLED_ELSEWHERE; exports.SETTING_CONTROLS = SETTING_CONTROLS; + exports.fieldMove = fieldMove; exports.mountBuilder = mountBuilder; exports.restatement = restatement; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); diff --git a/assets/js/builder.min.js b/assets/js/builder.min.js index 6eb926c..4a27c31 100644 --- a/assets/js/builder.min.js +++ b/assets/js/builder.min.js @@ -1,3 +1,3 @@ -var allTerrainFormsBuilder=function(U){"use strict";class ot{constructor(){this.targets=[],this.active=null,this.lastEndMs=0}start(e){if(this.active||e.origin.button!==0)return null;const{payload:t,origin:s}=e,i=s.clientX,a=s.clientY;let o=!1,l=!1,c=null,d=null,u=0,L=0;const w=()=>{document.removeEventListener("pointermove",E),document.removeEventListener("pointerup",B),document.removeEventListener("pointercancel",X),document.removeEventListener("keydown",ae),window.removeEventListener("blur",X),c?.remove(),c=null,t.source.classList.remove("atf-is-dragging"),d?.onLeave?.(m),d=null,this.active=null,this.lastEndMs=Date.now()},m={payload:t,isFinished:()=>l,cancel:(_="caller")=>{l||(l=!0,w(),e.onCancel?.(_))}},y=_=>{o=!0,t.source.classList.add("atf-is-dragging");const A=t.source.getBoundingClientRect();u=t.ghost?.offsetX??i-A.left,L=t.ghost?.offsetY??a-A.top,c=t.ghost?.element??t.source.cloneNode(!0),c.classList.add("atf-drag-ghost"),c.style.width=`${A.width}px`,document.body.appendChild(c),S(_)},S=_=>{c&&(c.style.transform=`translate3d(${_.clientX-u}px, ${_.clientY-L}px, 0)`)},E=_=>{if(l)return;if(!o){if(Math.hypot(_.clientX-i,_.clientY-a)<4)return;y(_)}S(_);const A=this.hitTest(_.clientX,_.clientY);A!==d&&(d?.onLeave?.(m),d=A,d?.onEnter?.(m))},B=_=>{if(l)return;if(!o){l=!0,w(),e.onClickOnly?.();return}const A=d;if(l=!0,w(),A&&A.accept(t)){e.onCommit?.(A),A.onDrop(m,{clientX:_.clientX,clientY:_.clientY});return}e.onCancel?.(A?"rejected":"no-target")},X=()=>m.cancel("pointercancel"),ae=_=>{_.key==="Escape"&&m.cancel("escape")};return document.addEventListener("pointermove",E),document.addEventListener("pointerup",B),document.addEventListener("pointercancel",X),document.addEventListener("keydown",ae),window.addEventListener("blur",X),this.active=m,m}registerDropTarget(e){return this.targets=this.targets.filter(t=>t.id!==e.id),this.targets.push(e),()=>{this.targets=this.targets.filter(t=>t.id!==e.id)}}isDragging(){return this.active!==null}recentlyEndedDrag(e=500){return Date.now()-this.lastEndMso.right||to.bottom)continue;const l=lt(a.element);if(l>i){s=a,i=l;continue}l===i&&s&&ct(a.element,s.element)&&(s=a)}return s}}function lt(n){let e=0,t=n;for(;t;)e++,t=t.parentElement;return e}function ct(n,e){return(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)!==0}function le(){return window.wp?.os??null}let ce=null;function Y(){const n=le();return n?.dragManager?n.dragManager:(ce||(ce=new ot),ce)}function dt(n){const e=i=>{const a=i.detail?.payload;return a&&n.includes(a.type)?a.source:null},t=i=>e(i)?.classList.add("atf-is-dragging"),s=i=>e(i)?.classList.remove("atf-is-dragging");return document.addEventListener("os.drag.start",t),document.addEventListener("os.drag.end",s),()=>{document.removeEventListener("os.drag.start",t),document.removeEventListener("os.drag.end",s)}}function Te(n,e,t,s,i){const a=e.getBoundingClientRect();return i&&(i.style.width=`${Math.round(a.width)}px`,i.style.maxWidth=`${Math.round(a.width)}px`,i.style.boxSizing="border-box"),{type:n,source:e,data:t,ghost:{element:i,offsetX:s.clientX-a.left,offsetY:s.clientY-a.top,hint:{neutral:"",accept:"",reject:"",hidden:!0}}}}function $e(n,e,t,s){const i=Array.from(n.querySelectorAll(e)).filter(a=>a!==s&&!a.classList.contains("atf-drag-ghost"));for(let a=0;ai.fetch(l,c,{source:"allterrain-forms"}):(l,c)=>fetch(l,c))(t,{credentials:"same-origin",...e,headers:s});if(!o.ok){let l=`Request failed with status ${o.status}.`,c="";try{const d=await o.json();l=d.message??l,c=d.code??""}catch{}throw new ee(l,o.status,c)}if(o.status!==204)return await o.json()}const N=n=>de(n);async function ht(n){if(!O?.wpRestUrl)throw new ee("AllTerrain Forms is not configured on this page.",0);const e=O.nonce?{"X-WP-Nonce":O.nonce}:{},t=le(),s=`${O.wpRestUrl}${n}`,i=t?.fetch?await t.fetch(s,{credentials:"same-origin",headers:e},{source:"allterrain-forms"}):await fetch(s,{credentials:"same-origin",headers:e});if(!i.ok)throw new ee(`Request failed with status ${i.status}.`,i.status);return await i.json()}function ut(n){if(!n||!n.includes("&"))return n;const e=document.createElement("textarea");return e.innerHTML=n,e.value}const W=(n,e)=>de(n,{method:"POST",body:JSON.stringify(e)}),te=n=>de(n,{method:"DELETE"});function Ae(n){const e=new URLSearchParams;for(const[s,i]of Object.entries(n))i===void 0||i===""||i===!1||e.set(s,String(i));const t=e.toString();return t?`?${t}`:""}const C={config:()=>N("/config"),listForms:()=>N("/forms"),getForm:n=>N(`/forms/${n}`),createForm:n=>W("/forms",n),updateForm:(n,e)=>W(`/forms/${n}`,e),duplicateForm:n=>W(`/forms/${n}/duplicate`,{}),deleteForm:n=>te(`/forms/${n}`),preview:(n,e)=>W(`/forms/${n}/preview`,e),pages:()=>ht("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(n=>n.map(e=>({id:e.id,title:ut(e.title?.rendered??"")||`#${e.id}`}))),mergeTags:n=>N(`/forms/${n}/merge-tags`).then(e=>e.groups),analytics:(n,e="")=>N(`/forms/${n}/analytics${e?`?dimension=${encodeURIComponent(e)}`:""}`),demoStatus:()=>N("/demo"),demoSeed:n=>W("/demo",n?{count:n}:{}),demoRemove:()=>te("/demo"),listEntries:n=>N(`/entries${Ae(n)}`),getEntry:n=>N(`/entries/${n}`),updateEntry:(n,e)=>W(`/entries/${n}`,e),deleteEntry:n=>te(`/entries/${n}`),exportEntries:n=>N(`/entries/export${Ae(n)}`),listThemes:()=>N("/themes"),saveTheme:n=>W("/themes",n),deleteTheme:n=>te(`/themes/${n}`)};function r(n,e={}){const t=document.createElement(n);e.class&&(t.className=e.class),e.text!==void 0&&(t.textContent=e.text),e.html!==void 0&&(t.innerHTML=e.html),e.title&&(t.title=e.title),e.type&&"type"in t&&(t.type=e.type),e.value!==void 0&&"value"in t&&(t.value=e.value),e.placeholder!==void 0&&"placeholder"in t&&(t.placeholder=e.placeholder),e.href&&"href"in t&&(t.href=e.href);for(const[s,i]of Object.entries(e.attrs??{}))i===void 0||i===!1||t.setAttribute(s,i===!0?"":String(i));Object.assign(t.style,e.style??{});for(const[s,i]of Object.entries(e.on??{}))t.addEventListener(s,i);for(const s of e.children??[])s==null||s===!1||t.append(s);return t}function D(n){n.replaceChildren()}function F(n){return r("span",{class:`dashicons ${n.startsWith("dashicons-")?n:`dashicons-${n}`}`,attrs:{"aria-hidden":"true"}})}function p(n,e,t){const s=e.id||`atf-c-${Math.random().toString(36).slice(2,9)}`;return e.id=s,r("div",{class:"atfb-row",children:[r("label",{class:"atfb-row__label",text:n,attrs:{for:s}}),e,t?r("p",{class:"atfb-row__hint",text:t}):null]})}function v(n,e,t=""){return r("input",{class:"atfb-input",type:"text",value:n,placeholder:t,on:{input:s=>e(s.target.value)}})}function P(n,e){if(J("os-number-field")){const t=document.createElement("os-number-field");return t.setAttribute("value",n),t.classList.add("atfb-field"),t.addEventListener("os-input-change",s=>{e(String(s.detail?.value??""))}),t}return r("input",{class:"atfb-input",type:"number",value:n,on:{input:t=>e(t.target.value)}})}function se(n,e,t=4){const s=r("textarea",{class:"atfb-input atfb-input--area",attrs:{rows:t},on:{input:i=>e(i.target.value)}});return s.value=n,s}function $(n,e,t){if(J("os-select")&&J("os-option")){const s=document.createElement("os-select");s.setAttribute("value",n),s.classList.add("atfb-field");for(const i of e){const a=document.createElement("os-option");a.setAttribute("value",i.value),a.textContent=i.label,s.append(a)}return s.addEventListener("os-pick",i=>{t(String(i.detail?.value??""))}),s}return r("select",{class:"atfb-input atfb-select",on:{change:s=>t(s.target.value)},children:e.map(s=>r("option",{value:s.value,text:s.label,attrs:{selected:s.value===n}}))})}function T(n,e,t){if(J("os-checkbox-label")){const i=document.createElement("os-checkbox-label");return i.setAttribute("label",n),i.classList.add("atfb-check"),e&&i.setAttribute("checked",""),i.addEventListener("os-checkbox-change",a=>{t(!!a.detail?.checked)}),i}const s=r("input",{type:"checkbox",on:{change:i=>t(i.target.checked)}});return s.checked=e,r("label",{class:"atfb-check",children:[s,r("span",{text:n})]})}function k(n,e,t="secondary",s){const i=[s?F(s):null,r("span",{text:n})];if(J("os-button")){const a=document.createElement("os-button");a.setAttribute("variant",t),a.setAttribute("type","button"),a.classList.add("atfb-button",`atfb-button--${t}`),a.addEventListener("click",e);for(const o of i)o&&a.append(o);return a}return r("button",{class:`atfb-button atfb-button--${t}`,type:"button",on:{click:e},children:i})}function J(n){return typeof customElements<"u"&&!!customElements.get(n)}const pt=["os-button","os-checkbox-label","os-number-field","os-select","os-option","os-segmented","os-segment","os-color-field","os-empty-state"];let ne=null;function ft(){if(ne)return ne;const n=window.wp?.os;return ne=n?.loadComponents?n.loadComponents(pt).catch(()=>{}):Promise.resolve(),ne}async function he(n,e=""){const t=window.wp?.os;return t?.confirm?t.confirm({title:e,message:n,danger:!0}):window.confirm(n)}function q(n,e="",t="info"){const s=window.wp?.os;if(s?.notify){s.notify({title:n,body:e,type:t});return}console.info(`[AllTerrain Forms] ${n}${e?`: ${e}`:""}`)}function mt(n){let e=0,t;return(...s)=>{t=s,!e&&(e=window.requestAnimationFrame(()=>{e=0,n(...t)}))}}function De(n,e){let t=0;return(...s)=>{window.clearTimeout(t),t=window.setTimeout(()=>n(...s),e)}}function bt(n){try{return window.localStorage.getItem(n)??""}catch{return""}}function gt(n,e){try{window.localStorage.setItem(n,e)}catch{}}function Me(){const n=[window];try{window.parent&&window.parent!==window&&n.push(window.parent)}catch{}return n}function vt(n){const e=n.wp?.os?.windowManager?.getAll?.()??[];for(const t of e){const s=t.iframe??t.element?.querySelector?.("iframe");if(s&&s.contentWindow===window)return t}return null}const wt=4e3,yt=100;function xt(n,e){const t=Date.now()+wt,s=()=>{const i=vt(n);if(i&&i.id!==e){typeof i.close=="function"?i.close():n.wp?.os?.windowManager?.remove?.(i.id);return}!i&&Date.now()0?{form:t}:void 0;t>0&&Et(t);for(const i of Me())if(i.wp?.os?.openWindow?.(e,{source:"handoff",params:s})){window.setTimeout(()=>xt(i,e),0);return}}function kt(){document.addEventListener("click",n=>{const e=n.target?.closest?.("[data-atf-open-window]");if(!e)return;n.preventDefault();const t=e.getAttribute("data-atf-open-window")??"";for(const s of Me())if(s.wp?.os?.openWindow?.(t,{source:"handoff"}))return})}const St="allterrain-forms/requested-form";function Et(n){try{window.sessionStorage.setItem(St,String(n))}catch{}}const Oe={is:"is",is_not:"is not",contains:"contains",not_contains:"does not contain",starts_with:"starts with",ends_with:"ends with",greater:"is more than",less:"is less than",greater_equal:"is at least",less_equal:"is at most",empty:"is empty",not_empty:"has any answer"},Fe=["empty","not_empty"];function Ie(n,e){const t=n.find(s=>s.id===e);return t?t.label||"an untitled question":null}function qe(n,e,t){return n.find(a=>a.id===e)?.choices?.find(a=>a.value===t)?.label||t}function Lt(n,e){const t=Oe[n.operator]??String(n.operator);if(Fe.includes(n.operator))return t;const s=qe(e,n.field,n.value);return`${n.operator==="is"?"":`${t} `}${s!==""?s:"(nothing)"}`}function Ne(n,e){const t=Ie(e,n.field),s=Oe[n.operator]??String(n.operator);if(t===null)return[{kind:"field",text:"a question that no longer exists",fieldId:n.field,missing:!0}];const i=[{kind:"field",text:t,fieldId:n.field,missing:!1},{kind:"operator",text:s}];if(Fe.includes(n.operator))return i;const a=qe(e,n.field,n.value);return i.push({kind:"value",text:a!==""?a:"(nothing)"}),i}function Ct(n,e){const t=n.logic;if(!t?.enabled||!t.rules.length)return[];const s=[{kind:"verb",text:t.action==="hide"?"Hidden when":"Shown when"}];return t.rules.forEach((i,a)=>{a>0&&s.push({kind:"join",text:t.match==="all"?"and":"or"}),s.push(...Ne(i,e))}),s}function Pe(n){return n.map(e=>e.text).join(" ")}function Tt(n,e){return Pe(Ne(n,e))}function ue(n){const e=[];for(const t of n){const s=t.logic;if(s?.enabled)for(const i of s.rules)!i.field||i.field===t.id||e.push({from:i.field,to:t.id,label:Tt(i,n),short:Lt(i,n),action:s.action,broken:Ie(n,i.field)===null})}return e}function $t(n){const e=new Map,t=new Set;for(const s of ue(n)){const i=`${s.from}->${s.to}`;s.broken||t.has(i)||(t.add(i),e.set(s.from,(e.get(s.from)??0)+1))}return e}const Re=10,Be=22,z="http://www.w3.org/2000/svg";class At{constructor(e){this.edges=[],this.teardowns=[],this.frame=0,this.host=e,this.svg=document.createElementNS(z,"svg"),this.svg.setAttribute("class","atfb-logicmap"),this.svg.setAttribute("aria-hidden","true"),this.svg.setAttribute("focusable","false"),e.append(this.svg);const t=()=>this.schedule();window.addEventListener("resize",t),this.teardowns.push(()=>window.removeEventListener("resize",t));const s=e.closest(".atfb__canvas")??e;if(s.addEventListener("scroll",t,{passive:!0}),this.teardowns.push(()=>s.removeEventListener("scroll",t)),typeof ResizeObserver<"u"){const i=new ResizeObserver(t);i.observe(e),this.teardowns.push(()=>i.disconnect())}}setEdges(e){this.edges=e,this.schedule()}highlight(e){this.svg.classList.toggle("is-focused",!!e),this.svg.querySelectorAll("[data-from]").forEach(t=>{const s=e&&(t.dataset.from===e||t.dataset.to===e);t.classList.toggle("is-lit",!!s)})}destroy(){this.teardowns.forEach(e=>e()),this.teardowns=[],this.svg.remove(),this.frame&&cancelAnimationFrame(this.frame)}schedule(){this.frame||(this.frame=requestAnimationFrame(()=>{this.frame=0,this.draw()}))}draw(){this.svg.replaceChildren();const e=this.host.getBoundingClientRect();this.svg.setAttribute("viewBox",`0 0 ${e.width} ${e.height}`),this.svg.setAttribute("width",String(e.width)),this.svg.setAttribute("height",String(e.height));const t=new Map;for(const s of this.edges){const i=this.cardRect(s.from),a=this.cardRect(s.to);if(!i||!a)continue;const o=t.get(s.from)??0;t.set(s.from,o+1),this.drawEdge(s,i,a,e,o)}}cardRect(e){const t=this.host.querySelector(`[data-atfb-card="${CSS.escape(e)}"]`);return t?t.getBoundingClientRect():null}drawEdge(e,t,s,i,a){const o=t.right-i.left,l=t.top+t.height/2-i.top,c=s.right-i.left,d=s.top+s.height/2-i.top,u=Math.max(0,i.width-Re-Math.max(o,c)),L=22+a*11+Math.min(22,Math.abs(d-l)/8),w=Math.max(12,Math.min(L,u*.4)),m=document.createElementNS(z,"path");m.setAttribute("d",`M ${o} ${l} C ${o+w} ${l}, ${c+w} ${d}, ${c} ${d}`),m.setAttribute("class",`atfb-logicmap__path is-${e.action}${e.broken?" is-broken":""}`),m.dataset.from=e.from,m.dataset.to=e.to;const y=document.createElementNS(z,"title");y.textContent=e.label,m.append(y);const S=document.createElementNS(z,"circle");S.setAttribute("cx",String(c)),S.setAttribute("cy",String(d)),S.setAttribute("r","3.5"),S.setAttribute("class",`atfb-logicmap__dot is-${e.action}${e.broken?" is-broken":""}`),S.dataset.from=e.from,S.dataset.to=e.to,this.svg.append(m,S);const E=document.createElementNS(z,"text");E.setAttribute("x",String(i.width-Re)),E.setAttribute("y",String(d+3)),E.setAttribute("text-anchor","end"),E.setAttribute("class","atfb-logicmap__label"),E.dataset.from=e.from,E.dataset.to=e.to,E.textContent=e.short.length>Be?`${e.short.slice(0,Be-1)}…`:e.short;const B=document.createElementNS(z,"title");B.textContent=e.label,E.append(B),this.svg.append(E)}}const Dt={text:"text",email:"text",url:"text",tel:"text",number:"text",password:"text",date:"text",time:"text",datetime:"text",date_range:"composite",textarea:"textarea",select:"select",country:"select",multiselect:"options",radio:"options",checkboxes:"options",image_choice:"options",quiz:"options",switch:"toggle",consent:"toggle",file:"file",range:"range",rating:"summary",scale:"summary",likert:"summary",signature:"summary",repeater:"summary",color:"text",name:"composite",address:"composite",total:"text",hidden:"static",heading:"static",html:"static",divider:"static",spacer:"static",page_break:"static"};function Mt(n){return Dt[n]??"text"}function He(n,e){const t=/^choice:(\d+):(label|value)$/.exec(e);return String(t?n.choices?.[Number(t[1])]?.[t[2]]??"":n[e]??"")}function H(n){const{value:e,onInput:t,onCommit:s}=n,i=r("span",{class:`${n.class} atfb-editable`,text:e,attrs:{contenteditable:"plaintext-only",role:"textbox",spellcheck:"false","data-placeholder":n.placeholder}});return n.bind&&(i.dataset.atfbBind=n.bind),i.addEventListener("pointerdown",a=>a.stopPropagation()),i.addEventListener("input",()=>t(i.textContent??"")),i.addEventListener("keydown",a=>{a.key==="Enter"&&(a.preventDefault(),i.blur()),a.key==="Escape"&&(i.textContent=e,i.blur()),a.stopPropagation()}),i.addEventListener("blur",()=>s?.()),i}function Ot(n){const e=r("input",{class:"atf-choice__input",type:n==="checkboxes"||n==="multiselect"?"checkbox":"radio"});return e.disabled=!0,e}function Ft(n,e,t){const s=Mt(n.type),i=H({value:n.label,placeholder:"Write the question…",class:"atf-label",bind:"label",onInput:o=>t.edit(l=>{l.label=o}),onCommit:()=>t.restructure(()=>{})}),a=[s==="static"||s==="toggle"?null:i,qt(n,e,s,t),It(n,e,t)];return r("div",{class:"atfb-preview atf-form",children:[r("div",{class:`atf-field atf-field--${n.type}`,children:a})]})}function It(n,e,t){if(!e?.supports.includes("hint"))return null;const s=H({value:n.hint??"",placeholder:"Add a hint…",class:"atf-hint",bind:"hint",onInput:i=>t.edit(a=>{a.hint=i})});return s.setAttribute("aria-label","Hint"),r("p",{class:"atfb-preview__hint",children:[s]})}function qt(n,e,t,s){switch(t){case"text":return pe(n,"atf-input",s);case"textarea":return pe(n,"atf-input atf-textarea",s,!0);case"select":return r("div",{class:"atfb-preview__select",children:[pe(n,"atf-input atf-select",s)]});case"options":return Pt(n,s);case"toggle":return r("div",{class:n.type==="switch"?"atf-toggle atf-toggle--switch":"atf-toggle",children:[(()=>{const i=r("input",{class:"atf-toggle__input",type:"checkbox"});return i.disabled=!0,i})(),H({value:n.label||"",placeholder:n.type==="consent"?"What are they agreeing to?…":"What does this turn on?…",class:"atf-toggle__label",bind:"label",onInput:i=>s.edit(a=>{a.label=i}),onCommit:()=>s.restructure(()=>{})})]});case"file":return r("div",{class:"atf-file",children:[r("input",{class:"atf-file__input",type:"file"})]});case"range":return r("input",{class:"atf-range__input",type:"range"});case"composite":return r("div",{class:"atf-composite__parts",children:[r("span",{class:"atf-input atfb-preview__ghost",text:""}),r("span",{class:"atf-input atfb-preview__ghost",text:""})]});case"static":return Rt(n,e,s);default:return r("div",{class:"atfb-preview__stack",children:[r("p",{class:"atfb-preview__summary",text:`${e?.label??n.type} — set this up in the panel on the right.`}),n.type==="repeater"?Nt(n,s):null]})}}function pe(n,e,t,s=!1){const i=H({value:n.placeholder??"",placeholder:n.type==="select"||n.type==="country"?"Choose…":"Placeholder…",class:`${e} atfb-preview__box${s?" atfb-preview__box--tall":""}`,bind:"placeholder",onInput:a=>t.edit(o=>{o.placeholder=a})});return i.setAttribute("aria-label","Placeholder"),i}function fe(n,e,t,s,i,a){return r("span",{class:`atf-button ${t} atfb-preview__button`,children:[H({value:n,placeholder:e,class:"atfb-preview__button-text",bind:s,onInput:o=>i.edit(l=>a(l,o))})]})}function Nt(n,e){return fe(String(n.addLabel??""),"Add another","atf-button--secondary","addLabel",e,(t,s)=>{t.addLabel=s})}function Pt(n,e){const t=r("div",{class:"atf-choices__list"});return(n.choices??[]).forEach((s,i)=>{t.append(r("div",{class:"atf-choice atfb-preview__option",children:[Ot(n.type),H({value:s.label,placeholder:"Option…",class:"atf-choice__label",bind:`choice:${i}:label`,onInput:a=>{e.edit(o=>{const l=o.choices?.[i];if(!l)return;const c=!l.value||l.value===l.label;l.label=a,c&&(l.value=a)})}}),r("button",{class:"atfb-preview__remove",type:"button",title:"Remove this option",attrs:{"aria-label":`Remove ${s.label||"this option"}`},on:{pointerdown:a=>a.stopPropagation(),click:a=>{a.stopPropagation(),e.restructure(o=>{o.choices.splice(i,1)})}},children:[r("span",{text:"×"})]})]}))}),t.append(r("button",{class:"atfb-preview__add",type:"button",on:{pointerdown:s=>s.stopPropagation(),click:s=>{s.stopPropagation(),e.restructure(i=>{const a=i.choices.length+1;i.choices.push({label:`Option ${a}`,value:`Option ${a}`})})}},children:[r("span",{text:"+ Add option"})]})),r("fieldset",{class:"atf-choices",children:[t]})}function Rt(n,e,t){return n.type==="heading"?H({value:n.label,placeholder:"Section heading…",class:"atf-heading",bind:"label",onInput:s=>t.edit(i=>{i.label=s}),onCommit:()=>t.restructure(()=>{})}):n.type==="divider"?r("hr",{class:"atf-divider"}):n.type==="page_break"?r("div",{class:"atfb-preview__stack",children:[r("p",{class:"atfb-progress-name",children:[H({value:n.label,placeholder:"Name this step…",class:"atf-progress__label",bind:"label",onInput:s=>t.edit(i=>{i.label=s}),onCommit:()=>t.restructure(()=>{})})]}),r("p",{class:"atfb-preview__summary",text:"Everything after this is a new page."}),r("div",{class:"atf-nav atfb-preview__nav",children:[fe(String(n.prevLabel??""),"Back","atf-button--secondary","prevLabel",t,(s,i)=>{s.prevLabel=i}),fe(String(n.nextLabel??""),"Next","","nextLabel",t,(s,i)=>{s.nextLabel=i})]})]}):r("p",{class:"atfb-preview__summary",text:`${e?.label??n.type} — nothing is shown to the visitor here.`})}const me=new Map;function be(n){let e=me.get(n);return e||(e=C.mergeTags(n).catch(()=>[]),me.set(n,e)),e}function Bt(n){me.delete(n)}function Ht(n){const e=new Map;for(const t of n)for(const s of t.items)e.set(s.tag,s);return e}function jt(n,e){const t=Ht(e);return n.replace(/\{[a-z_]+(?::[^}]*)?\}/gi,s=>{const i=t.get(s.toLowerCase());return i?i.sample:s})}function Ut(n){return/\{[a-z_]+(?::[^}]*)?\}/i.test(n)}let j=null;function K(){j?.remove(),j=null}typeof document<"u"&&(document.addEventListener("pointerdown",n=>{j&&!j.contains(n.target)&&K()}),document.addEventListener("keydown",n=>{n.key==="Escape"&&j&&(K(),n.stopPropagation())}));function Wt(n,e){const t=n.selectionStart??n.value.length,s=n.selectionEnd??n.value.length;n.value=n.value.slice(0,t)+e+n.value.slice(s);const i=t+e.length;n.setSelectionRange(i,i),n.focus(),n.dispatchEvent(new Event("input",{bubbles:!0}))}function Yt(n){let e=n.parentElement;for(;e&&e!==document.body;){const t=getComputedStyle(e).overflowY;if(t==="auto"||t==="scroll"||t==="hidden")return e.getBoundingClientRect().bottom;e=e.parentElement}return window.innerHeight}function zt(n,e){const t=r("input",{class:"atfb-input atfb-tagpick__search",type:"search",placeholder:"Search values…",attrs:{"aria-label":"Search values"}}),s=r("div",{class:"atfb-tagpick__list"}),i=a=>{s.replaceChildren();const o=a.trim().toLowerCase();let l=0;for(const c of n){const d=c.items.filter(u=>!o||u.label.toLowerCase().includes(o)||u.tag.toLowerCase().includes(o));if(!d.length){c.empty&&!o&&!c.items.length&&s.append(r("p",{class:"atfb-tagpick__group",text:c.label}),r("p",{class:"atfb-tagpick__empty",text:c.empty}));continue}s.append(r("p",{class:"atfb-tagpick__group",text:c.label}));for(const u of d)l+=1,s.append(r("button",{class:"atfb-tagpick__item",type:"button",on:{click:()=>{e(u.tag),K()}},children:[r("span",{class:"atfb-tagpick__item-main",children:[r("span",{class:"atfb-tagpick__label",text:u.label}),r("code",{class:"atfb-tagpick__tag",text:u.tag})]}),u.hint||u.sample?r("span",{class:"atfb-tagpick__meta",text:u.sample?`e.g. ${u.sample}`:u.hint}):null]}))}!l&&o&&s.append(r("p",{class:"atfb-tagpick__empty",text:`Nothing matches “${a}”.`}))};return i(""),t.addEventListener("input",()=>i(t.value)),r("div",{class:"atfb-tagpick",attrs:{role:"dialog","aria-label":"Insert a value"},children:[r("p",{class:"atfb-tagpick__intro",text:"Pick something to drop in. It is filled in when the form is submitted."}),t,s]})}function je(n,e){const t=r("button",{class:"atfb-button atfb-button--ghost atfb-tagpick__open",type:"button",title:"Insert a value from the submission",children:[F("shortcode"),r("span",{text:"Insert a value"})]}),s=r("div",{class:"atfb-taggable",children:[n,r("div",{class:"atfb-taggable__tools",children:[t]})]}),i=e.preview===!1?null:r("p",{class:"atfb-taggable__preview"});i&&s.append(i);const a=()=>{if(i){if(!Ut(n.value)){i.textContent="",i.hidden=!0;return}be(e.formId).then(o=>{i.hidden=!1,i.replaceChildren(r("span",{class:"atfb-taggable__preview-label",text:"Reads as"}),r("span",{text:jt(n.value,o)}))})}};return n.addEventListener("input",a),a(),t.addEventListener("click",o=>{if(o.stopPropagation(),j&&s.contains(j)){K();return}K(),be(e.formId).then(l=>{const c=zt(l,d=>{Wt(n,d),a()});s.append(c),j=c,(window.innerHeight-c.getBoundingClientRect().topYt(s))&&c.classList.add("atfb-tagpick--above"),c.querySelector(".atfb-tagpick__search")?.focus()})}),s}function Ue(n,e){const t=parseFloat(String(n??""));return Number.isFinite(t)?t:e}function We(n,e){let t=e[0];for(const s of e)Math.abs(s.at-n)n.accent??"#2271b1",apply:n=>({accent:n,"accent-soft":`color-mix( in srgb, ${n} 12%, transparent )`,"border-focus":n,"focus-ring-color":n,"button-bg":n,"button-bg-hover":`color-mix( in srgb, ${n} 88%, #000 )`})},{id:"roundness",label:"Roundness",hint:"Corners, everywhere at once.",kind:"scale",steps:ge.map(({value:n,label:e})=>({value:n,label:e})),owns:["radius-field","radius-button","radius-card","radius-check"],read:n=>We(Ue(n["radius-field"],4),ge),apply:n=>{const e=ge.find(t=>t.value===n)?.at??4;return{"radius-field":`${e}px`,"radius-button":`${e}px`,"radius-card":`${Math.min(e*2,28)}px`,"radius-check":`${Math.min(e,6)}px`}}},{id:"density",label:"Density",hint:"How much air the form has.",kind:"scale",steps:ve.map(({value:n,label:e})=>({value:n,label:e})),owns:["pad-field-x","pad-field-y","gap-fields","gap-label","button-pad-x","button-pad-y"],read:n=>We(Ue(n["pad-field-y"],9),ve),apply:n=>{const e=ve.find(t=>t.value===n)?.at??9;return{"pad-field-y":`${e}px`,"pad-field-x":`${Math.round(e*1.4)}px`,"gap-fields":`${Math.round(e*2.2)}px`,"gap-label":`${Math.max(4,Math.round(e*.7))}px`,"button-pad-y":`${e+2}px`,"button-pad-x":`${Math.round(e*2.2)}px`}}},{id:"shadow",label:"Depth",hint:"How far the form sits off the page.",kind:"scale",steps:Xt,owns:["shadow-field","shadow-field-focus","shadow-button","shadow-button-hover","shadow-card"],read:n=>{const e=n["shadow-card"]??"none";return e==="none"||e.trim()===""?n["shadow-field"]&&n["shadow-field"]!=="none"?"hard":"none":e.includes("0 1px")||e.includes("2px")?"subtle":"lifted"},apply:n=>{switch(n){case"subtle":return{"shadow-field":"none","shadow-field-focus":"none","shadow-button":"0 1px 2px rgba( 0, 0, 0, 0.12 )","shadow-button-hover":"0 2px 4px rgba( 0, 0, 0, 0.16 )","shadow-card":"0 1px 3px rgba( 0, 0, 0, 0.1 )"};case"lifted":return{"shadow-field":"none","shadow-field-focus":"0 4px 12px rgba( 0, 0, 0, 0.12 )","shadow-button":"0 4px 10px rgba( 0, 0, 0, 0.16 )","shadow-button-hover":"0 8px 20px rgba( 0, 0, 0, 0.2 )","shadow-card":"0 10px 30px rgba( 0, 0, 0, 0.14 )"};case"hard":return{"shadow-field":"3px 3px 0 currentColor","shadow-field-focus":"5px 5px 0 currentColor","shadow-button":"4px 4px 0 currentColor","shadow-button-hover":"2px 2px 0 currentColor","shadow-card":"none"};default:return{"shadow-field":"none","shadow-field-focus":"none","shadow-button":"none","shadow-button-hover":"none","shadow-card":"none"}}}},{id:"field-style",label:"Fields",hint:"How an input is drawn.",kind:"choice",steps:Gt,owns:["field-style"],read:n=>n["field-style"]??"outline",apply:n=>({"field-style":n})},{id:"labels",label:"Labels",hint:"Where the question sits relative to the answer.",kind:"choice",steps:Jt,owns:["label-position"],read:n=>n["label-position"]??"top",apply:n=>({"label-position":n})}]}function Kt(n){return Ye().find(e=>e.owns.includes(n))??null}function ze(n){let e=n.activeSlug,t={...n.overrides},s=n.themes;const i=r("div",{class:"atfs-preview__frame"}),a=r("div",{class:"atfs-controls__body"}),o=r("div",{class:"atfs-quick"}),l=r("div",{class:"atfs-themes"}),c=De(async()=>{try{const h=await n.previewFor(e,t);i.innerHTML=h,document.dispatchEvent(new CustomEvent("atf-refresh"))}catch(h){D(i),i.append(r("p",{class:"atfb-error",text:h instanceof Error?h.message:"Preview failed."}))}},180),d=h=>t[h.token]!==void 0?t[h.token]:s.find(b=>b.slug===e)?.resolved?.[h.token]??h.default,u=()=>{D(l);for(const h of s){const f=r("button",{class:`atfs-theme${h.slug===e?" is-active":""}`,type:"button",attrs:{"aria-pressed":h.slug===e,title:h.description},children:[r("span",{class:"atfs-theme__chip",style:{background:h.resolved.surface??"#fff",borderColor:h.resolved.border??"#ccc",borderRadius:h.resolved["radius-field"]??"4px",boxShadow:h.resolved["shadow-card"]??"none"},children:[r("span",{class:"atfs-theme__accent",style:{background:h.resolved.accent??"#2271b1",borderRadius:h.resolved["radius-button"]??"4px"}})]}),r("span",{class:"atfs-theme__name",text:h.label}),h.custom?r("span",{class:"atfb-badge",text:"yours"}):null],on:{click:()=>{e=h.slug,t={},n.onTheme(e),u(),m(),y(),c(),A()}}});l.append(f)}},L=()=>{const f={...s.find(b=>b.slug===e)?.resolved??{}};for(const[b,g]of Object.entries(t))f[b]=g;return f},w=(h,f)=>{const b=h.apply(f,L());for(const[g,x]of Object.entries(b))t[g]=x,n.onOverride(g,x);m(),y(),c()},m=()=>{D(o);const h=L();for(const f of Ye()){const b=f.read(h);let g;if(f.kind==="colour"){const x=r("input",{class:"atfs-color",type:"color",value:ye(b),attrs:{"aria-label":f.label},on:{input:I=>w(f,I.target.value)}});g=r("div",{class:"atfs-color-row",children:[x,v(b,I=>w(f,I))]})}else g=r("div",{class:`atfs-segmented atfs-segmented--${f.kind}`,attrs:{role:"group","aria-label":f.label},children:(f.steps??[]).map(x=>r("button",{class:`atfs-segment${x.value===b?" is-on":""}`,type:"button",text:x.label,attrs:{"aria-pressed":x.value===b},on:{click:()=>w(f,x.value)}}))});o.append(r("div",{class:"atfs-dial",children:[r("div",{class:"atfs-dial__head",children:[r("span",{class:"atfs-dial__label",text:f.label}),r("span",{class:"atfs-dial__hint",text:f.hint})]}),g]}))}},y=()=>{D(a);const h=new Map;for(const b of n.tokens){const g=h.get(b.group)??[];g.push(b),h.set(b.group,g)}const f={colour:"Colour",shape:"Corners and borders",shadow:"Shadows",fields:"Field style",space:"Spacing",type:"Type",labels:"Labels",button:"Buttons",focus:"Focus ring",motion:"Motion"};for(const[b,g]of h)a.append(r("details",{class:"atfs-group",attrs:{open:b==="colour"},children:[r("summary",{text:f[b]??b}),...g.map(x=>S(x))]}))},S=h=>{const f=d(h),b=M=>{M===""?delete t[h.token]:t[h.token]=M,n.onOverride(h.token,M),c()};let g;if(h.control==="color"){const M=r("input",{class:"atfs-color",type:"color",value:ye(f),attrs:{"aria-label":`${h.label} colour`},on:{input:G=>{const Z=G.target.value;re.value=Z,b(Z)}}}),re=v(f,G=>{M.value=ye(G),b(G)});g=r("div",{class:"atfs-color-row",children:[M,re]})}else if(h.control==="select")g=$(f,(h.options??[]).map(M=>({value:M,label:M})),b);else if(h.control==="length"){const M=parseFloat(f)||0,re=h.unit??"px",G=h.token.includes("radius")?60:80,Z=r("input",{class:"atfs-range",type:"range",value:String(M),attrs:{min:"0",max:String(G),step:"1","aria-label":h.label},on:{input:oe=>{const rt=`${oe.target.value}${re}`;at.value=rt,b(rt)}}}),at=v(f,oe=>{Z.value=String(parseFloat(oe)||0),b(oe)});g=r("div",{class:"atfs-length-row",children:[Z,at]})}else g=v(f,b);const x=p(h.label,g),I=Kt(h.token);return I&&(x.classList.add("is-dialled"),x.append(r("span",{class:"atfs-owned",text:I.label}))),t[h.token]!==void 0&&(x.classList.add("is-overridden"),x.append(r("button",{class:"atfs-reset",type:"button",text:"Reset",attrs:{"aria-label":`Reset ${h.label} to the theme's value`},on:{click:()=>{b(""),y()}}}))),x},E=async()=>{const h=s.find(g=>g.slug===e),f=h?`${h.label} (mine)`:"My theme",b=window.prompt("Name this theme",f);if(b)try{const g={};for(const I of n.tokens){const M=d(I);M!==I.default&&(g[I.token]=M)}const x=await C.saveTheme({label:b,tokens:g});s=[...s.filter(I=>I.slug!==x.slug),x],n.onThemesChanged(s),e=x.slug,t={},n.onTheme(e),u(),y(),c(),q("Theme saved",x.label)}catch(g){q("Could not save the theme",g instanceof Error?g.message:"","error")}},B=async()=>{const h=s.find(f=>f.slug===e);if(h?.custom&&await he(`Delete “${h.label}”? Forms using it fall back to Clean.`,"Delete theme"))try{await C.deleteTheme(h.id),s=s.filter(f=>f.slug!==h.slug),n.onThemesChanged(s),e="clean",t={},n.onTheme(e),u(),y(),c()}catch(f){q("Could not delete the theme",f instanceof Error?f.message:"","error")}},X=async()=>{const h=s.find(g=>g.slug===e),f={label:h?.label??e,tokens:{...h?.tokens??{},...t}},b=JSON.stringify(f,null," ");try{await navigator.clipboard.writeText(b),q("Theme copied","Paste it into another site to import it.")}catch{window.prompt("Copy this theme",b)}},ae=()=>{const h=window.prompt("Paste a theme");if(h)try{const f=JSON.parse(h);if(!f.tokens||typeof f.tokens!="object")throw new Error("That does not look like a theme.");for(const[b,g]of Object.entries(f.tokens))t[b]=String(g),n.onOverride(b,String(g));y(),c()}catch(f){q("Could not read that theme",f instanceof Error?f.message:"","error")}},_=k("Delete",()=>void B(),"danger"),A=()=>{const h=s.find(f=>f.slug===e);_.hidden=!h?.custom};return u(),m(),y(),c(),A(),r("div",{class:"atf-studio",children:[r("div",{class:"atf-studio__top",children:[r("div",{class:"atf-studio__picker",children:[r("h2",{class:"atf-studio__heading",text:"Theme"}),l]}),r("div",{class:"atfs__actions",children:[k("Save as a theme",()=>void E(),"primary"),k("Export",()=>void X()),k("Import",ae),_]})]}),r("div",{class:"atf-studio__panes",children:[r("aside",{class:"atf-studio__controls",children:[r("p",{class:"atfb-hint",text:"Changes apply to this form only, until you save them as a theme."}),o,r("details",{class:"atfs-advanced",children:[r("summary",{text:"Every setting"}),r("p",{class:"atfb-hint",text:"The full token list. The dials above write into it."}),a]})]}),r("main",{class:"atf-studio__preview",children:[r("h2",{class:"screen-reader-text",text:"Live preview"}),i]})]})]})}async function we(){const n=document.querySelector("[data-atfs-root]");if(!n||n.dataset.atfsMounted)return;n.dataset.atfsMounted="1";const e=n.querySelector("[data-atfs-bar]"),t=n.querySelector(".atfs__body")??n;try{const[s,i,a]=await Promise.all([C.config(),C.listThemes(),C.listForms()]);if(!a.length){D(t),t.append(r("div",{class:"atfb-empty",children:[r("h2",{text:"No forms to preview"}),r("p",{text:"Make a form first — a theme needs something to dress."})]}));return}let o=a[0],l={},c=o.theme;const d=()=>{D(t),t.append(ze({themes:i,tokens:s.tokens,activeSlug:c,overrides:l,standalone:!0,onTheme:u=>{c=u},onOverride:(u,L)=>{L===""?delete l[u]:l[u]=L},previewFor:async(u,L)=>{const w=await C.getForm(o.id);w.schema.settings.theme=u,w.schema.settings.themeOverrides=L;const{html:m}=await C.preview(o.id,{schema:w.schema,theme:u});return m},onThemesChanged:u=>{i.length=0,i.push(...u)}}))};e&&(D(e),e.append(r("span",{class:"atfs__label",text:"Preview against"}),$(String(o.id),a.map(u=>({value:String(u.id),label:u.title||"(untitled)"})),u=>{o=a.find(L=>String(L.id)===u)??a[0],l={},d()}))),d()}catch(s){D(t),t.append(r("p",{class:"atfb-error",text:s instanceof Error?s.message:"Could not load themes."}))}}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>void we()):we(),document.addEventListener("os-window-content-loaded",()=>void we()));function ye(n){const e=n.trim();return/^#[0-9a-f]{6}$/i.test(e)?e:/^#[0-9a-f]{3}$/i.test(e)?`#${e[1]}${e[1]}${e[2]}${e[2]}${e[3]}${e[3]}`:"#888888"}function xe(){return window.wp?.os??null}const Xe="allterrain-forms/preview",_e="allterrain-forms-preview";function Vt(n){const e=xe();if(!e?.registerTitleBarButton)return()=>{};const t=()=>{try{e.registerTitleBarButton({id:Xe,label:"Preview this form",icon:"dashicons-visibility",placement:"right",order:90,match:s=>{const i=s?.id??s?.config?.id??"";return i==="allterrain-forms"||i.startsWith("allterrain-forms#")},onClick:()=>void Ge(n),owner:"allterrain-forms-builder"})}catch{}};return e.ready?e.ready(t):t(),()=>{try{e.unregisterTitleBarButton?.(Xe)}catch{}}}async function Ge(n){n.isDirty()&&await n.save();const e=n.current();e&&Je(e.id,e.title,e.previewUrl)}function Je(n,e,t){const s=xe();if(!s?.windowManager?.open){window.open(t,"_blank","noopener");return}s.windowManager.open({id:`${_e}-${n}`,baseId:_e,url:t,title:`Preview: ${e}`,icon:"dashicons-visibility"})}function Qt(n,e,t){if(!xe()?.windowManager?.open||!document.querySelector(`[data-window-id^="${_e}-${n}"]`))return;const a=t.includes("?")?"&":"?";Je(n,e,`${t}${a}atf_r=${Date.now()}`)}const Zt="allterrain-forms/form";function es(){return window.wp?.os?.relations??null}function ts(n){const e=n.closest("[data-window-id], .os-window");if(!e)return null;const t=e.getAttribute("data-window-id");if(t)return t;const s=e.id??"";return s?s.replace(/^wp-window-/,""):null}const ss=6e3,Ke=120;function Ve(n,e){const t=es();if(ke.set(n,e),!t?.set)return;const s=a=>{if(ie.get(n)!==i)return;const o=ts(n);if(!o){Date.now()s(a),Ke);return}try{t.set(o,e)}catch(c){Qe||(Qe=!0,console.error("[AllTerrain Forms] The shell refused a window identity.",c,e)),ie.delete(n);return}if(!e||t.get?.(o)?.id===e.id||Date.now()>=a){ie.delete(n);return}window.setTimeout(()=>s(a),Ke)},i=Symbol("atf-identity");ie.set(n,i),s(Date.now()+ss)}let Qe=!1;const ie=new WeakMap,ke=new Map;function ns(){for(const[n,e]of ke){if(!n.isConnected){ke.delete(n);continue}Ve(n,e)}}if(typeof document<"u")for(const n of["os-window-content-loaded","os-window-opened"])document.addEventListener(n,()=>ns());function is(n,e){return{type:Zt,id:n.id,label:n.title||"Untitled form",related:[{id:`allterrain-forms/entries-${n.id}`,label:"Entries for this form",url:`${e}admin.php?page=allterrain-forms-entries&form=${n.id}`,group:"allterrain-forms",groupLabel:"Forms",icon:"dashicons-list-view"}]}}const V="allterrain-forms/field",as=["openstation/file","desktop-mode/file","openstation/attachment"],Ze="allterrain-forms/logic-map-v2";function R(n,e){return n.dataset.atfbBind=e,n}const et={level:{key:"level",label:"Heading level",control:"select",hint:"Headings should step down one at a time.",options:[{value:"2",label:"Heading 2"},{value:"3",label:"Heading 3"},{value:"4",label:"Heading 4"},{value:"5",label:"Heading 5"},{value:"6",label:"Heading 6"}]},content:{key:"content",label:"HTML",control:"textarea",hint:"Shown as written. Scripts are stripped when the form is saved."},consenttext:{key:"consentText",label:"What they are agreeing to",control:"textarea",hint:"Shown beside the tick box. Links are allowed."},height:{key:"height",label:"Height",control:"number",hint:"In pixels."},columns:{key:"columns",label:"Columns",control:"number",hint:"How many pictures sit side by side."},multiple:{key:"multiple",label:"Let them choose more than one",control:"checkbox"},inline:{key:"inline",label:"Lay the options out in a row",control:"checkbox"},other:{key:"other",label:"Offer an “Other” box",control:"checkbox",hint:"Adds a final option with a box to type in."},filetypes:{key:"filetypes",label:"Accepted file types",control:"commas",hint:"Extensions, separated by commas. Empty accepts anything the site allows."},maxsize:{key:"maxsize",label:"Largest file",control:"number",hint:"In megabytes."},maxfiles:{key:"maxfiles",label:"How many files",control:"number"},minrows:{key:"minRows",label:"Fewest rows",control:"number",also:{key:"maxRows",label:"Most rows"}},endlabels:{key:"minLabel",label:"Label at the low end",control:"text",also:{key:"maxLabel",label:"Label at the high end"}},points:{key:"points",label:"Points if correct",control:"number"},minchoices:{key:"minChoices",label:"Fewest they may pick",control:"number",also:{key:"maxChoices",label:"Most they may pick"}}},rs={label:"the canvas edits it in place, and the inspector mirrors it",placeholder:"edited by typing into the control on the canvas",hint:"edited under the control on the canvas",nextlabel:"edited on the page break’s own Next button",prevlabel:"edited on the page break’s own Back button",addlabel:"edited on the repeater’s own Add button",choices:"the choices editor, and the option list on the canvas",required:"the toggle on the card",width:"its own row",css:"its own row",prefill:"its own section",logic:"the conditional logic section",formula:"its own row, with the currency that goes with it",currency:"rendered with the formula",correct:"the choices editor marks the right answer",default:"its own row, typed to match what the field stores",rows:"a row count on a textarea; the statement list on a Likert matrix",parts:"a tick box per part, listed by the server",min:"the validation section, paired with max",max:"the validation section, paired with min",step:"the validation section",minlength:"the validation section, paired with maxlength",maxlength:"the validation section, paired with minlength",mindate:"the validation section, paired with maxdate",maxdate:"the validation section, paired with mindate",mintime:"the validation section, paired with maxtime",maxtime:"the validation section, paired with mintime",pattern:"the validation section",unique:"the validation section",maxchoices:"rendered with minchoices",maxrows:"rendered with minrows"};function tt(n,e,t){const s=n[e.key],i=a=>t(e.key,a);if(e.control==="checkbox")return T(e.label,!!s,i);if(e.control==="commas"){const a=Array.isArray(s)?s:[];return p(e.label,v(a.join(", "),o=>{i(o.split(",").map(l=>l.trim().replace(/^\./,"").toLowerCase()).filter(Boolean))}),e.hint)}return e.control==="select"?p(e.label,$(String(s??""),e.options??[],i),e.hint):e.control==="textarea"?p(e.label,se(String(s??""),i),e.hint):e.control==="number"?p(e.label,P(String(s??""),i),e.hint):p(e.label,R(v(String(s??""),i),e.key),e.hint)}function st(n,e){const t=new Set(n.map(i=>i.key).filter(Boolean));let s=n.length+1;return e.split(` -`).map(i=>i.trim()).filter(Boolean).map((i,a)=>{const o=n[a]?.key;if(o)return{key:o,label:i};for(;t.has(`r${s}`);)s+=1;return t.add(`r${s}`),{key:`r${s}`,label:i}})}const Se=[{value:"user:email",label:"Their email address",group:"About the person filling it in",tag:"{user:email}"},{value:"user:display_name",label:"Their name",group:"About the person filling it in",tag:"{user:display_name}"},{value:"user:first_name",label:"Their first name",group:"About the person filling it in"},{value:"user:last_name",label:"Their last name",group:"About the person filling it in"},{value:"user:login",label:"Their username",group:"About the person filling it in"},{value:"date:today",label:"Today’s date",group:"The date and time",tag:"{date}"},{value:"date:now",label:"The time right now",group:"The date and time",tag:"{time}"},{value:"site",label:"This site’s name",group:"About this site",tag:"{site}"},{value:"site:url",label:"This site’s address",group:"About this site",tag:"{site:url}"},{value:"site:admin_email",label:"The site administrator’s email",group:"About this site",tag:"{admin_email}"}],os=["About the person filling it in","The date and time","About this site"],Ee=(n,e)=>O?.i18n?.[n]??e;class nt{constructor(e){this.config=null,this.themes=[],this.forms=[],this.form=null,this.schema=null,this.selected=null,this.tab="build",this.logicMap=null,this.canvasTheme=r("style"),this.canvasThemeSignature="",this.openSections=new Map,this.logicMapOn=bt(Ze)!=="off",this.dirty=!1,this.teardowns=[],this.history=[],this.historyAt=-1,this.autosave=De(()=>{this.save(!0)},2500),this.renderInspector=mt(()=>{if(D(this.inspector),!this.schema||(this.root.classList.toggle("atfb--build-only-panes",this.tab!=="build"),this.tab!=="build"))return;const t=this.schema.fields.find(o=>o.id===this.selected);if(!t){this.inspector.append(r("div",{class:"atfb-placeholder",children:[r("p",{text:"Select a field to change it."}),r("p",{class:"atfb-hint",text:"Drag a field from the palette, or press one to add it to the end."})]}));return}const s=this.config?.fieldTypes.find(o=>o.type===t.type),i=s?.supports??[],a=(o,l)=>{t[o]=l,this.markDirty(),this.renderCanvas()};this.inspector.append(r("h3",{class:"atfb-inspector__title",text:s?.label??t.type}),r("p",{class:"atfb-hint",text:`Reference this field as {field:${t.id}}`})),i.includes("label")&&this.inspector.append(p("Label",R(v(t.label,o=>a("label",o)),"label"))),i.includes("placeholder")&&this.inspector.append(p("Placeholder",R(v(t.placeholder,o=>a("placeholder",o)),"placeholder"))),i.includes("hint")&&this.inspector.append(p("Hint",R(v(t.hint,o=>a("hint",o)),"hint"),"Shown under the field, and read out with it.")),i.includes("nextlabel")&&this.inspector.append(p("Next button",R(v(String(t.nextLabel??""),o=>a("nextLabel",o)),"nextLabel"),"Leave empty for “Next”.")),i.includes("prevlabel")&&this.inspector.append(p("Back button",R(v(String(t.prevLabel??""),o=>a("prevLabel",o)),"prevLabel"),"Leave empty for “Back”.")),i.includes("addlabel")&&this.inspector.append(p("Add button",R(v(String(t.addLabel??""),o=>a("addLabel",o)),"addLabel"),"Leave empty for “Add another”.")),i.includes("required")&&this.inspector.append(T("Required",t.required,o=>a("required",o))),i.includes("width")&&this.inspector.append(p("Width",$(t.width,[{value:"full",label:"Full width"},{value:"two-thirds",label:"Two thirds"},{value:"half",label:"Half"},{value:"third",label:"One third"},{value:"quarter",label:"One quarter"}],o=>a("width",o)))),s?.choices&&this.inspector.append(this.renderChoicesEditor(t,a)),this.renderTypeSettings(t,s,i,a),(t.type==="total"||i.includes("formula"))&&this.inspector.append(p("Formula",v(String(t.formula??""),o=>a("formula",o)),"Reference fields with braces: {f1} * {f2} + 10. Functions: min, max, sum, avg, round, ceil, floor, abs, sqrt, pow."),p("Currency symbol",v(String(t.currency??""),o=>a("currency",o)))),this.inspector.append(this.renderValidationSection(t,i,a)),this.inspector.append(this.renderLogicSection(t,a)),i.includes("prefill")&&this.inspector.append(this.prefillControl(t,a)),i.includes("css")&&this.inspector.append(p("CSS class",v(t.cssClass,o=>a("cssClass",o))))}),this.root=e,this.bar=e.querySelector("[data-atfb-bar]")??r("div"),this.palette=e.querySelector("[data-atfb-palette]")??r("div"),this.canvas=e.querySelector("[data-atfb-canvas]")??r("div"),this.inspector=e.querySelector("[data-atfb-inspector]")??r("div")}async start(){try{const[s,i,a]=await Promise.all([C.config(),C.listThemes(),C.listForms()]);this.config=s,this.themes=i,this.forms=a}catch(s){this.fail(s);return}this.teardowns.push(dt([V])),this.teardowns.push(Vt({current:()=>this.form?{id:this.form.id,title:this.form.title,previewUrl:this.form.previewUrl}:null,isDirty:()=>this.dirty,save:()=>this.save(!0)}));const e=s=>{this.dirty&&(s.preventDefault(),s.returnValue="")};window.addEventListener("beforeunload",e),this.teardowns.push(()=>window.removeEventListener("beforeunload",e));const t=s=>{!(s.metaKey||s.ctrlKey)||s.key.toLowerCase()!=="z"||s.target?.closest("input, textarea, [contenteditable]")||(s.preventDefault(),this.travel(s.shiftKey?1:-1))};this.root.addEventListener("keydown",t),this.teardowns.push(()=>this.root.removeEventListener("keydown",t)),this.renderBar(),this.renderPalette(),this.forms.length?await this.open(this.forms[0].id):this.renderFormsList()}destroy(){this.teardowns.forEach(e=>e()),this.teardowns=[]}fail(e){D(this.bar),this.bar.append(r("p",{class:"atfb-error",text:e instanceof Error?e.message:"Something went wrong loading your forms."}))}renderBar(){D(this.bar);const e=$(String(this.form?.id??""),[...this.forms.map(i=>({value:String(i.id),label:i.title||"(untitled)"}))],i=>void this.open(Number(i)));e.setAttribute("aria-label","Choose a form");const t=r("input",{class:"atfb-title",type:"text",value:this.form?.title??"",placeholder:"Untitled form",attrs:{"aria-label":"Form title"},on:{input:i=>{this.form&&(this.form.title=i.target.value,this.markDirty())}}}),s=r("div",{class:"atfb-tabs",attrs:{role:"tablist"},children:[["build","Build"],["theme","Theme"],["settings","Settings"],["notify","Notifications"],["confirm","Confirmations"]].map(([i,a])=>r("button",{class:`atfb-tab${this.tab===i?" is-active":""}`,type:"button",text:a,attrs:{role:"tab","aria-selected":this.tab===i},on:{click:()=>{this.tab=i,this.renderBar(),this.renderInspector(),this.renderCanvas()}}}))});this.bar.append(r("div",{class:"atfb-bar__left",children:[this.forms.length>1?e:null,t]}),s,r("div",{class:"atfb-bar__right",children:[this.historyButton("undo","Undo",-1),this.historyButton("redo","Redo",1),k("New",()=>void this.showTemplates(),"secondary","plus-alt2"),k("Export",()=>void this.exportForm(),"secondary","download"),k("Import",()=>void this.importForm(),"secondary","upload"),k("Preview",()=>void this.preview(),"secondary","visibility"),k("Entries",()=>this.openEntries(),"secondary","list-view"),this.logicMapButton(),this.saveButton()]}))}liveField(e){return this.schema?.fields.find(t=>t.id===e)}syncCanvas(e){const t=this.canvas.querySelector(`[data-atfb-card="${CSS.escape(e.id)}"]`);if(t)for(const s of t.querySelectorAll(".atfb-editable[data-atfb-bind]")){const i=He(e,s.dataset.atfbBind??"");s.textContent!==i&&(s.textContent=i)}}syncInspector(e){if(this.selected===e.id)for(const t of this.inspector.querySelectorAll("[data-atfb-bind]")){const s=He(e,t.dataset.atfbBind??"");t.value!==s&&("value"in t?t.value=s:t.setAttribute("value",s))}}rebindCanvas(){const e=document.activeElement;if(e instanceof HTMLElement&&this.canvas.contains(e)){e.addEventListener("blur",()=>this.rebindCanvas(),{once:!0});return}this.renderCanvas()}snapshot(){if(!this.schema)return;const e=JSON.stringify(this.schema);this.history[this.historyAt]!==e&&(this.history=this.history.slice(0,this.historyAt+1),this.history.push(e),this.history.length>60&&this.history.shift(),this.historyAt=this.history.length-1)}travel(e){const t=this.historyAt+e;!this.schema||t<0||t>=this.history.length||(this.historyAt=t,this.schema=JSON.parse(this.history[t]),this.schema.fields.some(s=>s.id===this.selected)||(this.selected=null),this.dirty=!0,this.autosave(),this.renderBar(),this.renderCanvas(),this.renderInspector())}historyButton(e,t,s){const i=this.historyAt+s,a=k(t,()=>this.travel(s),"secondary",e);return a.disabled=i<0||i>=this.history.length,a}exportForm(){if(!this.form||!this.schema)return;const e={plugin:"allterrain-forms",version:O?.version??"",title:this.form.title,schema:this.schema},t=new Blob([JSON.stringify(e,null," ")],{type:"application/json"}),s=URL.createObjectURL(t),i=r("a",{href:s,attrs:{download:`${this.form.title.replace(/[^a-z0-9]+/gi,"-").toLowerCase()||"form"}.json`}});document.body.append(i),i.click(),i.remove(),window.setTimeout(()=>URL.revokeObjectURL(s),1e3)}async importForm(){const e=r("input",{type:"file",attrs:{accept:"application/json,.json"}});e.addEventListener("change",async()=>{const t=e.files?.[0];if(t)try{const s=JSON.parse(await t.text());if(!s.schema)throw new Error("That file does not contain a form.");const i=await C.createForm({title:s.title??t.name.replace(/\.json$/i,""),schema:s.schema});this.forms.unshift({id:i.id,title:i.title,status:i.status,modified:i.modified,fields:i.schema.fields.length,theme:i.schema.settings.theme,entries:0,unread:0,views:0,submissions:0,shortcode:i.shortcode}),this.form=i,this.schema=i.schema,this.selected=null,this.dirty=!1,this.history=[],this.historyAt=-1,this.snapshot(),this.renderBar(),this.renderCanvas(),this.renderInspector(),q("Form imported",i.title)}catch(s){q("Could not import that file",s instanceof Error?s.message:"","error")}}),e.click()}saveButton(){const e=k("Save",()=>void this.save(),"primary");return e.disabled=!this.dirty,e.setAttribute("data-atfb-save",""),e}markDirty(){this.dirty=!0;const e=this.bar.querySelector("[data-atfb-save]");e&&(e.disabled=!1),this.autosave()}async save(e=!1){if(!(!this.form||!this.schema))try{const t=await C.updateForm(this.form.id,{title:this.form.title,schema:this.schema});this.form=t,this.schema=t.schema,this.dirty=!1,this.rebindCanvas(),Bt(t.id);const s=this.forms.find(a=>a.id===t.id);s&&(s.title=t.title);const i=this.bar.querySelector("[data-atfb-save]");i&&(i.disabled=!0),Qt(t.id,t.title,t.previewUrl),e||q("Form saved",t.title)}catch(t){q(Ee("saveFailed","Could not save"),t instanceof Error?t.message:"","error")}}async open(e){if(!(this.dirty&&!await he("You have unsaved changes. Discard them?"))){try{this.form=await C.getForm(e),this.schema=this.form.schema,this.selected=null,this.dirty=!1,this.history=[],this.historyAt=-1,this.snapshot()}catch(t){this.fail(t);return}this.renderBar(),this.renderCanvas(),this.renderInspector(),this.announceIdentity()}}announceIdentity(){this.form&&Ve(this.root,is(this.form,O?.adminUrl??""))}async showTemplates(){if(!this.config)return;const e=r("div",{class:"atfb-overlay"}),t=()=>e.remove(),s=r("div",{class:"atfb-templates",children:this.config.templates.map(i=>r("button",{class:"atfb-template",type:"button",on:{click:async()=>{t();try{const a=await C.createForm({template:i.slug});this.forms.unshift({id:a.id,title:a.title,status:a.status,modified:a.modified,fields:a.schema.fields.length,theme:a.schema.settings.theme,entries:0,unread:0,views:0,submissions:0,shortcode:a.shortcode}),this.form=a,this.schema=a.schema,this.selected=null,this.dirty=!1,this.renderBar(),this.renderCanvas(),this.renderInspector(),this.announceIdentity()}catch(a){q("Could not create the form",a instanceof Error?a.message:"","error")}}},children:[F(i.icon),r("strong",{text:i.label}),r("span",{text:i.description})]}))});e.append(r("div",{class:"atfb-modal",attrs:{role:"dialog","aria-label":"Start a new form"},children:[r("h2",{text:"Start a new form"}),s,r("div",{class:"atfb-modal__actions",children:[k("Cancel",t)]})]})),e.addEventListener("click",i=>{i.target===e&&t()}),document.addEventListener("keydown",i=>{i.key==="Escape"&&t()},{once:!0}),this.root.append(e),s.querySelector("button")?.focus()}renderFormsList(){D(this.canvas),this.canvas.append(r("div",{class:"atfb-empty",children:[r("h2",{text:"No forms yet"}),r("p",{text:"Start from a template, or build one from nothing."}),k("New form",()=>void this.showTemplates(),"primary","plus-alt2")]}))}openEntries(){const e=window.wp?.os;if(e?.openWindow){e.openWindow("allterrain-forms-entries");return}window.location.assign(`${O?.adminUrl??""}admin.php?page=allterrain-forms-entries`)}renderPalette(){if(!this.config)return;D(this.palette);const e=new Map;for(const s of this.config.fieldTypes){const i=e.get(s.group)??[];i.push(s),e.set(s.group,i)}const t=r("input",{class:"atfb-input atfb-palette__search",type:"search",placeholder:"Search fields",attrs:{"aria-label":"Search field types"},on:{input:s=>{const i=s.target.value.toLowerCase().trim();this.palette.querySelectorAll(".atfb-chip").forEach(a=>{const o=(a.textContent??"").toLowerCase();a.hidden=i!==""&&!o.includes(i)}),this.palette.querySelectorAll(".atfb-group").forEach(a=>{const o=Array.from(a.querySelectorAll(".atfb-chip")).some(l=>!l.hidden);a.hidden=!o})}}});this.palette.append(t);for(const[s,i]of Object.entries(this.config.groups)){const a=e.get(s);a?.length&&this.palette.append(r("div",{class:"atfb-group",children:[r("h3",{class:"atfb-group__title",text:i}),r("div",{class:"atfb-group__items",children:a.map(o=>this.paletteChip(o))})]}))}}paletteChip(e){const t=r("button",{class:"atfb-chip",type:"button",title:e.description,attrs:{"data-atf-type":e.type},children:[F(e.icon),r("span",{text:e.label})]});return t.addEventListener("pointerdown",s=>{const i=r("div",{class:"atfb-chip atfb-chip--ghost",children:[F(e.icon),r("span",{text:e.label})]});Y().start({payload:Te(V,t,{fieldType:e.type,isNew:!0},s,i),origin:s,onClickOnly:()=>this.addField(e.type)})}),t.addEventListener("click",s=>{Y().recentlyEndedDrag()&&s.preventDefault()}),t}renderCanvas(){if(D(this.canvas),!this.schema||!this.form){this.renderFormsList();return}if(this.tab!=="build"){this.canvas.append(this.renderTabCanvas());return}const e=r("div",{class:"atfb-canvas__list",attrs:{"data-atfb-list":""}});this.schema.fields.length||e.append(r("div",{class:"atfb-placeholder",text:Ee("emptyCanvas","Drag a field from the left to begin.")})),this.schema.fields.forEach((s,i)=>{e.append(this.renderFieldCard(s,i))});const t=r("div",{class:"atfb-canvas__inner",children:[r("p",{class:"atfb-shortcode",text:this.form.shortcode,title:"Paste this anywhere to place the form"}),e]});this.canvas.append(t),this.registerCanvasTarget(e),this.paintLogicMap(t),this.paintCanvasTheme()}prefillControl(e,t){const s=e.prefill.startsWith("query:"),i=Se.some(w=>w.value===e.prefill),a=s?"query":i&&e.prefill||(e.prefill?"custom":""),o=r("div",{class:"atfb-prefill__detail"}),l=r("p",{class:"atfb-row__hint atfb-prefill__preview"}),c=w=>{if(o.replaceChildren(),l.replaceChildren(),w==="query"){const m=e.prefill.startsWith("query:")?e.prefill.slice(6):"";o.append(v(m,y=>{const S=y.trim();t("prefill",S?`query:${S}`:""),d(`query:${S}`)},"utm_source"),r("p",{class:"atfb-row__hint",text:"The name of the parameter in the link people arrive on."}))}w==="custom"&&o.append(v(e.prefill,m=>{t("prefill",m),d(m)},"myplugin:something"),r("p",{class:"atfb-row__hint",text:"For a source another plugin has added through atf_resolve_prefill."})),d(e.prefill)},d=w=>{if(l.replaceChildren(),!w)return;if(w.startsWith("query:")){const y=w.slice(6);y&&(l.textContent=`A visit to …/your-page/?${y}=abc opens the form with “abc” in it.`);return}const m=Se.find(y=>y.value===w)?.tag;m&&be(this.form?.id??0).then(y=>{for(const S of y)for(const E of S.items)if(E.tag===m){const B=w.startsWith("user:");if(!E.sample){l.textContent="Empty unless the visitor is signed in.";return}l.textContent=B?`Opens with “${E.sample}” for you — empty for a visitor who is not signed in.`:`Opens with “${E.sample}”.`;return}})};c(a);const u=r("select",{class:"atfb-input atfb-select",on:{change:w=>{const m=w.target.value;t("prefill",m==="query"||m==="custom"?"":m),c(m)}}});u.append(r("option",{value:"",text:"Nothing — leave it empty",attrs:{selected:a===""}}));for(const w of os){const m=document.createElement("optgroup");m.label=w;for(const y of Se.filter(S=>S.group===w))m.append(r("option",{value:y.value,text:y.label,attrs:{selected:y.value===a}}));u.append(m)}const L=document.createElement("optgroup");return L.label="From the link they arrived on",L.append(r("option",{value:"query",text:"A parameter in the web address",attrs:{selected:a==="query"}})),u.append(L,r("option",{value:"custom",text:"Something else (advanced)",attrs:{selected:a==="custom"}})),p("Pre-fill this with",r("div",{class:"atfb-prefill",children:[u,o,l]}),"What the box already contains when the form opens. They can still change it.")}renderCondition(e){const t=e.some(s=>s.kind==="field"&&s.missing);return r("span",{class:`atfb-cond${t?" is-broken":""}`,attrs:{"aria-label":Pe(e)},children:[F("randomize"),...e.map(s=>this.renderConditionToken(s))]})}renderConditionToken(e){return e.kind==="field"&&!e.missing?r("button",{class:"atfb-cond__chip atfb-cond__chip--field",type:"button",text:e.text,title:"Go to this question",on:{click:i=>{i.stopPropagation(),this.selectField(e.fieldId),this.canvas.querySelector(`[data-atfb-card="${CSS.escape(e.fieldId)}"]`)?.scrollIntoView({block:"nearest",behavior:"smooth"})}}}):r("span",{class:{verb:"atfb-cond__verb",field:"atfb-cond__chip atfb-cond__chip--missing",operator:"atfb-cond__op",value:"atfb-cond__chip atfb-cond__chip--value",join:"atfb-cond__join"}[e.kind],text:e.text})}section(e,t,s,i=!1){const a=r("details",{class:"atfb-section",attrs:{open:this.openSections.get(e)??i},children:[r("summary",{text:t}),...s]});return a.addEventListener("toggle",()=>this.openSections.set(e,a.open)),a}logicMapButton(){const e=ue(this.schema?.fields??[]).length>0,t=k(this.logicMapOn?"Hide logic":"Show logic",()=>{this.logicMapOn=!this.logicMapOn,gt(Ze,this.logicMapOn?"on":"off"),this.renderBar(),this.renderCanvas()},this.logicMapOn?"primary":"secondary","randomize");return t.title="Draw a line from each question to the ones it decides.",t.hidden=!e,t}paintLogicMap(e){this.logicMap?.destroy(),this.logicMap=null;const t=this.schema?.fields??[],s=ue(t);if(!s.length||!this.logicMapOn)return;e.classList.add("has-logicmap");const i=new At(e);i.setEdges(s),i.highlight(this.selected??""),this.logicMap=i,e.addEventListener("pointerover",a=>{const o=a.target.closest("[data-atfb-card]");i.highlight(o?.dataset.atfbCard??this.selected??"")}),e.addEventListener("pointerleave",()=>i.highlight(this.selected??""))}renderFieldCard(e,t){const s=this.config?.fieldTypes.find(d=>d.type===e.type),i=this.selected===e.id,a=this.schema?.fields??[],o=Ct(e,a),l=$t(a).get(e.id)??0,c=r("div",{class:`atfb-card${i?" is-selected":""}`,attrs:{"data-atfb-card":e.id,"data-index":t,tabindex:"0",role:"button","aria-pressed":i,"aria-label":`${e.label||s?.label||e.type}, ${t+1} of ${this.schema?.fields.length??0}`},children:[r("div",{class:"atfb-card__grip",attrs:{"aria-hidden":"true"},children:[F("menu")]}),r("div",{class:"atfb-card__body",children:[r("div",{class:"atfb-card__head",children:[F(s?.icon??"dashicons-forms"),r("span",{class:"atfb-card__type",text:s?.label??e.type}),this.requiredToggle(e),l?r("span",{class:"atfb-badge atfb-badge--controls",text:l===1?"controls 1 field":`controls ${l} fields`,title:"Other questions appear or disappear based on this answer."}):null]}),Ft(e,s,{edit:d=>{const u=this.liveField(e.id);u&&(d(u),this.markDirty(),this.syncInspector(u))},restructure:d=>{const u=this.liveField(e.id);u&&(this.snapshot(),d(u),this.markDirty(),this.renderCanvas(),this.renderInspector())}}),o.length?this.renderCondition(o):null]}),r("div",{class:"atfb-card__actions",children:[this.cardAction("admin-page","Duplicate",()=>this.duplicateField(e.id)),this.cardAction("trash","Delete",()=>void this.deleteField(e.id))]})]});return c.addEventListener("click",d=>{d.target.closest(".atfb-card__actions")||Y().recentlyEndedDrag()||this.selectField(e.id)}),c.addEventListener("keydown",d=>{if(d.key==="Enter"||d.key===" "){d.preventDefault(),this.selectField(e.id);return}d.altKey&&(d.key==="ArrowUp"||d.key==="ArrowDown")&&(d.preventDefault(),this.moveField(e.id,d.key==="ArrowUp"?t-1:t+1),window.requestAnimationFrame(()=>{this.canvas.querySelector(`[data-atfb-card="${CSS.escape(e.id)}"]`)?.focus()})),(d.key==="Delete"||d.key==="Backspace")&&(d.preventDefault(),this.deleteField(e.id))}),c.addEventListener("pointerdown",d=>{d.target.closest(".atfb-card__actions")||Y().start({payload:Te(V,c,{fieldId:e.id,field:e,isNew:!1},d),origin:d})}),c}requiredToggle(e){return r("button",{class:`atfb-req${e.required?" is-on":""}`,type:"button",text:e.required?"Required":"Optional",title:e.required?"This must be answered. Click to make it optional.":"Click to make this required.",attrs:{"aria-pressed":e.required?"true":"false"},on:{pointerdown:s=>s.stopPropagation(),click:s=>{s.stopPropagation(),e.required=!e.required,this.markDirty(),this.renderCanvas(),this.renderInspector()}}})}cardAction(e,t,s){return r("button",{class:"atfb-card__action",type:"button",title:t,attrs:{"aria-label":t},on:{click:i=>{i.stopPropagation(),s()}},children:[F(e)]})}registerCanvasTarget(e){const t=r("div",{class:"atfb-marker",attrs:{"aria-hidden":"true"}}),s=Y().registerDropTarget({id:`atfb-canvas-${this.form?.id??0}`,element:e,accept:a=>a.type===V,onEnter:()=>e.classList.add("is-dropping"),onLeave:()=>{e.classList.remove("is-dropping"),t.remove()},onDrop:(a,o)=>{e.classList.remove("is-dropping"),t.remove();const l=a.payload.data,c=l.fieldId?this.canvas.querySelector(`[data-atfb-card="${CSS.escape(l.fieldId)}"]`):null,d=$e(e,".atfb-card",o.clientY,c??void 0);if(l.isNew&&l.fieldType){this.addField(l.fieldType,d);return}if(l.fieldId&&this.schema?.fields.some(u=>u.id===l.fieldId)){this.moveField(l.fieldId,d);return}l.field&&this.insertField({...l.field,id:""},d)}});this.teardowns.push(s);const i=a=>{const o=a.detail;if(o?.payload?.type!==V||!e.classList.contains("is-dropping"))return;const l=o.clientY??0,c=$e(e,".atfb-card",l),d=e.querySelectorAll(".atfb-card");c>=d.length?e.append(t):d[c].before(t)};document.addEventListener("os.drag.move",i),this.teardowns.push(()=>document.removeEventListener("os.drag.move",i))}addField(e,t){const s=this.config?.fieldTypes.find(a=>a.type===e);if(!s||!this.schema)return;const i={id:this.nextFieldId(),type:e,label:s.input?s.label:"",placeholder:"",hint:"",required:!1,width:"full",cssClass:"",default:"",choices:s.choices?[{label:"First choice",value:"first"},{label:"Second choice",value:"second"}]:[],logic:{enabled:!1,action:"show",match:"all",rules:[]},messages:{},prefill:"",...s.settings};this.insertField(i,t)}insertField(e,t){if(!this.schema)return;e.id||(e.id=this.nextFieldId()),this.snapshot();const s=t===void 0?this.schema.fields.length:Math.max(0,Math.min(t,this.schema.fields.length));this.schema.fields.splice(s,0,e),this.selected=e.id,this.markDirty(),this.renderCanvas(),this.renderInspector(),window.requestAnimationFrame(()=>{this.canvas.querySelector(`[data-atfb-card="${CSS.escape(e.id)}"]`)?.focus()})}moveField(e,t){if(!this.schema)return;const s=this.schema.fields.findIndex(o=>o.id===e);if(s<0)return;const i=Math.max(0,Math.min(t,this.schema.fields.length-1));if(s===i)return;this.snapshot();const[a]=this.schema.fields.splice(s,1);this.schema.fields.splice(i>s?i-1:i,0,a),this.markDirty(),this.renderCanvas()}duplicateField(e){if(!this.schema)return;const t=this.schema.fields.findIndex(i=>i.id===e);if(t<0)return;const s=JSON.parse(JSON.stringify(this.schema.fields[t]));s.id=this.nextFieldId(),this.insertField(s,t+1)}async deleteField(e){if(!this.schema)return;const t=this.schema.fields.filter(i=>i.logic?.rules?.some(a=>a.field===e)),s=t.length?`Delete this field? ${t.length} other field${t.length===1?"":"s"} use it in a condition, and those conditions will stop working.`:Ee("confirmDelete","Delete this? It cannot be undone.");await he(s,"Delete field")&&(this.snapshot(),this.schema.fields=this.schema.fields.filter(i=>i.id!==e),this.selected===e&&(this.selected=null),this.markDirty(),this.renderCanvas(),this.renderInspector())}selectField(e){this.selected=e;for(const t of this.canvas.querySelectorAll("[data-atfb-card]")){const s=t.dataset.atfbCard===e;t.classList.toggle("is-selected",s),t.setAttribute("aria-pressed",s?"true":"false")}this.logicMap?.highlight(e),this.renderInspector()}nextFieldId(){const e=new Set((this.schema?.fields??[]).map(s=>s.id));let t=e.size+1;for(;e.has(`f${t}`);)t++;return`f${t}`}renderTypeSettings(e,t,s,i){for(const a of s){const o=et[a];o&&(this.inspector.append(tt(e,o,i)),o.also&&this.inspector.append(tt(e,{...o,key:o.also.key,label:o.also.label,hint:o.also.hint},i)))}s.includes("rows")&&e.type!=="likert"&&this.inspector.append(p("Lines tall",P(String(e.rows??""),a=>i("rows",a)))),s.includes("rows")&&e.type==="likert"&&this.inspector.append(this.renderStatementList(e,i)),s.includes("parts")&&this.inspector.append(this.renderPartsEditor(e,t,i)),s.includes("default")&&!["files","object"].includes(t?.value??"")&&this.inspector.append(this.renderDefaultControl(e,t,i))}renderStatementList(e,t){const s=Array.isArray(e.rows)?e.rows:[];return p("Statements",se(s.map(i=>i.label??"").join(` -`),i=>t("rows",st(s,i)),5),"One per line. Each becomes a row of the matrix.")}renderPartsEditor(e,t,s){const i=t?.parts??[],a=Array.isArray(e.parts)?e.parts:i.map(l=>l.key),o=i.map(l=>T(l.label,a.includes(l.key),c=>{const d=i.map(u=>u.key).filter(u=>u===l.key?c:a.includes(u));s("parts",d.length?d:[l.key])}));return r("div",{class:"atfb-section",children:[r("h4",{class:"atfb-section__title",text:"Parts to ask for"}),...o]})}renderDefaultControl(e,t,s){const i="Filled in before they start. They can change it.";return e.type==="switch"||e.type==="consent"?T("On by default",!!e.default,a=>s("default",a)):t?.choices&&(e.choices??[]).length?p("Default answer",$(String(e.default??""),[{value:"",label:"Nothing chosen"},...(e.choices??[]).map(a=>({value:String(a.value),label:a.label||String(a.value)}))],a=>s("default",a)),i):p("Default answer",v(String(e.default??""),a=>s("default",a)),i)}renderChoicesEditor(e,t){const s=e.choices??[],i=r("div",{class:"atfb-choices"});return s.forEach((a,o)=>{const l=r("div",{class:"atfb-choice-row",children:[e.type==="image_choice"?this.choiceImageWell(a,o,e):null,R(v(a.label,c=>{const d=!a.value||a.value===a.label;a.label=c,d&&(a.value=c),this.markDirty(),this.syncCanvas(e)}),`choice:${o}:label`),R(v(a.value,c=>{a.value=c,this.markDirty()},"value"),`choice:${o}:value`),e.type==="quiz"||a.points!==void 0?P(String(a.points??""),c=>{a.points=c===""?void 0:Number(c),this.markDirty()}):P(String(a.price??""),c=>{a.price=c===""?void 0:Number(c),this.markDirty()}),r("button",{class:"atfb-card__action",type:"button",attrs:{"aria-label":`Remove ${a.label}`},on:{click:()=>{s.splice(o,1),t("choices",s),this.renderInspector()}},children:[F("trash")]})]});i.append(l)}),r("div",{class:"atfb-section",children:[r("h4",{text:"Choices"}),r("p",{class:"atfb-hint",text:e.type==="quiz"?"Label, value, points.":"Label, value, and a price for calculations."}),i,k("Add choice",()=>{s.push({label:"",value:""}),t("choices",s),this.renderInspector()},"ghost","plus-alt2"),e.type==="quiz"?p("Correct answer",$(String(e.correct??""),[{value:"",label:"—"},...s.map(a=>({value:a.value,label:a.label}))],a=>t("correct",a))):null]})}choiceImageWell(e,t,s){const i=r("div",{class:`atfb-well${e.image?" has-image":""}`,attrs:{"data-choice":t,"aria-label":`Image for ${e.label||`choice ${t+1}`}`},children:[e.image?r("span",{class:"atfb-well__id",text:`#${e.image}`}):F("format-image")]}),a=Y().registerDropTarget({id:`atfb-well-${s.id}-${t}`,element:i,accept:o=>as.includes(o.type),onEnter:()=>i.classList.add("is-dropping"),onLeave:()=>i.classList.remove("is-dropping"),onDrop:o=>{i.classList.remove("is-dropping");const l=o.payload.data,c=Number(l.attachmentId??l.id??l.file?.id??0);if(!c){q("That is not an image this field can use","","error");return}e.image=c,this.markDirty(),this.renderInspector()}});return this.teardowns.push(a),i.addEventListener("click",()=>{e.image&&(e.image=void 0,this.markDirty(),this.renderInspector())}),i}renderValidationSection(e,t,s){const i=[],a=[];t.includes("minlength")&&a.push(["minlength","Minimum characters"],["maxlength","Maximum characters"]),t.includes("min")?a.push(["min","Minimum"],["max","Maximum"]):t.includes("max")&&a.push(["max","Highest"]),t.includes("step")&&a.push(["step","Steps of"]),t.includes("mindate")&&a.push(["minDate","Earliest date"],["maxDate","Latest date"]),t.includes("mintime")&&a.push(["minTime","Earliest time"],["maxTime","Latest time"]);for(const[l,c]of a)i.push(p(c,v(String(e[l]??""),d=>s(l,d))));t.includes("pattern")&&i.push(p("Pattern",v(String(e.pattern??""),l=>s("pattern",l)),"A regular expression, without slashes.")),t.includes("unique")&&i.push(T("No two people may submit the same value",!!e.unique,l=>s("unique",l)));const o=e.messages??{};return t.includes("required")&&i.push(p("Message when required",v(o.required??"",l=>{o.required=l,s("messages",o)}),"Leave empty for the default wording.")),i.length?this.section(`validation:${e.id}`,"Validation",i):r("div",{class:"atfb-section is-empty"})}renderLogicSection(e,t){const s=e.logic,i=(this.schema?.fields??[]).filter(o=>o.id!==e.id&&o.type!=="page_break"),a=r("div",{class:"atfb-rules"});return s.rules.forEach((o,l)=>{a.append(r("div",{class:"atfb-rule",children:[$(o.field,i.map(c=>({value:c.id,label:c.label||c.id})),c=>{o.field=c,this.markDirty()}),$(o.operator,Object.entries(this.config?.operators??{}).map(([c,d])=>({value:c,label:d})),c=>{o.operator=c,this.markDirty()}),v(o.value,c=>{o.value=c,this.markDirty()}),r("button",{class:"atfb-card__action",type:"button",attrs:{"aria-label":"Remove this rule"},on:{click:()=>{s.rules.splice(l,1),t("logic",s),this.renderInspector()}},children:[F("trash")]})]}))}),this.section(`logic:${e.id}`,"Conditional logic",[T("Only show this field sometimes",s.enabled,o=>{s.enabled=o,t("logic",s),this.renderInspector()}),s.enabled?r("div",{children:[r("div",{class:"atfb-rule-head",children:[$(s.action,[{value:"show",label:"Show"},{value:"hide",label:"Hide"}],o=>{s.action=o,t("logic",s)}),r("span",{text:"this field when"}),$(s.match,[{value:"all",label:"all"},{value:"any",label:"any"}],o=>{s.match=o,t("logic",s)}),r("span",{text:"of these match:"})]}),a,k("Add rule",()=>{s.rules.push({field:i[0]?.id??"",operator:"is",value:""}),t("logic",s),this.renderInspector()},"ghost","plus-alt2")]}):null],s.enabled)}renderTabCanvas(){return this.schema?this.tab==="theme"?ze({themes:this.themes,tokens:this.config?.tokens??[],activeSlug:this.schema.settings.theme,overrides:this.schema.settings.themeOverrides,onTheme:e=>{this.schema.settings.theme=e,this.markDirty()},onOverride:(e,t)=>{t===""?delete this.schema.settings.themeOverrides[e]:this.schema.settings.themeOverrides[e]=t,this.markDirty()},previewFor:(e,t)=>this.previewHtml(e,t),onThemesChanged:e=>{this.themes=e}}):this.tab==="settings"?this.renderSettingsPane():this.tab==="notify"?this.renderNotificationsPane():this.renderConfirmationsPane():r("div")}async paintCanvasTheme(){if(!this.form||!this.schema)return;const e=this.schema.settings.theme,t=JSON.stringify([e,this.schema.settings.themeOverrides]);if(t!==this.canvasThemeSignature){this.canvasThemeSignature=t;try{const s=await this.previewHtml(e,this.schema.settings.themeOverrides??{}),i=/