diff --git a/assets/css/analytics.css b/assets/css/analytics.css index 559e2f0..b40764c 100644 --- a/assets/css/analytics.css +++ b/assets/css/analytics.css @@ -368,6 +368,22 @@ color: var( --atfb-faint ); } +/* --- Wide content scrolls in place ---------------------------------------- + + A table and a histogram both have a real minimum width — a table never + shrinks below its content, and a histogram column never shrinks below its + count label. In a narrow window that minimum exceeds the panel, and without + this the bars paint straight through the panel border and off the window. + The wrapper is the whole fix: the panel keeps its edge, the content keeps + its size, and the strip between them scrolls. */ + +.atfa-scroll { + overflow-x: auto; + /* The mean bucket wears an outline just outside its own box; without this + the scroller clips it flat along the bottom. */ + padding-block-end: 3px; +} + /* --- The cross-tab -------------------------------------------------------- */ .atfa-table { @@ -387,7 +403,14 @@ .atfa-table thead th { color: var( --atfb-muted ); font-weight: 500; - white-space: nowrap; +} + +/* The metric columns are headed by whole questions. Nowrap headers gave the + table a minimum width of every question laid end to end; letting them wrap + over a fixed column width keeps six metrics readable before the scroll has + to take over. */ +.atfa-table thead th:nth-child( n + 3 ) { + min-inline-size: 140px; } .atfa-table__label { diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 74a1b2e..c880474 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -670,10 +670,15 @@ var allTerrainFormsAnalytics = function(exports) { }) ); return this.panel(`Broken down by ${breakdown.label.toLowerCase()}`, "", [ - el("table", { - class: "atfa-table", - children: [el("thead", { children: [head] }), el("tbody", { children: rows })] - }) + // Six question columns do not fit a narrow window, and a table never + // shrinks below its content — it overflows. Wide content scrolls + // inside its own container; it must never bleed past the panel. + this.scrollable( + el("table", { + class: "atfa-table", + children: [el("thead", { children: [head] }), el("tbody", { children: rows })] + }) + ) ]); } /** One panel per question. */ @@ -753,7 +758,7 @@ var allTerrainFormsAnalytics = function(exports) { } const buckets = histogramBuckets(numbers); const peak = buckets.reduce((most, bucket) => Math.max(most, bucket.count), 0); - return el("div", { + return this.scrollable(el("div", { class: "atfa-hist", children: buckets.map( (bucket) => el("div", { @@ -769,7 +774,18 @@ var allTerrainFormsAnalytics = function(exports) { ] }) ) - }); + })); + } + /** + * Wraps something wider than the window in a sideways-scrolling strip. + * + * The one honest answer to content with a real minimum width: a table or a + * histogram that cannot shrink must scroll within its own panel, because + * the alternative — seen in the wild — is bars painting straight through + * the panel border and off the edge of the window. + */ + scrollable(child) { + return el("div", { class: "atfa-scroll", children: [child] }); } /** * The developer panel. diff --git a/assets/js/analytics.min.js b/assets/js/analytics.min.js index 59a263d..bbb82a9 100644 --- a/assets/js/analytics.min.js +++ b/assets/js/analytics.min.js @@ -1 +1 @@ -var allTerrainFormsAnalytics=function(p){"use strict";function F(){return window.wp?.os??null}const c=window.allTerrainForms;class y extends Error{constructor(e,t,n=""){super(e),this.name="ApiError",this.status=t,this.code=n}}function T(s,e){return s.includes("?")?`${s}${e.replace("?","&")}`:`${s}${e}`}async function M(s,e={}){if(!c?.restUrl)throw new y("AllTerrain Forms is not configured on this page.",0);const t=T(c.restUrl,s),n={"Content-Type":"application/json",...e.headers??{}};c.nonce&&(n["X-WP-Nonce"]=c.nonce);const o=F(),r=await(o?.fetch?(l,m)=>o.fetch(l,m,{source:"allterrain-forms"}):(l,m)=>fetch(l,m))(t,{credentials:"same-origin",...e,headers:n});if(!r.ok){let l=`Request failed with status ${r.status}.`,m="";try{const h=await r.json();l=h.message??l,m=h.code??""}catch{}throw new y(l,r.status,m)}if(r.status!==204)return await r.json()}const d=s=>M(s);async function D(s){if(!c?.wpRestUrl)throw new y("AllTerrain Forms is not configured on this page.",0);const e=c.nonce?{"X-WP-Nonce":c.nonce}:{},t=F(),n=T(c.wpRestUrl,s),o=t?.fetch?await t.fetch(n,{credentials:"same-origin",headers:e},{source:"allterrain-forms"}):await fetch(n,{credentials:"same-origin",headers:e});if(!o.ok)throw new y(`Request failed with status ${o.status}.`,o.status);return await o.json()}function j(s){if(!s||!s.includes("&"))return s;const e=document.createElement("textarea");return e.innerHTML=s,e.value}const u=(s,e)=>M(s,{method:"POST",body:JSON.stringify(e)}),w=s=>M(s,{method:"DELETE"});function C(s){const e=new URLSearchParams;for(const[n,o]of Object.entries(s))o===void 0||o===""||o===!1||e.set(n,String(o));const t=e.toString();return t?`?${t}`:""}function g(s){const e=s?.schema?.settings;return e&&(!e.themeOverrides||Array.isArray(e.themeOverrides))&&(e.themeOverrides={...e.themeOverrides}),s}const f={config:()=>d("/config"),listForms:()=>d("/forms"),getForm:s=>d(`/forms/${s}`).then(g),createForm:s=>u("/forms",s).then(g),updateForm:(s,e)=>u(`/forms/${s}`,e).then(g),duplicateForm:s=>u(`/forms/${s}/duplicate`,{}).then(g),deleteForm:s=>w(`/forms/${s}`),preview:(s,e)=>u(`/forms/${s}/preview`,e),pages:()=>D("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(s=>s.map(e=>({id:e.id,title:j(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=>u("/demo",s?{count:s}:{}),demoRemove:()=>w("/demo"),listEntries:s=>d(`/entries${C(s)}`),getEntry:s=>d(`/entries/${s}`),updateEntry:(s,e)=>u(`/entries/${s}`,e),deleteEntry:s=>w(`/entries/${s}`),exportEntries:s=>d(`/entries/export${C(s)}`),listThemes:()=>d("/themes"),saveTheme:s=>u("/themes",s),deleteTheme:s=>w(`/themes/${s}`)};function P(s){return`allterrain-forms/requested-form-${s}`}function R(s){try{const e=window.sessionStorage.getItem(P(s));return window.sessionStorage.removeItem(P(s)),Number(e)||0}catch{return 0}}function a(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[n,o]of Object.entries(e.attrs??{}))o===void 0||o===!1||t.setAttribute(n,o===!0?"":String(o));Object.assign(t.style,e.style??{});for(const[n,o]of Object.entries(e.on??{}))t.addEventListener(n,o);for(const n of e.children??[])n==null||n===!1||t.append(n);return t}function _(s){s.replaceChildren()}function N(s,e,t){if(E("os-select")&&E("os-option")){const n=document.createElement("os-select");n.setAttribute("value",s),n.classList.add("atfb-field");for(const o of e){const i=document.createElement("os-option");i.setAttribute("value",o.value),i.textContent=o.label,n.append(i)}return n.addEventListener("os-pick",o=>{t(String(o.detail?.value??""))}),n}return a("select",{class:"atfb-input atfb-select",on:{change:n=>t(n.target.value)},children:e.map(n=>a("option",{value:n.value,text:n.label,attrs:{selected:n.value===s}}))})}function I(s,e,t="secondary",n){const o=[null,a("span",{text:s})];if(E("os-button")){const i=document.createElement("os-button");i.setAttribute("variant",t),i.setAttribute("type","button"),i.classList.add("atfb-button",`atfb-button--${t}`),i.addEventListener("click",e);for(const r of o)r&&i.append(r);return i}return a("button",{class:`atfb-button atfb-button--${t}`,type:"button",on:{click:e},children:o})}function E(s){return typeof customElements<"u"&&!!customElements.get(s)}const B=["os-button","os-checkbox-label","os-number-field","os-select","os-option","os-segmented","os-segment","os-color-field","os-empty-state"];let $=null;function U(){if($)return $;const s=window.wp?.os;return $=s?.loadComponents?s.loadComponents(B).catch(()=>{}):Promise.resolve(),$}async function G(s,e=""){const t=window.wp?.os;return t?.confirm?t.confirm({title:e,message:s,danger:!0}):window.confirm(s)}function x(s,e="",t="info"){const n=window.wp?.os;if(n?.notify){n.notify({title:s,body:e,type:t});return}console.info(`[AllTerrain Forms] ${s}${e?`: ${e}`:""}`)}function W(s){const e=s.closest(".os-window__body");e&&(e.addEventListener("scroll",()=>{e.scrollTop&&(e.scrollTop=0),e.scrollLeft&&(e.scrollLeft=0)},{passive:!0}),e.scrollTop=0,e.scrollLeft=0)}const O=[{key:"detractors",label:"Detractors",hint:"0–6"},{key:"passives",label:"Passives",hint:"7–8"},{key:"promoters",label:"Promoters",hint:"9–10"}],q=50;function L(s){const e=Math.floor(s.min),t=Math.ceil(s.max);if(!Number.isFinite(e)||!Number.isFinite(t)||tMath.min(i-1,Math.max(0,Math.floor((h-e)/o))),l=[];for(let h=0;ht.id===e)?this.formId=e:this.formId||(this.formId=this.forms[0].id),await this.load()}async showForm(e){this.formId=e,this.dimension="",await this.load()}async load(){try{this.report=await f.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){_(this.bar),_(this.body),this.body.append(a("p",{class:"atfa__empty",text:e}))}renderBar(){_(this.bar),this.bar.append(N(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(a("span",{class:"atfa__bar-label",text:"Group by"}),N(this.dimension,e.map(t=>({value:t.id,label:t.label})),t=>{this.dimension=t,this.load()}))}render(){const e=this.report;if(_(this.body),!!e){if(this.body.append(this.kpis(e)),e.submissions<1&&e.sampled<1)this.body.append(a("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 a("div",{class:"atfa-kpis",children:t.map(n=>a("div",{class:"atfa-kpi",children:[a("span",{class:"atfa-kpi__value",text:n.value}),a("span",{class:"atfa-kpi__label",text:n.label}),n.hint?a("span",{class:"atfa-kpi__hint",text:n.hint}):null]}))})}timeline(e){const t=e.timeline,n=t.reduce((i,r)=>Math.max(i,r.count),0),o=t.reduce((i,r)=>i+r.count,0);return this.panel("When they answered",`${o} in the last ${t.length} days · busiest day ${n}`,[a("div",{class:"atfa-timeline",children:t.map(i=>a("div",{class:`atfa-timeline__day${i.count?"":" is-empty"}`,attrs:{style:`height:${n?Math.max(2,i.count/n*100):0}%`,title:`${i.date}: ${i.count}`}}))}),a("div",{class:"atfa-timeline__axis",children:[a("span",{text:t[0]?.date??""}),a("span",{text:t[t.length-1]?.date??""})]})])}npsPanel(e){const t=e.nps;if(!t)return a("div");const n=o=>t.responses?o/t.responses*100:0;return this.panel(e.label,`${t.responses} answers · Net Promoter Score`,[a("div",{class:"atfa-nps",children:[a("div",{class:`atfa-nps__score is-${t.score>=50?"great":t.score>=0?"ok":"poor"}`,children:[a("span",{class:"atfa-nps__number",text:`${t.score>0?"+":""}${t.score}`}),a("span",{class:"atfa-nps__caption",text:"NPS"})]}),a("div",{class:"atfa-nps__detail",children:[a("div",{class:"atfa-nps__bar",children:O.map(o=>a("div",{class:`atfa-nps__band is-${o.key}`,attrs:{style:`width:${n(t[o.key])}%`,title:`${o.label} (${o.hint}): ${t[o.key]}`}}))}),a("div",{class:"atfa-nps__legend",children:O.map(o=>a("span",{class:`atfa-nps__key is-${o.key}`,children:[a("strong",{text:String(t[o.key])}),a("span",{text:` ${o.label} `}),a("small",{text:o.hint})]}))})]})]})])}breakdownPanel(e){const t=e.groups[0]?.metrics??[];if(!t.length)return a("div");const n=new Map;for(const r of e.groups)for(const l of r.metrics)n.set(l.id,Math.max(n.get(l.id)??0,l.mean));const o=a("tr",{children:[a("th",{text:e.label,attrs:{scope:"col"}}),a("th",{text:"Answers",attrs:{scope:"col"}}),...t.map(r=>a("th",{text:r.label,attrs:{scope:"col"}}))]}),i=e.groups.map(r=>a("tr",{children:[a("th",{class:"atfa-table__label",text:r.label,attrs:{scope:"row"}}),a("td",{class:"atfa-table__count",text:String(r.count)}),...r.metrics.map(l=>a("td",{children:[a("div",{class:"atfa-meter",children:[a("div",{class:"atfa-meter__fill",attrs:{style:`width:${l.mean/(n.get(l.id)||1)*100}%`}})]}),a("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()}`,"",[a("table",{class:"atfa-table",children:[a("thead",{children:[o]}),a("tbody",{children:i})]})])}questions(e){return a("div",{class:"atfa-questions",children:e.fields.map(t=>this.question(t,e.sampled))})}question(e,t){const n=[];if(e.choices.length){const o=e.choices.reduce((i,r)=>Math.max(i,r.count),0);n.push(a("ul",{class:"atfa-bars",children:e.choices.map(i=>a("li",{class:"atfa-bars__row",children:[a("span",{class:"atfa-bars__label",text:i.label}),a("div",{class:"atfa-meter",children:[a("div",{class:"atfa-meter__fill",attrs:{style:`width:${o?i.count/o*100:0}%`}})]}),a("span",{class:"atfa-bars__value",text:`${i.count} ${e.answered?Math.round(i.count/e.answered*100):0}%`})]}))}))}return e.numbers&&n.push(this.histogram(e)),n.length||n.push(a("p",{class:"atfa-question__note",text:`${e.answered} of ${e.of??t} ${e.unit??"people"} answered this.`})),this.panel(e.label,this.questionSummary(e,t),n)}questionSummary(e,t){const n=[`${e.rate}% answered`];return e.numbers&&n.push(`mean ${e.numbers.mean}`,`median ${e.numbers.median}`),n.push(`${e.answered}/${e.of??t}`),n.join(" · ")}histogram(e){const t=e.numbers;if(!t)return a("div");const n=L(t),o=n.reduce((i,r)=>Math.max(i,r.count),0);return a("div",{class:"atfa-hist",children:n.map(i=>a("div",{class:`atfa-hist__col${i.holdsMean?" is-mean":""}`,attrs:{title:`${i.label}: ${i.count}`},children:[a("span",{class:"atfa-hist__count",text:i.count?String(i.count):""}),a("div",{class:"atfa-hist__bar",attrs:{style:`height:${o?Math.max(2,i.count/o*100):0}%`}}),a("span",{class:"atfa-hist__tick",text:i.label})]}))})}demoPanel(){const e=this.demo,t=a("p",{class:"atfa-demo__status",text:this.demoSummary(e)}),n=I("Fill with demo data",async()=>{await this.seedAll(t)}),o=I("Remove demo data",async()=>{if(await G("Delete the demo survey and every entry it generated?","Remove demo data")){t.textContent="Removing…";try{const i=await f.demoRemove();x("Demo data removed",`${i.entries} entries and ${i.forms} form.`)}catch{x("Could not remove the demo data","","error")}await this.refreshDemo(t)}});return a("div",{class:"atfa-demo",attrs:{"data-atfa-demo":""},children:[a("h3",{class:"atfa-demo__title",text:"Developer"}),a("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,a("div",{class:"atfa-demo__actions",children:[n,o]})]})}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,n=0;for(;;){try{const o=await f.demoSeed();if(this.demo=o,n=0,e.textContent=`Generating… ${o.entries} of ${o.target}`,o.remaining<1){x("Demo data ready",`${o.entries} submissions.`);break}}catch{if(n+=1,n>=3){e.textContent="Stopped early. Press it again to carry on from here.",x("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,n){return a("section",{class:"atfa-panel",children:[a("header",{class:"atfa-panel__head",children:[a("h3",{class:"atfa-panel__title",text:e}),t?a("p",{class:"atfa-panel__note",text:t}):null]}),...n]})}revealDemo(){this.body.querySelector("[data-atfa-demo]")?.scrollIntoView({behavior:"smooth",block:"start"})}}let b=null;document.addEventListener("atf-open-analytics-form",s=>{const e=Number(s.detail?.formId??0);e&&b?.showForm(e)});function S(){const s=document.querySelector("[data-atfa-root]");!s||s.dataset.atfaMounted||(s.dataset.atfaMounted="1",W(s),b=new H(s),b.start())}let A=!1;return document.addEventListener("atf-open-demo-panel",()=>{A=!0,window.setTimeout(()=>{b?.revealDemo(),A=!1},600)}),document.addEventListener("os-window-content-loaded",()=>{S(),A&&window.setTimeout(()=>b?.revealDemo(),400)}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",S):S(),p.MAX_HISTOGRAM_BUCKETS=q,p.histogramBuckets=L,p.mountAnalytics=S,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({}); +var allTerrainFormsAnalytics=function(p){"use strict";function F(){return window.wp?.os??null}const c=window.allTerrainForms;class y extends Error{constructor(e,t,n=""){super(e),this.name="ApiError",this.status=t,this.code=n}}function T(s,e){return s.includes("?")?`${s}${e.replace("?","&")}`:`${s}${e}`}async function M(s,e={}){if(!c?.restUrl)throw new y("AllTerrain Forms is not configured on this page.",0);const t=T(c.restUrl,s),n={"Content-Type":"application/json",...e.headers??{}};c.nonce&&(n["X-WP-Nonce"]=c.nonce);const o=F(),r=await(o?.fetch?(l,m)=>o.fetch(l,m,{source:"allterrain-forms"}):(l,m)=>fetch(l,m))(t,{credentials:"same-origin",...e,headers:n});if(!r.ok){let l=`Request failed with status ${r.status}.`,m="";try{const h=await r.json();l=h.message??l,m=h.code??""}catch{}throw new y(l,r.status,m)}if(r.status!==204)return await r.json()}const d=s=>M(s);async function D(s){if(!c?.wpRestUrl)throw new y("AllTerrain Forms is not configured on this page.",0);const e=c.nonce?{"X-WP-Nonce":c.nonce}:{},t=F(),n=T(c.wpRestUrl,s),o=t?.fetch?await t.fetch(n,{credentials:"same-origin",headers:e},{source:"allterrain-forms"}):await fetch(n,{credentials:"same-origin",headers:e});if(!o.ok)throw new y(`Request failed with status ${o.status}.`,o.status);return await o.json()}function j(s){if(!s||!s.includes("&"))return s;const e=document.createElement("textarea");return e.innerHTML=s,e.value}const u=(s,e)=>M(s,{method:"POST",body:JSON.stringify(e)}),w=s=>M(s,{method:"DELETE"});function C(s){const e=new URLSearchParams;for(const[n,o]of Object.entries(s))o===void 0||o===""||o===!1||e.set(n,String(o));const t=e.toString();return t?`?${t}`:""}function g(s){const e=s?.schema?.settings;return e&&(!e.themeOverrides||Array.isArray(e.themeOverrides))&&(e.themeOverrides={...e.themeOverrides}),s}const f={config:()=>d("/config"),listForms:()=>d("/forms"),getForm:s=>d(`/forms/${s}`).then(g),createForm:s=>u("/forms",s).then(g),updateForm:(s,e)=>u(`/forms/${s}`,e).then(g),duplicateForm:s=>u(`/forms/${s}/duplicate`,{}).then(g),deleteForm:s=>w(`/forms/${s}`),preview:(s,e)=>u(`/forms/${s}/preview`,e),pages:()=>D("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(s=>s.map(e=>({id:e.id,title:j(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=>u("/demo",s?{count:s}:{}),demoRemove:()=>w("/demo"),listEntries:s=>d(`/entries${C(s)}`),getEntry:s=>d(`/entries/${s}`),updateEntry:(s,e)=>u(`/entries/${s}`,e),deleteEntry:s=>w(`/entries/${s}`),exportEntries:s=>d(`/entries/export${C(s)}`),listThemes:()=>d("/themes"),saveTheme:s=>u("/themes",s),deleteTheme:s=>w(`/themes/${s}`)};function P(s){return`allterrain-forms/requested-form-${s}`}function R(s){try{const e=window.sessionStorage.getItem(P(s));return window.sessionStorage.removeItem(P(s)),Number(e)||0}catch{return 0}}function a(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[n,o]of Object.entries(e.attrs??{}))o===void 0||o===!1||t.setAttribute(n,o===!0?"":String(o));Object.assign(t.style,e.style??{});for(const[n,o]of Object.entries(e.on??{}))t.addEventListener(n,o);for(const n of e.children??[])n==null||n===!1||t.append(n);return t}function _(s){s.replaceChildren()}function N(s,e,t){if(E("os-select")&&E("os-option")){const n=document.createElement("os-select");n.setAttribute("value",s),n.classList.add("atfb-field");for(const o of e){const i=document.createElement("os-option");i.setAttribute("value",o.value),i.textContent=o.label,n.append(i)}return n.addEventListener("os-pick",o=>{t(String(o.detail?.value??""))}),n}return a("select",{class:"atfb-input atfb-select",on:{change:n=>t(n.target.value)},children:e.map(n=>a("option",{value:n.value,text:n.label,attrs:{selected:n.value===s}}))})}function I(s,e,t="secondary",n){const o=[null,a("span",{text:s})];if(E("os-button")){const i=document.createElement("os-button");i.setAttribute("variant",t),i.setAttribute("type","button"),i.classList.add("atfb-button",`atfb-button--${t}`),i.addEventListener("click",e);for(const r of o)r&&i.append(r);return i}return a("button",{class:`atfb-button atfb-button--${t}`,type:"button",on:{click:e},children:o})}function E(s){return typeof customElements<"u"&&!!customElements.get(s)}const B=["os-button","os-checkbox-label","os-number-field","os-select","os-option","os-segmented","os-segment","os-color-field","os-empty-state"];let $=null;function U(){if($)return $;const s=window.wp?.os;return $=s?.loadComponents?s.loadComponents(B).catch(()=>{}):Promise.resolve(),$}async function G(s,e=""){const t=window.wp?.os;return t?.confirm?t.confirm({title:e,message:s,danger:!0}):window.confirm(s)}function x(s,e="",t="info"){const n=window.wp?.os;if(n?.notify){n.notify({title:s,body:e,type:t});return}console.info(`[AllTerrain Forms] ${s}${e?`: ${e}`:""}`)}function W(s){const e=s.closest(".os-window__body");e&&(e.addEventListener("scroll",()=>{e.scrollTop&&(e.scrollTop=0),e.scrollLeft&&(e.scrollLeft=0)},{passive:!0}),e.scrollTop=0,e.scrollLeft=0)}const O=[{key:"detractors",label:"Detractors",hint:"0–6"},{key:"passives",label:"Passives",hint:"7–8"},{key:"promoters",label:"Promoters",hint:"9–10"}],q=50;function L(s){const e=Math.floor(s.min),t=Math.ceil(s.max);if(!Number.isFinite(e)||!Number.isFinite(t)||tMath.min(i-1,Math.max(0,Math.floor((h-e)/o))),l=[];for(let h=0;ht.id===e)?this.formId=e:this.formId||(this.formId=this.forms[0].id),await this.load()}async showForm(e){this.formId=e,this.dimension="",await this.load()}async load(){try{this.report=await f.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){_(this.bar),_(this.body),this.body.append(a("p",{class:"atfa__empty",text:e}))}renderBar(){_(this.bar),this.bar.append(N(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(a("span",{class:"atfa__bar-label",text:"Group by"}),N(this.dimension,e.map(t=>({value:t.id,label:t.label})),t=>{this.dimension=t,this.load()}))}render(){const e=this.report;if(_(this.body),!!e){if(this.body.append(this.kpis(e)),e.submissions<1&&e.sampled<1)this.body.append(a("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 a("div",{class:"atfa-kpis",children:t.map(n=>a("div",{class:"atfa-kpi",children:[a("span",{class:"atfa-kpi__value",text:n.value}),a("span",{class:"atfa-kpi__label",text:n.label}),n.hint?a("span",{class:"atfa-kpi__hint",text:n.hint}):null]}))})}timeline(e){const t=e.timeline,n=t.reduce((i,r)=>Math.max(i,r.count),0),o=t.reduce((i,r)=>i+r.count,0);return this.panel("When they answered",`${o} in the last ${t.length} days · busiest day ${n}`,[a("div",{class:"atfa-timeline",children:t.map(i=>a("div",{class:`atfa-timeline__day${i.count?"":" is-empty"}`,attrs:{style:`height:${n?Math.max(2,i.count/n*100):0}%`,title:`${i.date}: ${i.count}`}}))}),a("div",{class:"atfa-timeline__axis",children:[a("span",{text:t[0]?.date??""}),a("span",{text:t[t.length-1]?.date??""})]})])}npsPanel(e){const t=e.nps;if(!t)return a("div");const n=o=>t.responses?o/t.responses*100:0;return this.panel(e.label,`${t.responses} answers · Net Promoter Score`,[a("div",{class:"atfa-nps",children:[a("div",{class:`atfa-nps__score is-${t.score>=50?"great":t.score>=0?"ok":"poor"}`,children:[a("span",{class:"atfa-nps__number",text:`${t.score>0?"+":""}${t.score}`}),a("span",{class:"atfa-nps__caption",text:"NPS"})]}),a("div",{class:"atfa-nps__detail",children:[a("div",{class:"atfa-nps__bar",children:O.map(o=>a("div",{class:`atfa-nps__band is-${o.key}`,attrs:{style:`width:${n(t[o.key])}%`,title:`${o.label} (${o.hint}): ${t[o.key]}`}}))}),a("div",{class:"atfa-nps__legend",children:O.map(o=>a("span",{class:`atfa-nps__key is-${o.key}`,children:[a("strong",{text:String(t[o.key])}),a("span",{text:` ${o.label} `}),a("small",{text:o.hint})]}))})]})]})])}breakdownPanel(e){const t=e.groups[0]?.metrics??[];if(!t.length)return a("div");const n=new Map;for(const r of e.groups)for(const l of r.metrics)n.set(l.id,Math.max(n.get(l.id)??0,l.mean));const o=a("tr",{children:[a("th",{text:e.label,attrs:{scope:"col"}}),a("th",{text:"Answers",attrs:{scope:"col"}}),...t.map(r=>a("th",{text:r.label,attrs:{scope:"col"}}))]}),i=e.groups.map(r=>a("tr",{children:[a("th",{class:"atfa-table__label",text:r.label,attrs:{scope:"row"}}),a("td",{class:"atfa-table__count",text:String(r.count)}),...r.metrics.map(l=>a("td",{children:[a("div",{class:"atfa-meter",children:[a("div",{class:"atfa-meter__fill",attrs:{style:`width:${l.mean/(n.get(l.id)||1)*100}%`}})]}),a("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()}`,"",[this.scrollable(a("table",{class:"atfa-table",children:[a("thead",{children:[o]}),a("tbody",{children:i})]}))])}questions(e){return a("div",{class:"atfa-questions",children:e.fields.map(t=>this.question(t,e.sampled))})}question(e,t){const n=[];if(e.choices.length){const o=e.choices.reduce((i,r)=>Math.max(i,r.count),0);n.push(a("ul",{class:"atfa-bars",children:e.choices.map(i=>a("li",{class:"atfa-bars__row",children:[a("span",{class:"atfa-bars__label",text:i.label}),a("div",{class:"atfa-meter",children:[a("div",{class:"atfa-meter__fill",attrs:{style:`width:${o?i.count/o*100:0}%`}})]}),a("span",{class:"atfa-bars__value",text:`${i.count} ${e.answered?Math.round(i.count/e.answered*100):0}%`})]}))}))}return e.numbers&&n.push(this.histogram(e)),n.length||n.push(a("p",{class:"atfa-question__note",text:`${e.answered} of ${e.of??t} ${e.unit??"people"} answered this.`})),this.panel(e.label,this.questionSummary(e,t),n)}questionSummary(e,t){const n=[`${e.rate}% answered`];return e.numbers&&n.push(`mean ${e.numbers.mean}`,`median ${e.numbers.median}`),n.push(`${e.answered}/${e.of??t}`),n.join(" · ")}histogram(e){const t=e.numbers;if(!t)return a("div");const n=L(t),o=n.reduce((i,r)=>Math.max(i,r.count),0);return this.scrollable(a("div",{class:"atfa-hist",children:n.map(i=>a("div",{class:`atfa-hist__col${i.holdsMean?" is-mean":""}`,attrs:{title:`${i.label}: ${i.count}`},children:[a("span",{class:"atfa-hist__count",text:i.count?String(i.count):""}),a("div",{class:"atfa-hist__bar",attrs:{style:`height:${o?Math.max(2,i.count/o*100):0}%`}}),a("span",{class:"atfa-hist__tick",text:i.label})]}))}))}scrollable(e){return a("div",{class:"atfa-scroll",children:[e]})}demoPanel(){const e=this.demo,t=a("p",{class:"atfa-demo__status",text:this.demoSummary(e)}),n=I("Fill with demo data",async()=>{await this.seedAll(t)}),o=I("Remove demo data",async()=>{if(await G("Delete the demo survey and every entry it generated?","Remove demo data")){t.textContent="Removing…";try{const i=await f.demoRemove();x("Demo data removed",`${i.entries} entries and ${i.forms} form.`)}catch{x("Could not remove the demo data","","error")}await this.refreshDemo(t)}});return a("div",{class:"atfa-demo",attrs:{"data-atfa-demo":""},children:[a("h3",{class:"atfa-demo__title",text:"Developer"}),a("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,a("div",{class:"atfa-demo__actions",children:[n,o]})]})}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,n=0;for(;;){try{const o=await f.demoSeed();if(this.demo=o,n=0,e.textContent=`Generating… ${o.entries} of ${o.target}`,o.remaining<1){x("Demo data ready",`${o.entries} submissions.`);break}}catch{if(n+=1,n>=3){e.textContent="Stopped early. Press it again to carry on from here.",x("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,n){return a("section",{class:"atfa-panel",children:[a("header",{class:"atfa-panel__head",children:[a("h3",{class:"atfa-panel__title",text:e}),t?a("p",{class:"atfa-panel__note",text:t}):null]}),...n]})}revealDemo(){this.body.querySelector("[data-atfa-demo]")?.scrollIntoView({behavior:"smooth",block:"start"})}}let b=null;document.addEventListener("atf-open-analytics-form",s=>{const e=Number(s.detail?.formId??0);e&&b?.showForm(e)});function S(){const s=document.querySelector("[data-atfa-root]");!s||s.dataset.atfaMounted||(s.dataset.atfaMounted="1",W(s),b=new H(s),b.start())}let A=!1;return document.addEventListener("atf-open-demo-panel",()=>{A=!0,window.setTimeout(()=>{b?.revealDemo(),A=!1},600)}),document.addEventListener("os-window-content-loaded",()=>{S(),A&&window.setTimeout(()=>b?.revealDemo(),400)}),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",S):S(),p.MAX_HISTOGRAM_BUCKETS=q,p.histogramBuckets=L,p.mountAnalytics=S,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({}); diff --git a/src/analytics.ts b/src/analytics.ts index 6299e43..6758bbb 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -513,10 +513,15 @@ class AnalyticsWindow { ); return this.panel( `Broken down by ${ breakdown.label.toLowerCase() }`, '', [ - el( 'table', { - class: 'atfa-table', - children: [ el( 'thead', { children: [ head ] } ), el( 'tbody', { children: rows } ) ], - } ), + // Six question columns do not fit a narrow window, and a table never + // shrinks below its content — it overflows. Wide content scrolls + // inside its own container; it must never bleed past the panel. + this.scrollable( + el( 'table', { + class: 'atfa-table', + children: [ el( 'thead', { children: [ head ] } ), el( 'tbody', { children: rows } ) ], + } ) + ), ] ); } @@ -617,7 +622,11 @@ class AnalyticsWindow { const buckets = histogramBuckets( numbers ); const peak = buckets.reduce( ( most, bucket ) => Math.max( most, bucket.count ), 0 ); - return el( 'div', { + // A wide-ranging answer — forty hour buckets — has more columns than a + // narrow window has pixels, and each column's count label sets a width + // it cannot shrink below. The histogram scrolls sideways rather than + // painting past the panel. + return this.scrollable( el( 'div', { class: 'atfa-hist', children: buckets.map( ( bucket ) => el( 'div', { @@ -633,7 +642,19 @@ class AnalyticsWindow { ], } ) ), - } ); + } ) ); + } + + /** + * Wraps something wider than the window in a sideways-scrolling strip. + * + * The one honest answer to content with a real minimum width: a table or a + * histogram that cannot shrink must scroll within its own panel, because + * the alternative — seen in the wild — is bars painting straight through + * the panel border and off the edge of the window. + */ + private scrollable( child: HTMLElement ): HTMLElement { + return el( 'div', { class: 'atfa-scroll', children: [ child ] } ); } /**