@evotecit/canopyx 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CanopyX.Html/Assets/canopyx-flow-checks.css +24 -0
  2. package/CanopyX.Html/Assets/canopyx-flow-checks.js +13 -0
  3. package/CanopyX.Html/Assets/canopyx-flow-command.css +14 -0
  4. package/CanopyX.Html/Assets/canopyx-flow-command.js +19 -0
  5. package/CanopyX.Html/Assets/canopyx-flow-consent.css +6 -0
  6. package/CanopyX.Html/Assets/canopyx-flow-consent.js +8 -0
  7. package/CanopyX.Html/Assets/canopyx-flow-dialog.css +14 -0
  8. package/CanopyX.Html/Assets/canopyx-flow-dialog.js +3 -0
  9. package/CanopyX.Html/Assets/canopyx-flow-drafts.css +17 -0
  10. package/CanopyX.Html/Assets/canopyx-flow-drafts.js +21 -0
  11. package/CanopyX.Html/Assets/canopyx-flow-endpoint.js +6 -0
  12. package/CanopyX.Html/Assets/canopyx-flow-entry.css +1 -0
  13. package/CanopyX.Html/Assets/canopyx-flow-entry.js +6 -2
  14. package/CanopyX.Html/Assets/canopyx-flow-host.css +5 -0
  15. package/CanopyX.Html/Assets/canopyx-flow-host.js +4 -0
  16. package/CanopyX.Html/Assets/canopyx-flow-links.js +6 -0
  17. package/CanopyX.Html/Assets/canopyx-flow-module.mjs +6 -2
  18. package/CanopyX.Html/Assets/canopyx-flow-options.js +25 -0
  19. package/CanopyX.Html/Assets/canopyx-flow-pick.css +19 -0
  20. package/CanopyX.Html/Assets/canopyx-flow-pick.js +9 -0
  21. package/CanopyX.Html/Assets/canopyx-flow-preview.css +11 -0
  22. package/CanopyX.Html/Assets/canopyx-flow-preview.js +9 -0
  23. package/CanopyX.Html/Assets/canopyx-flow-records.css +9 -0
  24. package/CanopyX.Html/Assets/canopyx-flow-records.js +12 -0
  25. package/CanopyX.Html/Assets/canopyx-flow-tasks.css +29 -0
  26. package/CanopyX.Html/Assets/canopyx-flow-tasks.js +20 -0
  27. package/CanopyX.Html/Assets/canopyx-flow.js +48 -45
  28. package/CanopyX.Html/Assets/canopyx-grid-module.mjs +11 -6
  29. package/CanopyX.Html/Assets/canopyx-grid.css +1 -0
  30. package/CanopyX.Html/Assets/canopyx-grid.js +11 -6
  31. package/CanopyX.Html/Assets/canopyx-interactive.js +1 -1
  32. package/CanopyX.Html/Assets/canopyx-labels-de.js +74 -0
  33. package/CanopyX.Html/Assets/canopyx-labels-es.js +74 -0
  34. package/CanopyX.Html/Assets/canopyx-labels-fr.js +74 -0
  35. package/CanopyX.Html/Assets/canopyx-labels-it.js +74 -0
  36. package/CanopyX.Html/Assets/canopyx-labels-pl.js +74 -0
  37. package/CanopyX.Html/Assets/canopyx-module.mjs +1 -1
  38. package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +11 -6
  39. package/CanopyX.Html/Assets/canopyx-reporting.css +1 -0
  40. package/CanopyX.Html/Assets/canopyx-reporting.js +11 -6
  41. package/CanopyX.Html/Assets/canopyx-tree-tools.js +4 -3
  42. package/CanopyX.Html/Assets/canopyx.js +1 -1
  43. package/JavaScript/flow.d.ts +180 -9
  44. package/JavaScript/grid.d.ts +2 -0
  45. package/JavaScript/locales/de.mjs +74 -0
  46. package/JavaScript/locales/es.mjs +74 -0
  47. package/JavaScript/locales/fr.mjs +74 -0
  48. package/JavaScript/locales/it.mjs +74 -0
  49. package/JavaScript/locales/pl.mjs +74 -0
  50. package/README.md +2 -2
  51. package/package.json +3 -1
@@ -0,0 +1,12 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"f3c09661bdffb8d0",requires:["flow"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowRecordActions:"Actions",flowRecordCell:"{column}, row {row}",flowRecordRemove:"Remove row {row}",flowRecordAdd:"Add row",flowRecordsEmpty:"No rows yet.",flowRecordProblem:"Row {row}: {message}",flowRecordsCount:"[one]{count} row[other]{count} rows"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_RECORD_TYPES=["text","number","date","time","select","toggle"];function flowRecordColumns(raw,bad){if(!Array.isArray(raw)||!raw.length||raw.length>16)bad("needs 1 to 16 columns");const ids=new Set();return raw.map(column=>{flowKeys(column,["id","title","type","required","options","validation","width"],"A records column");const id=flowName(column.id,"A column id"),type=column.type??"text",rules=column.validation??{};if(ids.has(id))bad("has two columns named "+id);ids.add(id);if(!FLOW_RECORD_TYPES.includes(type))bad("has a column of an unknown type");flowKeys(rules,["min","max","minLength","maxLength","pattern","message"],"The validation of column "+id);if((rules.min!==undefined||rules.max!==undefined)&&type!=="number"||[rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&type!=="text")bad("has checks its column type does not take");if((column.options!==undefined)!==(type==="select")||type==="select"&&!(Array.isArray(column.options)&&column.options.length))bad("needs options as a select column only");if(column.width!==undefined&&(!Number.isInteger(column.width)||column.width<60||column.width>480))bad("needs a column width of 60 to 480 pixels");const required=flowFlag(column.required,"required"),shared=formRuleSpec({},{required,min:rules.min,max:rules.max,minLength:rules.minLength,maxLength:rules.maxLength,pattern:rules.pattern,message:rules.message},bad);return Object.freeze({id,title:flowString(column.title,200,"A column title",true),type,required,width:column.width??null,options:type==="select"?flowOptions(column.options,bad):[],spec:Object.freeze({id,pattern:shared.pattern,rules:shared.rules})})})}
2
+ function flowCellFits(column,value){if(value===null)return true;switch(column.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return flowFits({type:"date"},value);case "time":return flowFits({type:"time"},value);case "select":return column.options.some(option=>option.value===value);default:return typeof value==="string"&&value.length<=4096}}
3
+ FLOW_HOST.records={columns:flowRecordColumns,fits:(field,value)=>Array.isArray(value)&&value.length<=1000&&value.every(row=>flowIs(row)&&
4
+ Object.keys(row).every(key=>field.columns.some(column=>column.id===key))&&field.columns.every(column=>flowCellFits(column,row[column.id]??null))),input:(context,field,view)=>flowRecordsInput(context,field,view),problem:(view,field,rows)=>flowRecordsProblem(view,field,rows),display:(view,value)=>fill(plural(view.labels.flowRecordsCount,value.length,view.lang),{count:numberFormat(view.lang,0,0).format(value.length)})};function flowRecordsInput(context,field,view){const labels=view.labels,group=h("div","cx-flow-records"),table=h("table","cx-flow-records-table"),head=h("thead"),body=h("tbody");const add=h("button","cx-flow-btn cx-flow-list-add"),rows=[],max=field.items.max??1000;group.setAttribute("role","group");const headRow=h("tr");for(const column of field.columns){const th=h("th");th.scope="col";setText(th,column.title);if(column.width)th.style.width=column.width+"px";if(column.required)th.classList.add("cx-flow-records-required");headRow.appendChild(th)}
5
+ const actions=h("th","cx-flow-records-actions"),actionsText=h("span","cx-sr-only");actions.scope="col";setText(actionsText,labels.flowRecordActions);actions.appendChild(actionsText);headRow.appendChild(actions);head.appendChild(headRow);table.append(head,body);const changed=()=>group.dispatchEvent(new Event("change",{bubbles:true}));const cellText=(column,value)=>value==null?"":column.type==="number"?numberFormat(context.locale,0,20).format(value):
6
+ column.type==="select"?column.options.find(option=>option.value===value)?.label??String(value):String(value);const renumber=()=>rows.forEach((row,index)=>{row.cells.forEach(({column,instance})=>(instance.control||instance.element).setAttribute("aria-label",fill(labels.flowRecordCell,{column:column.title,row:index+1})));row.remove.setAttribute("aria-label",fill(labels.flowRecordRemove,{row:index+1}))});const paintTools=()=>{add.disabled=context.readOnly||rows.length>=max};const addRow=(values,focus)=>{const tr=h("tr","cx-flow-records-row"),row={tr,cells:[],abort:new AbortController()};for(const column of field.columns){const td=h("td"),value=values?.[column.id]??null;const cellContext=Object.freeze({id:context.id+"-r"+rows.length+"-"+column.id,value,text:cellText(column,value),readOnly:context.readOnly,row:Object.freeze({id:field.id,cells:Object.freeze({})}),column:Object.freeze({id:column.id,title:column.title,kind:column.type==="number"?"number":"text"}),editor:Object.freeze({type:column.type}),options:column.options.length?column.options:null,rules:column.spec.rules,labels,locale:context.locale,timeZone:"utc",mode:"flow",initial:undefined,signal:row.abort.signal,lookupMinLength:1,commit:changed,cancel:()=>{},lookup:()=>[],remember:changed});const instance=EDIT_EDITORS[column.type].create(cellContext),control=instance.control||instance.element;control.classList.add("cx-flow-input");if(context.readOnly){control.disabled=control.tagName==="SELECT"||control.type==="checkbox";control.readOnly=true}
7
+ td.appendChild(instance.element);tr.appendChild(td);row.cells.push({column,instance})}
8
+ const cell=h("td","cx-flow-records-actions"),remove=h("button","cx-flow-icon-btn");remove.type="button";setText(remove,"×");remove.hidden=context.readOnly;remove.addEventListener("click",()=>{const at=rows.indexOf(row);rows.splice(at,1);row.abort.abort();row.cells.forEach(({instance})=>instance.destroy?.());tr.remove();renumber();paintTools();changed();const next=rows[Math.min(at,rows.length-1)];(next?next.cells[0].instance.control||next.cells[0].instance.element:add).focus()});cell.appendChild(remove);tr.appendChild(cell);row.remove=remove;rows.push(row);body.appendChild(tr);renumber();paintTools();if(focus)(row.cells[0].instance.control||row.cells[0].instance.element).focus();return row};for(const values of Array.isArray(context.value)?context.value:[])addRow(values,false);add.type="button";setText(add,"+ "+labels.flowRecordAdd);add.setAttribute("aria-label",fill(labels.flowAddItem,{field:field.label}));add.hidden=context.readOnly;add.addEventListener("click",()=>{addRow(null,true);changed()});const empty=h("p","cx-flow-records-empty");setText(empty,labels.flowRecordsEmpty);const paintEmpty=()=>{empty.hidden=rows.length>0;table.hidden=!rows.length};group.addEventListener("change",paintEmpty);paintEmpty();const scroller=h("div","cx-flow-records-scroll");scroller.appendChild(table);group.append(scroller,empty,add);paintTools();return{element:group,control:group,group:true,focus:()=>(rows[0]?rows[0].cells[0].instance.control||rows[0].cells[0].instance.element:add).focus(),error:()=>{for(const[index,row]of rows.entries())for(const{column,instance}of row.cells){const problem=instance.error?.();if(problem)return fill(labels.flowRecordProblem,{row:index+1,message:problem})}
9
+ return null},getValue:()=>rows.map(row=>Object.fromEntries(row.cells.map(({column,instance})=>{const value=instance.getValue();return[column.id,value===""||value===undefined?null:value]}))),destroy:()=>rows.forEach(row=>{row.abort.abort();row.cells.forEach(({instance})=>instance.destroy?.())})}}
10
+ function flowRecordsProblem(view,field,rows){const labels=view.labels,title=field.label,items=field.items;if(!rows.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(items.min!==undefined&&rows.length<items.min)return fill(labels.flowItemsMin,{field:title,min:items.min});if(items.max!==undefined&&rows.length>items.max)return fill(labels.flowItemsMax,{field:title,max:items.max});for(const[index,row]of rows.entries())for(const column of field.columns){const value=row[column.id]??null,shown=bound=>typeof bound==="number"?numberFormat(view.lang,0,20).format(bound):String(bound);const problem=column.type==="toggle"?null:formCheck(column.spec,value,column.type==="number"?"number":"text",column.title,labels,shown);if(problem)return fill(labels.flowRecordProblem,{row:index+1,message:problem})}
11
+ return null}
12
+ installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-records",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,29 @@
1
+ /* The flow-tasks part: the hub of a task list. */
2
+ .cx-flow-tasks > .cx-flow-rail{display:none;}
3
+ .cx-flow-hub-progress{margin-top:var(--cx-size-4px);color:var(--cx-text-2);font-size:var(--cx-size-13d5px);}
4
+ .cx-flow-step > .cx-flow-task-groups{margin-top:var(--cx-flow-gap);}
5
+ .cx-flow-task-group + .cx-flow-task-group{margin-top:var(--cx-size-16px);}
6
+ .cx-flow-task-list{list-style:none;padding:0;border-top:var(--cx-size-1px) solid var(--cx-line);}
7
+ .cx-flow-task-group > .cx-flow-section-title{padding-bottom:var(--cx-size-6px);}
8
+ .cx-flow-task{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--cx-size-2px) var(--cx-size-12px);padding:var(--cx-size-4px) 0;border-bottom:var(--cx-size-1px) solid var(--cx-line);}
9
+ .cx-flow-task-open{display:flex;flex-direction:column;align-items:flex-start;gap:var(--cx-size-2px);min-height:var(--cx-size-44px);padding:var(--cx-size-6px) var(--cx-size-8px);margin-inline-start:calc(var(--cx-size-8px) * -1);
10
+ border:0;border-radius:var(--cx-radius);background:transparent;color:var(--cx-accent-text);font:inherit;text-align:start;cursor:pointer;}
11
+ .cx-flow-task-open:hover{background:var(--cx-hover-bg);}
12
+ .cx-flow-task-open:focus-visible{outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-1px);}
13
+ .cx-flow-task-open[aria-disabled="true"]{color:var(--cx-text-2);cursor:default;}
14
+ .cx-flow-task-open[aria-disabled="true"]:hover{background:transparent;}
15
+ .cx-flow-task-title{font-weight:600;text-decoration:underline;text-underline-offset:var(--cx-size-2px);}
16
+ .cx-flow-task-open[aria-disabled="true"] .cx-flow-task-title{text-decoration:none;}
17
+ .cx-flow-task-summary{font-size:var(--cx-size-13px);color:var(--cx-text-2);}
18
+ .cx-flow-task-state{display:inline-flex;align-items:center;gap:var(--cx-size-6px);font-size:var(--cx-size-13px);color:var(--cx-muted);white-space:nowrap;}
19
+ .cx-flow-task-mark{display:inline-flex;align-items:center;justify-content:center;min-width:var(--cx-size-18px);height:var(--cx-size-18px);border-radius:50%;font-size:var(--cx-size-12px);font-weight:700;}
20
+ .cx-flow-task[data-cx-state="complete"] .cx-flow-task-mark{color:var(--cx-tone-success);background:var(--cx-tone-success-bg);}
21
+ .cx-flow-task[data-cx-state="complete"] .cx-flow-task-state{color:var(--cx-text-2);}
22
+ .cx-flow-task[data-cx-state="needsAction"] :is(.cx-flow-task-state,.cx-flow-task-mark){color:var(--cx-tone-warning);}
23
+ .cx-flow-task[data-cx-state="needsAction"] .cx-flow-task-mark{background:var(--cx-tone-warning-bg);}
24
+ .cx-flow-task[data-cx-state="error"] :is(.cx-flow-task-state,.cx-flow-task-mark){color:var(--cx-tone-danger);}
25
+ .cx-flow-task[data-cx-state="error"] .cx-flow-task-mark{background:var(--cx-tone-danger-bg);}
26
+ .cx-flow-task-blockers{grid-column:1 / -1;margin:0 0 var(--cx-size-4px);padding-inline-start:var(--cx-size-18px);font-size:var(--cx-size-13px);color:var(--cx-text-2);}
27
+ .cx-flow-hub-note{font-size:var(--cx-size-13px);color:var(--cx-muted);}
28
+ .cx-flow-next[aria-disabled="true"]{opacity:.55;cursor:default;}
29
+ @media (forced-colors:active){.cx-flow-task-mark{forced-color-adjust:none;border:var(--cx-size-1px) solid CanvasText;}}
@@ -0,0 +1,20 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"147dd31638a9cb94",requires:["flow","flow-host"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowTasks:"Tasks",flowTasksProgress:"{done} of {count} tasks complete",flowTaskNeedsAction:"Needs action",flowTaskBlocked:"Cannot start yet",flowTasksReview:"Review and submit",flowTasksIncomplete:"Complete the required tasks first.",flowBackToTasks:"Back to tasks",flowSaveAndReturn:"Save and return"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_TASK_STATES=["complete","needsAction","blocked","notStarted"];const FLOW_TASK_MARKS={complete:"✓",error:"!",needsAction:"!",blocked:"—",notStarted:""};function flowTaskOrder(parent,nodes){const wanted=new Set(nodes);for(const child of[...parent.children])if(!wanted.has(child))child.remove();let at=parent.firstChild;for(const node of nodes){if(node===at)at=at.nextSibling;else parent.insertBefore(node,at)}}
2
+ const{back:flowStepBack,goTo:flowStepGoTo,renderStage:flowStepStage,renderRail:flowStepRail,renderActions:flowStepActions,lastStep:flowLastStep,orientation:flowOrientation,begin:flowTaskBegin,setState:flowTaskRestore,submit:flowTaskSubmit,validateAll:flowTaskValidate}=FlowView.prototype;Object.assign(FlowView.prototype,{begin(values){this.hub??=this.model.mode==="tasks"?{id:"#tasks",type:"hub",title:null,asks:false,blocks:[],checks:[]}:null;this.hubNode=undefined;flowTaskBegin.call(this,values)},setState(state){const controller=this.statusController,result=flowTaskRestore.call(this,state);if(controller&&this.statusController===controller&&!this.destroyed&&this.step===this.hub)this.loadStatus();return result},orientation(){return this.hub?"tasks":flowOrientation.call(this)},lastStep(){return this.hub?this.step.type==="review":flowLastStep.call(this)},renderActions(){if(!this.hub||this.phase!=="form")return flowStepActions.call(this);if(this.step===this.hub){this.actions.hidden=false;if(this.hubAction?.go.parentNode!==this.actions){this.actions.replaceChildren();this.hubActions()}
3
+ this.paintHubActions();return}
4
+ flowStepActions.call(this);const back=this.actions.querySelector(".cx-flow-back");setText(back,this.labels.flowBackToTasks);back.hidden=false;if(!this.lastStep())setText(this.actions.querySelector(".cx-flow-next"),this.labels.flowSaveAndReturn)},taskButton(step){return step!==this.hub&&this.stage.querySelector('[data-cx-task="'+CSS.escape(step.id)+ '"] button')},async back(){return this.hub?!this.busy&&this.phase==="form"&&this.step!==this.hub&&this.go(this.hub,"back"):flowStepBack.call(this)},async goTo(id){if(!this.hub)return flowStepGoTo.call(this,id);if(this.busy||this.phase!=="form")return false;if(id===this.hub.id)return this.go(this.hub,"back");const target=this.visibleSteps().find(step=>step.id===id);return target&&this.taskOpens(target)?this.go(target,"forward"):false},renderStage(){if(this.hub&&this.step===this.hub&&this.phase==="form"){if(this.stage.firstChild!==this.hubNode)this.stage.replaceChildren(this.hubSection());else this.paintHub()}else flowStepStage.call(this)},renderRail(){if(!this.hub)return flowStepRail.call(this);if(!this.root)return;this.railKey=this.visibleSteps().map(step=>step.id).join();if(this.step===this.hub&&this.phase==="form")this.paintHub()},tasks(){return this.visibleSteps().filter(step=>step.type!=="review")},counted(step){return step.asks&&!step.optional},taskState(step){const host=this.stepStatus.get(step.id);if(host?.state==="blocked")return "blocked";if(this.stepErrorCount(step))return "error";if(host?.state==="needsAction")return "needsAction";if(host?.state==="complete"||this.completed.has(step.id))return "complete";return "notStarted"},taskOpens(step){return step.type==="review"?this.tasksReady():this.taskState(step)!=="blocked"},tasksReady(){return this.tasks().filter(step=>this.counted(step)).every(step=>this.taskState(step)==="complete")},async submit(){if(this.hub&&!this.tasksReady()){this.announce(this.labels.flowTasksIncomplete);return null}
5
+ return flowTaskSubmit.call(this)},async validateAll(...args){const result=await flowTaskValidate.apply(this,args);if(this.hub&&!this.tasksReady()){result.valid=false;result.errors.push({message:this.labels.flowTasksIncomplete})}
6
+ return result},hubAnnouncement(){const counted=this.tasks().filter(step=>this.counted(step));return fill(this.labels.flowTasksProgress,{done:counted.filter(step=>this.taskState(step)==="complete").length,count:counted.length})},hubSection(){const section=h("section","cx-flow-step cx-flow-hub"),heading=h("h3","cx-flow-step-title"),progress=h("p","cx-flow-hub-progress");heading.id=this.id+"-hub";heading.tabIndex=-1;setText(heading,this.labels.flowTasks);section.setAttribute("aria-labelledby",heading.id);progress.id=this.id+"-hub-progress";const lists=h("div","cx-flow-task-groups");section.append(heading,progress,lists);this.hubNode=section;this.paintHub();this.loadStatus();return section},paintHub(){const section=this.hubNode;if(!section)return;const labels=this.labels,lists=section.querySelector(".cx-flow-task-groups"),groups=new Map();for(const step of this.tasks()){const name=step.group||"";if(!groups.has(name))groups.set(name,[]);groups.get(name).push(step)}
7
+ const existing=new Map([...lists.children].map(group=>[group._cxName,group]));const rows=new Map([...lists.querySelectorAll("[data-cx-task]")].map(row=>[row.dataset.cxTask,row]));flowTaskOrder(lists,[...groups].map(([name,steps])=>{let group=existing.get(name);if(!group){group=h("section","cx-flow-task-group");group._cxName=name;if(name){const title=h("h4","cx-flow-section-title");title.id=this.id+"-tg-"+steps[0].id;setText(title,name);group.setAttribute("aria-labelledby",title.id);group.appendChild(title)}
8
+ group.appendChild(h("ol","cx-flow-task-list"))}
9
+ flowTaskOrder(group.querySelector("ol"),steps.map(step=>this.taskRow(step,rows.get(step.id))));return group}));setText(section.querySelector(".cx-flow-hub-progress"),this.hubAnnouncement());const banner=this.reviewStep()?null:this.banner;if(section._cxBanner!==banner){section.querySelector(":scope > .cx-flow-answer-banner")?.remove();if(banner)section.appendChild(this.answerBanner());section._cxBanner=banner}
10
+ if(this.step===this.hub)this.renderActions()},taskRow(step,row){const labels=this.labels,state=this.taskState(step),host=this.stepStatus.get(step.id);if(!row){row=h("li","cx-flow-task");const open=h("button","cx-flow-task-open"),title=h("span","cx-flow-task-title"),status=h("span","cx-flow-task-state"),mark=h("span","cx-flow-task-mark");const words=document.createTextNode(""),summary=h("span","cx-flow-task-summary"),blockers=h("ul","cx-flow-task-blockers");open.type="button";mark.setAttribute("aria-hidden","true");status.id=this.id+"-ts-"+step.id;summary.id=this.id+"-tsum-"+step.id;status.append(mark,words);open.append(title);row.append(open,status);open.addEventListener("click",()=>{if(open.getAttribute("aria-disabled")!=="true")this.goTo(step.id).catch(error=>this.report(error,"goTo"))});row._cx={open,title,status,mark,words,summary,blockers}}
11
+ const{open,title,status,mark,words,summary,blockers}=row._cx;row.dataset.cxTask=step.id;row.dataset.cxState=state;setText(title,step.title);words.nodeValue=state==="complete"?labels.flowStepComplete:state==="error"?fill(labels.flowStepErrors,{count:this.stepErrorCount(step)}):
12
+ state==="needsAction"?labels.flowTaskNeedsAction:state==="blocked"?labels.flowTaskBlocked:step.optional?labels.flowStepOptional:labels.flowStepNotStarted;setText(mark,FLOW_TASK_MARKS[state]||"");const notes=[status.id];if(host?.summary){setText(summary,host.summary);if(summary.parentNode!==open)open.appendChild(summary);notes.push(summary.id)}
13
+ else summary.remove();open.setAttribute("aria-describedby",notes.join(" "));if(state==="blocked")open.setAttribute("aria-disabled","true");else open.removeAttribute("aria-disabled");blockers.replaceChildren(...(host?.blockers||[]).map(text=>{const item=h("li");setText(item,text);return item}));if(blockers.children.length){if(blockers.parentNode!==row)row.appendChild(blockers)}else blockers.remove();return row},hubActions(){const labels=this.labels,review=this.reviewStep(),go=h("button","cx-flow-btn cx-primary cx-flow-next"),note=h("span","cx-flow-hub-note");go.type="button";setText(go,review?labels.flowTasksReview:this.model.submitLabel||labels.flowSubmit);note.id=this.id+"-hub-note";go.addEventListener("click",()=>{if(!this.tasksReady()){this.announce(labels.flowTasksIncomplete);return}
14
+ (review?this.goTo(review.id):this.submit()).catch(error=>this.report(error,review?"goTo":"submit"))});this.actions.append(...[this.draftButton?.(),this.cancelButton?.()].filter(Boolean),note,h("span","cx-flow-spacer"),go);this.hubAction={go,note}},paintHubActions(){const{go,note}=this.hubAction,ready=this.tasksReady();setText(note,ready?"":this.labels.flowTasksIncomplete);note.hidden=ready;if(!ready){go.setAttribute("aria-disabled","true");go.setAttribute("aria-describedby",note.id)}
15
+ else{go.removeAttribute("aria-disabled");go.removeAttribute("aria-describedby")}
16
+ go.disabled=!!this.busy;if(this.busy)go.setAttribute("aria-busy","true");else go.removeAttribute("aria-busy")},loadStatus(){if(typeof this.hooks.status!=="function")return;this.statusController?.abort();const controller=this.statusController=new AbortController();this.timed("status",signal=>this.hooks.status(this.getValues(),{signal}),controller).then(answer=>{if(this.statusController!==controller||this.destroyed||answer.cancelled||answer.failed)return;if(!flowIs(answer.value)){this.report(new Error("CanopyX: the status hook answers an object of step states."),"status");return}
17
+ for(const[id,state]of Object.entries(answer.value))if(this.model.steps.some(step=>step.id===id)){try{this.applyStepState(id,state)}catch(error){this.report(error,"status")}}
18
+ if(!this.tasksReady())this.retireOperation();this.paintHub()})},setStepState(id,state){if(!this.hub)flowFail("this flow does not use setStepState");if(!this.model.steps.some(step=>step.id===id))flowFail("there is no step '"+id+"'");this.applyStepState(id,state);if(!this.tasksReady())this.retireOperation();if(this.step===this.hub)this.paintHub();else this.renderRail()},applyStepState(id,state){if(state===null){this.stepStatus.delete(id);return}
19
+ const text=value=>typeof value==="string"&&/\S/.test(value) ? value.slice(0, 400) : null;
20
+ if(!flowIs(state)||!FLOW_TASK_STATES.includes(state.state)||state.blockers!==undefined&&!Array.isArray(state.blockers))flowFail("a step state is { state: complete | needsAction | blocked | notStarted, summary, blockers }");this.stepStatus.set(id,{state:state.state,summary:text(state.summary),blockers:(state.blockers||[]).map(text).filter(Boolean).slice(0,10)})}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-tasks",part])})(typeof window!=="undefined"?window:globalThis);
@@ -1,9 +1,8 @@
1
- (function(global){"use strict";const installed=new WeakSet();const part={version:"75b9643b282dddb7",requires:["forms"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}=context;const{editParseNumber,EDIT_EDITORS,formRuleSpec,formCheck,FORM_LABELS}=FORMS_IMPL;const FLOW_LABELS={flowSteps:"Steps",flowStepOf:"Step {current} of {count}",flowStepAnnounce:"Step {current} of {count}, {title}",flowNext:"Next",flowBack:"Back",flowSubmit:"Submit",flowSubmitting:"Submitting…",flowReviewTitle:"Check your answers",flowChange:"Change",flowChangeField:"Change {field}",flowNotProvided:"Not provided",flowOptional:"(optional)",flowStepOptional:"Optional",flowStepComplete:"Complete",flowStepCurrent:"Current step",flowStepErrors:"Errors: {count}",flowStepLocked:"Complete the earlier steps first",flowStepNotStarted:"Not started",flowErrorSummary:"Fix these before you continue",flowReady:"Ready to submit",flowNotReady:"Errors to fix: {count}",flowWarnings:"Warnings: {count}",flowChangesOnly:"Show changes only",flowNoChanges:"No answers have changed.",flowWas:"Was: {value}",flowChangedCount:"Changed answers: {count}",flowSecretSet:"Set",flowSecretKept:"Unchanged",flowSecretNotSet:"Not set",flowSecretHint:"A value is saved. Leave blank to keep it.",flowSecretShow:"Show",flowSecretHide:"Hide",flowSecretReveal:"Show {field}",flowOn:"On",flowOff:"Off",flowAdd:"Add",flowAddItem:"Add to {field}",flowRemoveItem:"Remove {item}",flowItemLabel:"{field}, item {index}",flowSelectAll:"Select all",flowSelectNone:"Clear",flowSelectedCount:"{count} of {total} selected",flowUnit:"Unit",flowUnitSeconds:"seconds",flowUnitMinutes:"minutes",flowUnitHours:"hours",flowUnitDays:"days",flowSeconds:"[one]{count} second[other]{count} seconds",flowMinutes:"[one]{count} minute[other]{count} minutes",flowHours:"[one]{count} hour[other]{count} hours",flowDays:"[one]{count} day[other]{count} days",flowDurationInvalid:"{field} must be a whole number.",flowItemsMin:"{field} needs at least {min}.",flowItemsMax:"{field} allows at most {max}.",flowItemEmail:"“{item}” is not an email address.",flowItemHost:"“{item}” is not a host name.",flowItemIp:"“{item}” is not an IP address.",flowItemCidr:"“{item}” is not a network range.",flowItemUrl:"“{item}” is not a web address.",flowItemPattern:"“{item}” does not have the expected format.",flowSaved:"Saved",flowAccepted:"In progress",flowProgress:"{done} of {total}",flowConflict:"These settings changed while you were editing them.",flowReload:"Reload current settings",flowNotSubmitted:"The answers were not submitted.",flowNoHost:"No host received the answers.",flowStartAgain:"Start another",flowCopy:"Copy",flowCopied:"Copied",flowSavedIt:"I have saved it",flowShownOnce:"Shown only once. Copy it now."};function flowLabels(...sources){const labels={...FORM_LABELS,...FLOW_LABELS};for(const source of sources){if(source==null)continue;if(!flowIs(source))flowFail("labels must be an object of strings");for(const[name,text]of Object.entries(source)){if(!Object.hasOwn(labels,name)){if(/^flow[A-Z]/.test(name)) flowFail("there is no flow label '" + name + "'"); continue; }
2
- if(typeof text!=="string"||!/\S/.test(text)) flowFail("label '" + name + "' must be a string with visible text");
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"842e1f1956f6213f",requires:["forms"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}=context;const{editParseNumber,EDIT_EDITORS,formRuleSpec,formCheck,FORM_LABELS}=FORMS_IMPL;const FLOW_LABELS={flowSteps:"Steps",flowStepOf:"Step {current} of {count}",flowStepAnnounce:"Step {current} of {count}, {title}",flowNext:"Next",flowBack:"Back",flowSubmit:"Submit",flowSubmitting:"Submitting…",flowReviewTitle:"Check your answers",flowChange:"Change",flowChangeField:"Change {field}",flowNotProvided:"Not provided",flowOptional:"(optional)",flowStepOptional:"Optional",flowStepComplete:"Complete",flowStepCurrent:"Current step",flowStepErrors:"Errors: {count}",flowStepLocked:"Complete the earlier steps first",flowStepNotStarted:"Not started",flowErrorSummary:"Fix these before you continue",flowReady:"Ready to submit",flowNotReady:"Errors to fix: {count}",flowWarnings:"Warnings: {count}",flowChangesOnly:"Show changes only",flowNoChanges:"No answers have changed.",flowWas:"Was: {value}",flowChangedCount:"Changed answers: {count}",flowSecretSet:"Set",flowSecretKept:"Unchanged",flowSecretNotSet:"Not set",flowSecretHint:"A value is saved. Leave blank to keep it.",flowSecretShow:"Show",flowSecretHide:"Hide",flowSecretReveal:"Show {field}",flowOn:"On",flowOff:"Off",flowAdd:"Add",flowAddItem:"Add to {field}",flowRemoveItem:"Remove {item}",flowItemLabel:"{field}, item {index}",flowSelectAll:"Select all",flowSelectNone:"Clear",flowSelectedCount:"{count} of {total} selected",flowUnit:"Unit",flowUnitSeconds:"seconds",flowUnitMinutes:"minutes",flowUnitHours:"hours",flowUnitDays:"days",flowSeconds:"[one]{count} second[other]{count} seconds",flowMinutes:"[one]{count} minute[other]{count} minutes",flowHours:"[one]{count} hour[other]{count} hours",flowDays:"[one]{count} day[other]{count} days",flowDurationInvalid:"{field} must be a whole number.",flowItemsMin:"{field} needs at least {min}.",flowItemsMax:"{field} allows at most {max}.",flowItemEmail:"“{item}” is not an email address.",flowItemHost:"“{item}” is not a host name.",flowItemIp:"“{item}” is not an IP address.",flowItemCidr:"“{item}” is not a network range.",flowItemUrl:"“{item}” is not a web address.",flowItemPattern:"“{item}” does not have the expected format.",flowSaved:"Saved",flowAccepted:"In progress",flowProgress:"{done} of {total}",flowConflict:"These settings changed while you were editing them.",flowReload:"Reload current settings",flowNotSubmitted:"The answers were not submitted.",flowNoHost:"No host received the answers.",flowStartAgain:"Start another",flowCopy:"Copy",flowCopied:"Copied",flowSavedIt:"I have saved it",flowShownOnce:"Shown only once. Copy it now."};function flowLabels(...sources){const labels={...FORM_LABELS,...FLOW_LABELS};for(const source of sources){if(source==null)continue;if(!flowIs(source))flowFail("labels must be an object of strings");for(const[name,text]of Object.entries(source)){if(!Object.hasOwn(labels,name))continue;if(typeof text!=="string"||!/\S/.test(text)) flowFail("label '" + name + "' must be a string with visible text");
3
2
  labels[name]=text}}
4
3
  return Object.freeze(labels)}
5
4
  const FLOW_NAME=/^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
6
- const FLOW_KINDS={text:"text",number:"number",date:"text",datetime:"datetime",time:"text",select:"text",toggle:"text",lookup:"text",choice:"text",multiChoice:"list",textarea:"text",list:"list",secret:"text",duration:"duration",custom:"text"};const FLOW_UNITS={seconds:1,minutes:60,hours:3600,days:86400};const flowFail=problem=>{throw new Error("CanopyX: "+problem+".")};const flowIs=value=>value!==null&&typeof value==="object"&&!Array.isArray(value);const flowEmpty=value=>value==null||value===""||Array.isArray(value)&&!value.length;const flowScalar=value=>typeof value==="string"||typeof value==="boolean"||Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;function flowKeys(object,allowed,what){if(!flowIs(object))flowFail(what+" must be an object");for(const key of Object.keys(object))if(!allowed.includes(key))flowFail(what+" has an unknown property '"+key+"'")}
5
+ const FLOW_KINDS={text:"text",number:"number",date:"text",datetime:"datetime",time:"text",select:"text",toggle:"text",lookup:"text",choice:"text",multiChoice:"list",textarea:"text",list:"list",secret:"text",duration:"duration",custom:"text",records:"records",pick:"pick"};const FLOW_UNITS={seconds:1,minutes:60,hours:3600,days:86400};const flowFail=problem=>{throw new Error("CanopyX: "+problem+".")};const flowIs=value=>value!==null&&typeof value==="object"&&!Array.isArray(value);const flowEmpty=value=>value==null||value===""||Array.isArray(value)&&!value.length;const flowScalar=value=>typeof value==="string"||typeof value==="boolean"||Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;const FLOW_HOST={drafts:0,checks:0,preview:0,command:0,consent:0};const flowEach=(hook,...args)=>{for(const feature of Object.values(FLOW_HOST))feature?.[hook]?.(...args)};const flowHost=name=>FLOW_HOST[name]||flowFail("the flow-"+name+" part did not load");function flowKeys(object,allowed,what){if(!flowIs(object))flowFail(what+" must be an object");for(const key of Object.keys(object))if(!allowed.includes(key))flowFail(what+" has an unknown property '"+key+"'")}
7
6
  function flowString(value,max,what,required){if(value===undefined&&!required)return undefined;if(typeof value!=="string"||!/\S/.test(value) || value.length > max) flowFail(what + " needs visible text of at most " + max + " characters");
8
7
  return value}
9
8
  function flowName(value,what){if(typeof value!=="string"||!FLOW_NAME.test(value))flowFail(what+" must be a letter, then up to 63 letters, digits, - or _");return value}
@@ -11,22 +10,24 @@ const flowFlag=(value,what)=>{if(value!==undefined&&typeof value!=="boolean")flo
11
10
  return match?Number(match[1]||0)*86400+Number(match[2]||0)*3600+Number(match[3]||0)*60+Number(match[4]||0):null}
12
11
  function flowDuration(seconds){if(!seconds)return "PT0S";const days=Math.floor(seconds / 86400), hours = Math.floor(seconds % 86400 / 3600), minutes = Math.floor(seconds % 3600 / 60), rest = seconds % 60;
13
12
  return "P" +(days?days+"D":"")+(hours||minutes||rest?"T" +(hours?hours+"H":"")+(minutes?minutes+"M":"")+(rest?rest+"S":""):"")}
14
- function flowFits(field,value){if(value===null)return field.type!=="secret";const listed=item=>field.options.some(option=>option.value===item);switch(field.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return typeof value==="string"&&/^\d{4}-\d\d-\d\d$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().startsWith(value);
13
+ function flowFits(field,value){if(value===null)return field.type!=="secret";const listed=item=>field.host?flowScalar(item):field.options.some(option=>option.value===item);switch(field.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return typeof value==="string"&&/^\d{4}-\d\d-\d\d$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().startsWith(value);
15
14
  case "time":return typeof value==="string"&&/^([01]\d|2[0-3]):[0-5]\d$/.test(value);
16
15
  case "datetime":return typeof value==="string"&&/^\d{4}-\d\d-\d\dT([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{1,7})?)?(Z|[+-](0\d|1[0-3]):[0-5]\d|[+-]14:00)$/.test(value) && flowFits({ type: "date" }, value.slice(0, 10)) && Number.isFinite(Date.parse(value.replace(/(\.\d{3})\d+/, "$1")));
17
- case "select":case "choice":return listed(value);case "lookup":return listed(value);case "multiChoice":return Array.isArray(value)&&value.every(listed)&&new Set(value).size===value.length;case "list":return Array.isArray(value)&&value.length<=1000&&value.every(item=>typeof item==="string");case "duration":return flowSeconds(value)!==null;case "secret":return false;case "custom":return flowScalar(value)||Array.isArray(value)&&value.length<=1000&&value.every(flowScalar);default:return typeof value==="string"}}
18
- const FLOW_FIELD_KEYS=["id","type","label","help","placeholder","required","readOnly","default","when","width","parameter","path","sensitive","validation","options","display","onText","offText","rows","step","keepWhenBlank","reveal","units","editor"];const FLOW_RULE_KEYS=["min","max","minLength","maxLength","pattern","message","minItems","maxItems","item","itemPattern"];function flowField(raw,step,section,model){flowKeys(raw,FLOW_FIELD_KEYS,"A flow field");const id=flowName(raw.id,"A field id");if(model.fields.has(id))flowFail("the field id '"+id+"' is used twice");if(model.fields.size>=1000)flowFail("a flow holds at most 1000 fields");if(!Object.hasOwn(FLOW_KINDS,raw.type))flowFail("field "+id+" has an unknown type");const type=raw.type,rules=raw.validation===undefined?{}:raw.validation,bad=problem=>flowFail("field "+id+" "+problem);flowKeys(rules,FLOW_RULE_KEYS,"The validation of field "+id);const field={id,type,kind:FLOW_KINDS[type],step,section,label:flowString(raw.label,200,"Field "+id+"'s label",true),help:flowString(raw.help,1000,"Field "+id+"'s help"),placeholder:flowString(raw.placeholder,256,"Field "+id+"'s placeholder"),required:flowFlag(raw.required,"required"),readOnly:flowFlag(raw.readOnly,"readOnly"),sensitive:flowFlag(raw.sensitive,"sensitive"),width:raw.width??"full",parameter:raw.parameter,path:raw.path,display:raw.display,rows:raw.rows??4,reveal:raw.reveal!==false,onText:flowString(raw.onText,60,"onText"),offText:flowString(raw.offText,60,"offText"),keepWhenBlank:raw.keepWhenBlank,raw};if(!["full","half"].includes(field.width))bad("has an unknown width");if(field.parameter!==undefined&&(typeof field.parameter!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(field.parameter))) bad("has an invalid parameter name");
16
+ case "select":case "choice":return listed(value);case "lookup":return listed(value);case "multiChoice":return Array.isArray(value)&&value.every(listed)&&new Set(value).size===value.length;case "list":return Array.isArray(value)&&value.length<=1000&&value.every(item=>typeof item==="string");case "duration":return flowSeconds(value)!==null;case "secret":return false;case "pick":case "records":return flowHost(field.type).fits(field,value);case "custom":return flowScalar(value)||Array.isArray(value)&&value.length<=1000&&value.every(flowScalar);default:return typeof value==="string"}}
17
+ const FLOW_FIELD_KEYS=["id","type","label","help","placeholder","required","readOnly","default","when","width","parameter","path","sensitive","validation","options","display","onText","offText","rows","step","keepWhenBlank","reveal","units","editor","source","dependsOn","columns"];const FLOW_RULE_KEYS=["min","max","minLength","maxLength","pattern","message","minItems","maxItems","item","itemPattern"];function flowField(raw,step,section,model){flowKeys(raw,FLOW_FIELD_KEYS,"A flow field");const id=flowName(raw.id,"A field id");if(model.fields.has(id))flowFail("the field id '"+id+"' is used twice");if(model.fields.size>=1000)flowFail("a flow holds at most 1000 fields");if(!Object.hasOwn(FLOW_KINDS,raw.type)||(raw.type==="pick")!==(step.type==="pick"))flowFail("field "+id+" has an unknown type");const type=raw.type,rules=raw.validation===undefined?{}:raw.validation,bad=problem=>flowFail("field "+id+" "+problem);flowKeys(rules,FLOW_RULE_KEYS,"The validation of field "+id);const field={id,type,kind:FLOW_KINDS[type],step,section,label:flowString(raw.label,200,"Field "+id+"'s label",true),help:flowString(raw.help,1000,"Field "+id+"'s help"),placeholder:flowString(raw.placeholder,256,"Field "+id+"'s placeholder"),required:flowFlag(raw.required,"required"),readOnly:flowFlag(raw.readOnly,"readOnly"),sensitive:flowFlag(raw.sensitive,"sensitive"),width:raw.width??"full",parameter:raw.parameter,path:raw.path,display:raw.display,rows:raw.rows??4,reveal:raw.reveal!==false,onText:flowString(raw.onText,60,"onText"),offText:flowString(raw.offText,60,"offText"),keepWhenBlank:raw.keepWhenBlank,host:false,dependsOn:[],raw};if(!["full","half"].includes(field.width))bad("has an unknown width");if(field.parameter!==undefined&&(typeof field.parameter!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(field.parameter))) bad("has an invalid parameter name");
19
18
  if(field.path!==undefined&&(typeof field.path!=="string"||!/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/.test(field.path) || field.path.length > 256)) bad("has an invalid path");
20
19
  if(raw.keepWhenBlank!==undefined&&(type!=="secret"||typeof raw.keepWhenBlank!=="boolean"))bad("can keep a blank value only as a secret");if(raw.reveal!==undefined&&(type!=="secret"||typeof raw.reveal!=="boolean"))bad("has a reveal setting but is not a secret");if((raw.onText!==undefined||raw.offText!==undefined)&&type!=="toggle")bad("names on and off texts but is not a toggle");if(raw.rows!==undefined&&(type!=="textarea"||!Number.isInteger(raw.rows)||raw.rows<2||raw.rows>20))bad("needs 2 to 20 rows, as a text area only");if(raw.display!==undefined&&(type!=="choice"||!["cards","list"].includes(raw.display)))bad("has an unknown display");if(raw.editor!==undefined?type!=="custom"||typeof raw.editor!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(raw.editor) : type === "custom") bad("needs an editor name as a custom field only");
21
- if(raw.step!==undefined&&type!=="number")bad("has a step but is not a number");if(raw.placeholder!==undefined&&!["text","number","lookup","textarea","list"].includes(type))bad("has a placeholder, which text, number, lookup, text area and list fields take");const choosing=["select","choice","multiChoice","lookup"].includes(type);if(raw.options!==undefined&&!choosing)bad("has options but does not choose");if(choosing&&!(Array.isArray(raw.options)&&raw.options.length))bad("needs options");if(Array.isArray(raw.options)&&raw.options.length>1000)bad("needs at most 1000 options");field.options=(raw.options||[]).map(option=>{flowKeys(flowIs(option)?option:{value:option},["value","label","description","badge","disabledReason","group"],"An option of field "+id);const item=flowIs(option)?option:{value:option};if(!flowScalar(item.value))bad("has an invalid option value");return Object.freeze({value:item.value,label:flowString(item.label,200,"An option label")??String(item.value),description:flowString(item.description,1000,"An option description"),badge:flowString(item.badge,40,"An option badge"),disabledReason:flowString(item.disabledReason,400,"A disabled reason"),group:flowString(item.group,200,"An option group")})});if(field.options.length>1000||new Set(field.options.map(option=>option.value)).size!==field.options.length)bad("needs at most 1000 options with distinct values");const list=type==="list"||type==="multiChoice";if(list&&[rules.min,rules.max,rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined))bad("checks items, not lengths or ranges");if(!list&&[rules.minItems,rules.maxItems].some(value=>value!==undefined)||type!=="list"&&(rules.item!==undefined||rules.itemPattern!==undefined))bad("has rules only lists take");if((rules.min!==undefined||rules.max!==undefined)&&!["number","datetime","duration"].includes(type))bad("has a range but is not a number, date and time or duration");if([rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&!["text","textarea","secret"].includes(type))bad("has lengths or a pattern, which text, text area and secret fields take");if(rules.minLength>rules.maxLength)bad("needs the shorter length first");const count=value=>value===undefined||Number.isInteger(value)&&value>=0&&value<=1000;if(!count(rules.minItems)||!count(rules.maxItems)||rules.minItems>rules.maxItems)bad("needs 0 to 1000 items, the fewest first");if(rules.item!==undefined&&!["email","host","ip","cidr","url"].includes(rules.item))bad("has an unknown item rule");let itemPattern=null;if(rules.itemPattern!==undefined)try{itemPattern=boundedPattern(rules.itemPattern,true,true)}catch(error){bad("has an invalid or unsafe item pattern")}
20
+ if(raw.step!==undefined&&type!=="number")bad("has a step but is not a number");if(raw.placeholder!==undefined&&!["text","number","lookup","textarea","list"].includes(type))bad("has a placeholder, which text, number, lookup, text area and list fields take");const choosing=["select","choice","multiChoice","lookup"].includes(type);if(raw.source!==undefined&&(raw.source!=="document"||!choosing)||raw.dependsOn!==undefined)flowHost("options").field(field,raw,choosing,bad);if(raw.options!==undefined&&!choosing)bad("has options but does not choose");if(choosing&&!field.host&&!(Array.isArray(raw.options)&&raw.options.length))bad("needs options");if(raw.options!==undefined&&!Array.isArray(raw.options))bad("needs a list of options");field.options=flowOptions(raw.options||[],bad);if((raw.columns!==undefined)!==(type==="records"))bad("needs columns as a records field only");if(type==="records")field.columns=flowHost("records").columns(raw.columns,bad);const list=type==="list"||type==="multiChoice"||type==="records"||type==="pick";if(list&&[rules.min,rules.max,rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined))bad("checks items, not lengths or ranges");if(!list&&[rules.minItems,rules.maxItems].some(value=>value!==undefined)||type!=="list"&&(rules.item!==undefined||rules.itemPattern!==undefined))bad("has rules only lists take");if((rules.min!==undefined||rules.max!==undefined)&&!["number","datetime","duration"].includes(type))bad("has a range but is not a number, date and time or duration");if([rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&!["text","textarea","secret"].includes(type))bad("has lengths or a pattern, which text, text area and secret fields take");if(rules.minLength>rules.maxLength)bad("needs the shorter length first");const count=value=>value===undefined||Number.isInteger(value)&&value>=0&&value<=1000;if(!count(rules.minItems)||!count(rules.maxItems)||rules.minItems>rules.maxItems)bad("needs 0 to 1000 items, the fewest first");if(rules.item!==undefined&&!["email","host","ip","cidr","url"].includes(rules.item))bad("has an unknown item rule");let itemPattern=null;if(rules.itemPattern!==undefined)try{itemPattern=boundedPattern(rules.itemPattern,true,true)}catch(error){bad("has an invalid or unsafe item pattern")}
22
21
  const range=type==="duration"?{min:rules.min===undefined?undefined:flowSeconds(rules.min),max:rules.max===undefined?undefined:flowSeconds(rules.max)}:rules;if(type==="duration"&&(range.min===null||range.max===null))bad("needs ISO 8601 durations as its range");if(type==="number"&&![rules.min,rules.max].every(bound=>bound===undefined||Number.isFinite(bound)&&Math.abs(bound)<=Number.MAX_SAFE_INTEGER))bad("needs numbers as its range");if(type==="datetime"&&![rules.min,rules.max].every(bound=>bound===undefined||flowFits({type:"datetime"},bound)))bad("needs ISO 8601 moments as its range");const low=type==="datetime"&&typeof range.min==="string"?Date.parse(range.min):range.min,high=type==="datetime"&&typeof range.max==="string"?Date.parse(range.max):range.max;if(low!==undefined&&high!==undefined&&low>high)bad("needs the lower limit first");const shared=formRuleSpec({step:raw.step,placeholder:raw.placeholder},{required:field.required,min:range.min,max:range.max,minLength:rules.minLength,maxLength:rules.maxLength,pattern:rules.pattern,message:rules.message},bad);field.spec=Object.freeze({id,pattern:shared.pattern,rules:shared.rules});field.items=Object.freeze({min:rules.minItems,max:rules.maxItems,rule:rules.item,pattern:itemPattern});const units=raw.units??["minutes","hours","days"];if(raw.units!==undefined&&type!=="duration"||!Array.isArray(units)||!units.length||units.some(unit=>!Object.hasOwn(FLOW_UNITS,unit))||new Set(units).size!==units.length)bad("has invalid duration units");field.units=Object.keys(FLOW_UNITS).filter(unit=>units.includes(unit));if(raw.default!==undefined&&(type==="secret"||list&&!Array.isArray(raw.default)||!flowFits(field,raw.default)))bad("has a default that does not fit it");field.start=raw.default===undefined?(list?[]:null):raw.default;model.fields.set(id,field);return field}
23
- function flowRead(doc){flowKeys(doc,["kind","version","id","title","description","mode","layout","density","timeZone","steps","initial","storedSecrets","baseline","submit","labels"],"A flow document");if(doc.kind!=="flow"||doc.version!==1)flowFail("a flow document needs kind \"flow\" and version 1");const model={id:flowName(doc.id,"The flow id"),title:flowString(doc.title,200,"The flow title",true),description:flowString(doc.description,1000,"The flow description"),mode:doc.mode??"linear",density:doc.density??"normal",local:doc.timeZone==="local",fields:new Map(),steps:[],sections:new Set()};if(!["linear","free"].includes(model.mode)||!["compact","normal","comfortable"].includes(model.density)||doc.timeZone!==undefined&&!["utc","local"].includes(doc.timeZone))flowFail("the flow's mode, density or time zone is unknown");if(doc.layout!==undefined)flowKeys(doc.layout,["orientation"],"The flow layout");model.orientation=doc.layout?.orientation??"auto";if(!["auto","horizontal","vertical"].includes(model.orientation))flowFail("the flow layout has an unknown orientation");if(!Array.isArray(doc.steps)||!doc.steps.length||doc.steps.length>64)flowFail("a flow needs 1 to 64 steps");for(const[index,raw]of doc.steps.entries()){flowKeys(raw,["id","title","description","type","fields","sections","when","optional","declaration","changesOnly"],"A flow step");const step={id:flowName(raw.id,"A step id"),title:flowString(raw.title,200,"A step title",raw.type!=="review"),description:flowString(raw.description,1000,"A step description"),type:raw.type??"form",optional:flowFlag(raw.optional,"optional"),declaration:flowString(raw.declaration,1000,"The declaration"),changesOnly:raw.changesOnly!==false,blocks:[],raw};if(model.steps.some(other=>other.id===step.id))flowFail("the step id '"+step.id+"' is used twice");if(!["form","info","review"].includes(step.type))flowFail("step "+step.id+" has an unknown type");if(step.type==="review"&&index!==doc.steps.length-1)flowFail("the review step must be the last step");if(step.type==="review"&&(raw.when!==undefined||raw.optional===true))flowFail("the review step takes no condition and is never optional");if(step.type!=="review"&&(raw.declaration!==undefined||raw.changesOnly!==undefined))flowFail("only a review step has a declaration or changesOnly");if(raw.changesOnly!==undefined&&typeof raw.changesOnly!=="boolean")flowFail("changesOnly must be true or false");if(step.type!=="form"&&(raw.fields!==undefined||raw.sections!==undefined))flowFail("only a form step has fields and sections");if(raw.fields!==undefined&&!Array.isArray(raw.fields)||raw.sections!==undefined&&!Array.isArray(raw.sections))flowFail("step "+step.id+" needs arrays of fields and sections");if(raw.fields?.length)step.blocks.push({section:null,fields:raw.fields.map(field=>flowField(field,step,null,model))});if((raw.sections||[]).length>64)flowFail("step "+step.id+" holds at most 64 sections");for(const rawSection of raw.sections||[]){flowKeys(rawSection,["id","label","description","fields","when"],"A flow section");const section={id:flowName(rawSection.id,"A section id"),label:flowString(rawSection.label,200,"A section label",true),description:flowString(rawSection.description,1000,"A section description"),raw:rawSection};if(model.sections.has(section.id))flowFail("the section id '"+section.id+"' is used twice");model.sections.add(section.id);if(!Array.isArray(rawSection.fields))flowFail("section "+section.id+" needs an array of fields");step.blocks.push({section,fields:rawSection.fields.map(field=>flowField(field,step,section,model))})}
24
- model.steps.push(step)}
22
+ function flowRead(doc){flowKeys(doc,["kind","version","id","title","description","mode","layout","density","timeZone","steps","initial","storedSecrets","baseline","submit","labels","command","presets","draft","deepLinks","endpoint"],"A flow document");if(doc.kind!=="flow"||doc.version!==1)flowFail("a flow document needs kind \"flow\" and version 1");const model={id:flowName(doc.id,"The flow id"),title:flowString(doc.title,200,"The flow title",true),description:flowString(doc.description,1000,"The flow description"),mode:doc.mode??"linear",density:doc.density??"normal",local:doc.timeZone==="local",fields:new Map(),steps:[],sections:new Set(),checks:new Map()};if(!["linear","free","tasks"].includes(model.mode)||!["compact","normal","comfortable"].includes(model.density)||doc.timeZone!==undefined&&!["utc","local"].includes(doc.timeZone))flowFail("the flow's mode, density or time zone is unknown");if(doc.layout!==undefined)flowKeys(doc.layout,["orientation","container","launch"],"The flow layout");model.orientation=doc.layout?.orientation??"auto";if(!["auto","horizontal","vertical"].includes(model.orientation))flowFail("the flow layout has an unknown orientation");if(!Array.isArray(doc.steps)||!doc.steps.length||doc.steps.length>64)flowFail("a flow needs 1 to 64 steps");for(const[index,raw]of doc.steps.entries()){flowKeys(raw,["id","title","description","type","fields","sections","when","optional","declaration","changesOnly","checks","group","pick","confirm","acknowledge","preview"],"A flow step");const step={id:flowName(raw.id,"A step id"),title:flowString(raw.title,200,"A step title",raw.type!=="review"),description:flowString(raw.description,1000,"A step description"),type:raw.type??"form",optional:flowFlag(raw.optional,"optional"),declaration:flowString(raw.declaration,1000,"The declaration"),changesOnly:raw.changesOnly!==false,group:flowString(raw.group,200,"A step group"),blocks:[],checks:[],raw};if(model.steps.some(other=>other.id===step.id))flowFail("the step id '"+step.id+"' is used twice");if(!["form","info","review","pick"].includes(step.type))flowFail("step "+step.id+" has an unknown type");step.asks=step.type==="form"||step.type==="pick";if(step.type==="review"&&index!==doc.steps.length-1)flowFail("the review step must be the last step");if(step.type==="review"&&(raw.when!==undefined||raw.optional===true||raw.group!==undefined))flowFail("the review step takes no condition or group and is never optional");const reviewKey=["declaration","changesOnly","confirm","acknowledge","preview"].find(key=>raw[key]!==undefined);if(step.type!=="review"&&reviewKey)flowFail("only a review step has "+reviewKey);if(raw.changesOnly!==undefined&&typeof raw.changesOnly!=="boolean")flowFail("changesOnly must be true or false");if(step.type!=="form"&&(raw.fields!==undefined||raw.sections!==undefined))flowFail("only a form step has fields and sections");if((raw.pick!==undefined)!==(step.type==="pick"))flowFail("step "+step.id+" has a pick only as a pick step, and a pick step needs one");if(raw.fields!==undefined&&!Array.isArray(raw.fields)||raw.sections!==undefined&&!Array.isArray(raw.sections))flowFail("step "+step.id+" needs arrays of fields and sections");if(raw.fields?.length)step.blocks.push({section:null,fields:raw.fields.map(field=>flowField(field,step,null,model))});if((raw.sections||[]).length>64)flowFail("step "+step.id+" holds at most 64 sections");for(const rawSection of raw.sections||[]){flowKeys(rawSection,["id","label","description","fields","when","source","dependsOn"],"A flow section");const section={id:flowName(rawSection.id,"A section id"),label:flowString(rawSection.label,200,"A section label",true),description:flowString(rawSection.description,1000,"A section description"),host:false,dependsOn:[],raw:rawSection};if(model.sections.has(section.id))flowFail("the section id '"+section.id+"' is used twice");model.sections.add(section.id);if(rawSection.source!==undefined||rawSection.dependsOn!==undefined)flowHost("options").section(section,rawSection);else if(!Array.isArray(rawSection.fields))flowFail("section "+section.id+" needs an array of fields");step.blocks.push({section,fields:(rawSection.fields||[]).map(field=>flowField(field,step,section,model))})}
23
+ if(step.type==="pick")flowHost("pick").read(raw.pick,step,model);if(step.type==="review"){step.preview=flowFlag(raw.preview,"preview");if(raw.confirm!==undefined||raw.acknowledge!==undefined)flowHost("consent").read(raw,step)}
24
+ if(raw.checks!==undefined)flowHost("checks").read(raw.checks,step,model);model.steps.push(step)}
25
25
  const paths=new Set();for(const path of[...model.fields.values()].map(field=>field.path).filter(Boolean)){if(paths.has(path))flowFail("the path '"+path+"' is used twice");paths.add(path)}
26
- for(const path of paths)for(let at=path.indexOf(".");at>0;at=path.indexOf(".",at+1))if(paths.has(path.slice(0,at)))flowFail("the path '"+path+"' lies inside '"+path.slice(0,at)+ "'");flowConditions(model);flowEditMode(doc,model);if(doc.submit!==undefined)flowKeys(doc.submit,["label"],"The flow's submit");model.submitLabel=flowString(doc.submit?.label,80,"The submit label");model.labels=doc.labels;return model}
26
+ for(const path of paths)for(let at=path.indexOf(".");at>0;at=path.indexOf(".",at+1))if(paths.has(path.slice(0,at)))flowFail("the path '"+path+"' lies inside '"+path.slice(0,at)+ "'");flowConditions(model);flowEach("finish",model);flowEditMode(doc,model);Object.assign(model,{container:"inline",command:null,presets:[],draft:"none",deepLinks:false,endpoint:null});flowEach("document",doc,model);if(doc.submit!==undefined)flowKeys(doc.submit,["label"],"The flow's submit");model.submitLabel=flowString(doc.submit?.label,80,"The submit label");model.labels=doc.labels;return model}
27
27
  function flowEditMode(doc,model){if(doc.initial!==undefined){if(!flowIs(doc.initial))flowFail("initial must be an object of field values");for(const[id,value]of Object.entries(doc.initial)){const field=model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))flowFail("initial has no value for '"+id+"' that fits a field")}}
28
28
  model.initial=doc.initial?{...doc.initial}:null;if(doc.storedSecrets!==undefined&&(!Array.isArray(doc.storedSecrets)||doc.storedSecrets.some(id=>model.fields.get(id)?.type!=="secret")||new Set(doc.storedSecrets).size!==doc.storedSecrets.length))
29
29
  flowFail("storedSecrets lists only secret fields");model.stored=new Set(doc.storedSecrets||[]);if(doc.baseline!==undefined&&(typeof doc.baseline!=="string"||!doc.baseline.length||doc.baseline.length>256))flowFail("the baseline is 1 to 256 characters");model.baseline=doc.baseline??null;if(!model.initial&&(model.stored.size||model.baseline!==null))model.initial={}}
30
+ function flowOptions(raw,bad){if(raw.length>1000)bad("needs at most 1000 options");const options=raw.map(option=>{flowKeys(flowIs(option)?option:{value:option},["value","label","description","badge","disabledReason","group"],"An option");const item=flowIs(option)?option:{value:option};if(!flowScalar(item.value))bad("has an invalid option value");return Object.freeze({value:item.value,label:flowString(item.label,200,"An option label")??String(item.value),description:flowString(item.description,1000,"An option description"),badge:flowString(item.badge,40,"An option badge"),disabledReason:flowString(item.disabledReason,400,"A disabled reason"),group:flowString(item.group,200,"An option group")})});if(new Set(options.map(option=>option.value)).size!==options.length)bad("needs options with distinct values");return options}
30
31
  const FLOW_ARITY={eq:1,ne:1,gt:1,ge:1,lt:1,le:1,contains:1,notContains:1,startsWith:1,endsWith:1,matches:1,notMatches:1,in:-1,notIn:-1,between:2,isNull:0,isNotNull:0};function flowCondition(raw,model,counts,depth=1){flowKeys(raw,["operator","filters","groups"],"A condition");if(depth>8||++counts.groups>64)flowFail("a condition holds at most eight levels and 64 groups");const filters=raw.filters??[],groups=raw.groups??[];if(!["and","or","none"].includes(raw.operator)||!Array.isArray(filters)||!Array.isArray(groups)||!filters.length&&!groups.length)
31
32
  flowFail("a condition group needs and, or or none and at least one predicate or group");return Object.freeze({operator:raw.operator,groups:groups.map(group=>flowCondition(group,model,counts,depth+1)),filters:filters.map(filter=>{flowKeys(filter,["field","operator","values"],"A condition predicate");if(++counts.filters>64)flowFail("a condition holds at most 64 predicates");if(!model.fields.has(filter.field))flowFail("a condition names the unknown field '"+filter.field+"'");if(model.fields.get(filter.field).type==="secret")flowFail("a condition cannot read the secret field '"+filter.field+"'");const arity=FLOW_ARITY[filter.operator],values=filter.values??[];if(arity===undefined||!Array.isArray(values)||(arity<0?values.length<1||values.length>1000:values.length!==arity)||!values.every(flowScalar))
32
33
  flowFail("a condition on '"+filter.field+"' has an unknown operator or the wrong values");const safe=value=>typeof value!=="number"||Math.abs(value)<=Number.MAX_SAFE_INTEGER,op=filter.operator;if(!values.every(value=>safe(value)&&(typeof value!=="string"||value.length<=4096))||
@@ -51,12 +52,12 @@ hint.id=id+"-hint";hint.hidden=true;message.id=id+"-error";message.hidden=true;c
51
52
  return instance};fv.mount=value=>{try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}
52
53
  fv.instance=create(value);fv.pending=null;const control=fv.control=fv.instance.control||fv.instance.element;if(fv.instance.group){control.setAttribute("aria-labelledby",label.id);label.removeAttribute("for")}
53
54
  else{control.id=id;if(label.tagName==="LABEL")label.htmlFor=id}
54
- if(field.type==="toggle")control.setAttribute("role","switch");if(fv.instance.hint){setText(hint,fv.instance.hint);hint.hidden=false}else hint.hidden=true;fv.describe();const holder=wrapper.querySelector(".cx-flow-control");holder.replaceChildren(fv.instance.element);if(field.type==="toggle")holder.appendChild(label)};fv.describe=()=>{const target=fv.instance.describes||fv.control;const ids=[help&&help.id,!hint.hidden&&hint.id,!message.hidden&&message.id].filter(Boolean);if(ids.length)target.setAttribute("aria-describedby",ids.join(" "));else target.removeAttribute("aria-describedby");if(message.hidden)target.removeAttribute("aria-invalid");else target.setAttribute("aria-invalid","true")};fv.read=()=>{const parse=fv.instance.error?.()||null;if(parse)return{error:parse};let value=fv.instance.getValue();if(value===""||value===undefined)value=null;if(field.type!=="secret"&&!flowFits(field,value))return{error:fill(view.labels.gridEditPattern,{column:field.label})};return{value}};fv.message=text=>{setText(message,text||"");message.hidden=!text;fv.describe();wrapper.classList.toggle("cx-flow-invalid",!!text)};fv.focus=()=>fv.instance.focus?fv.instance.focus():fv.control.focus({preventScroll:false});fv.destroy=()=>{fv.abort?.abort();try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}};const holder=h("div","cx-flow-control" +(field.type==="toggle"?" cx-flow-toggle":""));if(field.type==="toggle")wrapper.append(holder);else wrapper.append(label);if(help)wrapper.appendChild(help);wrapper.append(hint);if(field.type!=="toggle")wrapper.append(holder);wrapper.append(message);return fv}
55
+ if(field.type==="toggle")control.setAttribute("role","switch");if(fv.instance.hint){setText(hint,fv.instance.hint);hint.hidden=false}else hint.hidden=true;fv.describe();const holder=wrapper.querySelector(".cx-flow-control");holder.replaceChildren(fv.instance.element);if(field.type==="toggle")holder.appendChild(label)};fv.describe=()=>{const target=fv.instance.describes||fv.control;const ids=[help&&help.id,!hint.hidden&&hint.id,fv.note&&!fv.note.hidden&&fv.note.id,!message.hidden&&message.id].filter(Boolean);if(ids.length)target.setAttribute("aria-describedby",ids.join(" "));else target.removeAttribute("aria-describedby");if(message.hidden)target.removeAttribute("aria-invalid");else target.setAttribute("aria-invalid","true")};fv.read=()=>{const parse=fv.instance.error?.()||null;if(parse)return{error:parse};let value=fv.instance.getValue();if(value===""||value===undefined)value=null;if(field.type!=="secret"&&!flowFits(field,value))return{error:fill(view.labels.gridEditPattern,{column:field.label})};return{value}};fv.message=text=>{setText(message,text||"");message.hidden=!text;fv.describe();wrapper.classList.toggle("cx-flow-invalid",!!text)};fv.focus=()=>fv.instance.focus?fv.instance.focus():fv.control.focus({preventScroll:false});fv.destroy=()=>{fv.abort?.abort();try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}};const holder=h("div","cx-flow-control" +(field.type==="toggle"?" cx-flow-toggle":""));if(field.type==="toggle")wrapper.append(holder);else wrapper.append(label);if(help)wrapper.appendChild(help);wrapper.append(hint);if(field.type!=="toggle")wrapper.append(holder);wrapper.append(message);return fv}
55
56
  function flowEditorContext(view,field,id,value,signal){const kind=field.kind==="number"||field.kind==="datetime"?field.kind:"text";return Object.freeze({id,value,text:value==null?"":flowDisplay(view,field,value),readOnly:field.readOnly,row:Object.freeze({id:view.model.id,cells:Object.freeze(view.getValues())}),column:Object.freeze({id:field.id,title:field.label,kind}),editor:Object.freeze({type:field.type,placeholder:field.placeholder,step:field.raw.step}),options:field.options.length?field.options:null,rules:field.spec.rules,labels:view.labels,locale:view.lang,timeZone:view.model.local?"local":"utc",mode:"flow",initial:undefined,signal,lookupMinLength:1,commit:()=>view.changed(field,"edit"),cancel:()=>{},lookup:search=>{const folded=foldText(search.trim());return field.options.filter(option=>!option.disabledReason&&foldText(option.label).includes(folded)).slice(0,20)},remember:()=>queueMicrotask(()=>view.changed(field,"edit"))})}
56
- function flowEditor(view,field,context){switch(field.type){case "choice":return flowChoiceInput(context,field);case "multiChoice":return flowMultiInput(context,field,view.labels);case "textarea":return flowTextAreaInput(context,field);case "list":return flowListInput(context,field,view.labels);case "secret":return flowSecretInput(context,field,view.labels,view.keeps(field));case "duration":return flowDurationInput(context,field,view.labels);case "custom":{const editor=view.editors[field.raw.editor];if(!editor||typeof editor.create!=="function")flowFail("the field "+field.id+" names the editor '"+field.raw.editor+"', which nobody registered");return editor.create(context)}
57
+ function flowEditor(view,field,context){switch(field.type){case "choice":return flowChoiceInput(context,field);case "multiChoice":return flowMultiInput(context,field,view.labels);case "textarea":return flowTextAreaInput(context,field);case "list":return flowListInput(context,field,view.labels);case "secret":return flowSecretInput(context,field,view.labels,view.keeps(field));case "duration":return flowDurationInput(context,field,view.labels);case "records":case "pick":return flowHost(field.type).input(context,field,view);case "custom":{const editor=view.editors[field.raw.editor];if(!editor||typeof editor.create!=="function")flowFail("the field "+field.id+" names the editor '"+field.raw.editor+"', which nobody registered");return editor.create(context)}
57
58
  default:{const instance=EDIT_EDITORS[field.type].create(context),control=instance.control||instance.element;control.classList.add("cx-flow-input");if(field.readOnly){control.disabled=control.tagName==="SELECT"||control.type==="checkbox";control.readOnly=true}
58
59
  return instance}}}
59
- function flowDisplay(view,field,value){const labels=view.labels,option=item=>field.options.find(entry=>entry.value===item)?.label??String(item);if(flowEmpty(value))return labels.flowNotProvided;switch(field.type){case "number":return numberFormat(view.lang,0,20).format(value);case "toggle":return value?field.onText||labels.flowOn:field.offText||labels.flowOff;case "select":case "choice":case "lookup":return option(value);case "multiChoice":return value.map(option).join(", ");case "list":return value.join("\n");case "date":return flowDate(view,value+"T00:00:00Z",{dateStyle:"medium",timeZone:"UTC"});case "datetime":return flowDate(view,value,{dateStyle:"medium",timeStyle:"short",timeZone:view.model.local?undefined:"UTC"})+(view.model.local?"":" UTC");case "duration":return flowWords(view,flowSeconds(value),field.units[0]);default:return String(value)}}
60
+ function flowDisplay(view,field,value){const labels=view.labels,option=item=>field.options.find(entry=>entry.value===item)?.label??String(item);if(flowEmpty(value))return labels.flowNotProvided;switch(field.type){case "number":return numberFormat(view.lang,0,20).format(value);case "toggle":return value?field.onText||labels.flowOn:field.offText||labels.flowOff;case "select":case "choice":case "lookup":return option(value);case "multiChoice":return value.map(option).join(", ");case "list":return value.join("\n");case "pick":case "records":return flowHost(field.type).display(view,value);case "date":return flowDate(view,value+"T00:00:00Z",{dateStyle:"medium",timeZone:"UTC"});case "datetime":return flowDate(view,value,{dateStyle:"medium",timeStyle:"short",timeZone:view.model.local?undefined:"UTC"})+(view.model.local?"":" UTC");case "duration":return flowWords(view,flowSeconds(value),field.units[0]);default:return String(value)}}
60
61
  function flowDate(view,iso,options){try{return new Intl.DateTimeFormat(view.lang||undefined,options).format(new Date(iso))}catch(error){return iso}}
61
62
  function flowWords(view,seconds,smallest="seconds"){const unit=["days","hours","minutes","seconds"].find(name=>seconds%FLOW_UNITS[name]===0&&seconds>=FLOW_UNITS[name])||(seconds?"seconds":smallest),count=seconds / FLOW_UNITS[unit];
62
63
  const label=view.labels["flow"+unit[0].toUpperCase()+ unit.slice(1)];return fill(plural(label,count,view.lang),{count:numberFormat(view.lang,0,0).format(count)})}
@@ -80,11 +81,11 @@ function flowDurationInput(context,field,labels){const group=h("div","cx-flow-du
80
81
  group.append(amount,unit);const read=()=>amount.value.trim()===""?null:editParseNumber(amount.value,context.locale);return{element:group,control:group,group:true,amount,describes:amount,focus:()=>amount.focus(),error:()=>{const value=read(),seconds=value*FLOW_UNITS[unit.value];return value!==null&&!(Number.isSafeInteger(value)&&value>=0&&Number.isSafeInteger(seconds)&&flowSeconds(flowDuration(seconds))!==null)?fill(labels.flowDurationInvalid,{field:field.label}):null},getValue:()=>{const value=read();return value===null?null:flowDuration(value*FLOW_UNITS[unit.value])}}}
81
82
  let FLOW_SEQUENCE=0;const flowCopy=value=>value===undefined?undefined:JSON.parse(JSON.stringify(value));const flowSameValue=(left,right)=>JSON.stringify(left??null)===JSON.stringify(right??null);const FLOW_HOOKS=["validateStep","validateFlow","submit"],FLOW_CALLBACKS=["onReady","onStepChange","onValueChange","onSubmit","onSubmitted","onReload"];class FlowView{constructor(host,options,editors){const hooks=options.hooks??{};if(!flowIs(hooks)||FLOW_HOOKS.some(name=>hooks[name]!=null&&typeof hooks[name]!=="function")||hooks.validators!=null&&!flowIs(hooks.validators)||
82
83
  FLOW_CALLBACKS.some(name=>options[name]!=null&&typeof options[name]!=="function")||options.editors!=null&&!flowIs(options.editors)||
83
- options.values!=null&&!flowIs(options.values))flowFail("invalid flow options");this.host=host;this.options=options;this.hooks=hooks;this.model=flowRead(options.flow);this.labels=flowLabels(this.model.labels,options.labels);this.editors=Object.assign({},editors,options.editors);this.id="cx-flow-" + ++FLOW_SEQUENCE;this.lang=host.closest("[lang]")?.lang||document.documentElement.lang||"";this.lifetime=new AbortController();this.fields=new Map();this.sections=new Map();this.visibility=new Map();this.secrets=new Map();this.errors=new Map();this.hostErrors=new Map();this.hostStepErrors=new Map();this.stepErrors=new Map();this.completed=new Set();this.banner=null;this.phase="form";this.returnToReview=false;this.generation=0;this.answerRevision=0;try{this.begin(options.values);this.build()}
84
+ options.values!=null&&!flowIs(options.values))flowFail("invalid flow options");this.host=host;this.options=options;this.model=flowRead(options.flow);this.hooks=this.model.endpoint?flowHost("endpoint").hooks(this.model,hooks,options.headers||{}):hooks;this.labels=flowLabels(this.model.labels,options.labels);this.editors=Object.assign({},editors,options.editors);this.id="cx-flow-" + ++FLOW_SEQUENCE;this.lang=host.closest("[lang]")?.lang||document.documentElement.lang||"";this.lifetime=new AbortController();this.fields=new Map();this.sections=new Map();this.visibility=new Map();this.secrets=new Map();this.errors=new Map();this.hostErrors=new Map();this.hostStepErrors=new Map();this.stepErrors=new Map();this.completed=new Set();this.banner=null;this.phase="form";this.returnToReview=false;this.generation=0;this.answerRevision=0;this.hostState?.();try{this.begin(options.values);this.build()}
84
85
  catch(error){this.destroy();throw error}}
85
- destroy(){if(this.destroyed)return;this.destroyed=true;this.retireOperation();this.lifetime.abort();this.resize?.disconnect();for(const fv of this.fields.values())fv.destroy();this.secrets.clear();this.root?.remove();this.host.removeAttribute("data-cx-ready")}
86
+ destroy(){if(this.destroyed)return;this.destroyed=true;this.retireOperation();this.lifetime.abort();this.resize?.disconnect();for(const fv of this.fields.values())fv.destroy();this.releaseHostWork?.();this.secrets.clear();this.root?.remove();this.host.removeAttribute("data-cx-ready")}
86
87
  begin(values){const model=this.model;this.values=new Map();for(const field of model.fields.values()){if(field.type==="secret")continue;const initial=model.initial?Object.hasOwn(model.initial,field.id)?model.initial[field.id]:null:field.start;this.values.set(field.id,flowCopy(initial))}
87
- for(const[id,value]of Object.entries(values||{}))this.assign(id,value);this.startValues=new Map([...this.values].map(([id,value])=>[id,flowCopy(value)]));this.secrets.clear();this.errors.clear();this.hostErrors.clear();this.hostStepErrors.clear();this.stepErrors.clear();this.showAll=undefined;this.reviewStale=true;this.completed.clear();this.visibility.clear();this.banner=null;this.returnToReview=false;this.step=this.visibleSteps()[0];if(model.initial)for(const step of this.visibleSteps())if(step.type==="form"&&this.stepFields(step).every(field=>!this.problemNow(field)))this.completed.add(step.id)}
88
+ for(const[id,value]of Object.entries(values||{}))this.assign(id,value);this.startValues=new Map([...this.values].map(([id,value])=>[id,flowCopy(value)]));this.secrets.clear();this.errors.clear();this.hostErrors.clear();this.hostStepErrors.clear();this.stepErrors.clear();this.showAll=undefined;this.reviewStale=true;this.completed.clear();this.visibility.clear();this.banner=null;this.returnToReview=false;this.step=this.hub||this.visibleSteps()[0];if(model.initial)for(const step of this.visibleSteps())if(step.asks&&this.stepFields(step).every(field=>!this.problemNow(field)))this.completed.add(step.id)}
88
89
  assign(id,value){const field=this.model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))flowFail("no field '"+id+"' takes that value");if(!flowSameValue(this.values.get(id),value)||this.fields.get(id)?.pending){this.answersChanged();this.forgetHost(field)}
89
90
  this.values.set(id,flowCopy(value))}
90
91
  keeps(field){return field.type==="secret"&&this.model.stored.has(field.id)&&field.keepWhenBlank!==false}
@@ -102,7 +103,7 @@ changed(field,source){const fv=this.fields.get(field.id);if(!fv||this.destroyed|
102
103
  if(source!=="blur"&&this.errors.has(field.id))this.checkField(field);this.refreshVisibility()}
103
104
  forgetHost(field){this.completed.delete(field.step.id);const affected=new Set([field.id]);let changed;do{changed=false;for(const other of this.model.fields.values())if(!affected.has(other.id)&&[...other.refs].some(id=>affected.has(id))){affected.add(other.id);this.completed.delete(other.step.id);changed=true}}while(changed);if(this.hostStepErrors.delete(field.step.id)){const summary=this.sections.get(field.step.id)?._cxSummary;if(summary)summary.hidden=true}
104
105
  this.hostErrors.delete(field.id);this.reviewStale=true}
105
- refreshVisibility(){this.visibility.clear();for(const fv of this.fields.values())fv.element.hidden=!this.visible(fv.field);for(const[stepId,section]of this.sections)for(const block of section._cxBlocks||[])block.element.hidden=!block.fields.some(field=>this.visible(field));if(!this.root)return;if(this.visibleSteps().map(step=>step.id).join()+(this.orientation()==="stack"?"|stack":"")!==this.railKey){this.railTooWide=false;this.render()}
106
+ refreshVisibility(){this.visibility.clear();for(const fv of this.fields.values())fv.element.hidden=!this.visible(fv.field);for(const[stepId,section]of this.sections)for(const block of section._cxBlocks||[])block.element.hidden=!block.fields.some(field=>this.visible(field));this.prepareVisible?.();if(!this.root)return;if(this.visibleSteps().map(step=>step.id).join()+(this.orientation()==="stack"?"|stack":"")!==this.railKey){this.railTooWide=false;this.render()}
106
107
  else this.renderRail()}
107
108
  setValues(partial){if(!flowIs(partial))flowFail("setValues takes an object of field values");for(const[id,value]of Object.entries(partial)){const previous=this.values.get(id)??null;this.assign(id,value);this.fields.get(id)?.mount(flowCopy(value));this.errors.delete(id);this.hostErrors.delete(id);this.fields.get(id)?.message(null);if(!flowSameValue(previous,value))this.emit("flowValueChanged",{field:id,value:flowCopy(value),previous:flowCopy(previous),source:"api"},"onValueChange")}
108
109
  this.refreshVisibility();this.refreshReview()}
@@ -111,46 +112,47 @@ isDirty(){return this.secrets.size>0||[...this.values].some(([id,value])=>!flowS
111
112
  getStep(){return this.step.id}
112
113
  getState(){return{step:this.step.id,values:Object.fromEntries([...this.values].map(([id,value])=>[id,flowCopy(value)])),completed:[...this.completed]}}
113
114
  setState(state){if(!flowIs(state)||state.values!=null&&!flowIs(state.values)||state.completed!=null&&!Array.isArray(state.completed))flowFail("setState takes a flow state");for(const[id,value]of Object.entries(state.values||{})){this.assign(id,value);this.fields.get(id)?.mount(flowCopy(value));this.errors.delete(id);this.hostErrors.delete(id);this.fields.get(id)?.message(null)}
114
- this.completed=new Set((state.completed||[]).filter(id=>this.model.steps.some(step=>step.id===id)));this.refreshVisibility();const target=this.model.steps.find(step=>step.id===state.step);return this.go(target&&this.stepVisible(target)?target:this.visibleSteps()[0],"jump",true)}
115
- reset(){this.generation++;this.answersChanged();this.busy=false;this.checks?.clear();this.reviewTicket=null;this.reviewChecking=false;for(const fv of this.fields.values())fv.destroy();this.fields.clear();this.sections.clear();this.begin(this.options.values);this.phase="form";this.render();return true}
116
- emit(name,detail,callback,cancelable=false){const event=new CustomEvent("canopyx:"+name,{detail,bubbles:true,cancelable});const go=this.host.dispatchEvent(event);if(callback&&typeof this.options[callback]==="function")try{this.options[callback](detail)}catch(error){this.report(error,callback)}
115
+ this.completed=new Set((state.completed||[]).filter(id=>this.model.steps.some(step=>step.id===id)));this.refreshVisibility();const target=this.hub&&state.step===this.hub.id?this.hub:this.model.steps.find(step=>step.id===state.step);return this.go(target&&this.stepVisible(target)?target:this.hub||this.visibleSteps()[0],"jump",true)}
116
+ reset(){this.generation++;this.answersChanged();this.busy=false;this.checks?.clear();this.reviewTicket=null;this.reviewChecking=false;for(const fv of this.fields.values())fv.destroy();this.fields.clear();this.sections.clear();this.releaseHostWork?.();this.begin(this.options.values);this.phase="form";this.render();return true}
117
+ emit(name,detail,callback,cancelable=false){const event=new CustomEvent("canopyx:"+name,{detail,bubbles:true,cancelable});let go=this.host.dispatchEvent(event);if(callback&&typeof this.options[callback]==="function")try{if(this.options[callback](detail)===false&&cancelable)go=false}catch(error){this.report(error,callback)}
117
118
  return go}
118
119
  report(error,hook){this.host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error,hook},bubbles:true}))}
119
120
  async hook(name,run){try{return{value:await run()}}
120
121
  catch(error){this.report(error,name);return{failed:error&&error.message?error.message:String(error)}}}}
121
122
  const FLOW_NARROW=640;Object.assign(FlowView.prototype,{build(){const root=this.root=h("div","cx-flow"),head=h("header","cx-flow-head"),title=h("h2","cx-flow-title");root.dataset.cxDensity=this.model.density;setText(title,this.model.title);title.id=this.id+"-title";head.appendChild(title);if(this.model.description){const description=h("p","cx-flow-desc");setText(description,this.model.description);head.appendChild(description)}
122
- root.setAttribute("aria-labelledby",title.id);root.setAttribute("role","group");this.layout=h("div","cx-flow-layout");this.rail=h("div","cx-flow-rail");this.railList=h("ol","cx-flow-marks");this.rail.appendChild(this.railList);this.main=h("div","cx-flow-main");this.stage=h("div","cx-flow-stage");this.actions=h("div","cx-flow-actions");this.main.append(this.stage,this.actions);this.live=h("div","cx-sr-only");this.live.setAttribute("aria-live","polite");this.layout.append(this.rail,this.main);root.append(head,this.layout,this.live);this.host.appendChild(root);const signal=this.lifetime.signal;root.addEventListener("input",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("change",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("focusout",event=>this.fieldEvent(event,"blur"),{signal});root.addEventListener("keydown",event=>this.key(event),{signal});this.narrow=false;if(global.ResizeObserver){this.resize=new ResizeObserver(()=>this.measure());this.resize.observe(this.host)}
123
- this.measure(true);this.render()},fieldEvent(event,source){const wrapper=event.target.closest?.("[data-cx-field]");const field=wrapper&&this.model.fields.get(wrapper.dataset.cxField);if(!field||this.phase!=="form")return;if(source==="blur"){if(wrapper.contains(event.relatedTarget))return;this.changed(field,"blur");this.checkField(field);return}
124
- this.changed(field,source)},measure(first){const width=this.host.clientWidth||global.innerWidth,narrow=width<FLOW_NARROW;if(!first&&width===this.width)return;const changed=narrow!==this.narrow||this.railTooWide&&width>this.width;this.width=width;this.narrow=narrow;if(changed){this.railTooWide=false;if(!first)this.render()}
125
- else if(!first)this.fitRail()},orientation(){if(this.visibleSteps().length<=1)return "single";if(this.narrow)return "stack";return this.model.orientation==="auto"?(this.visibleSteps().length>6||this.railTooWide?"vertical":"horizontal"):this.model.orientation},fitRail(){if(this.model.orientation!=="auto"||this.railTooWide||this.orientation()!=="horizontal"||!this.rail.clientWidth)return;if(this.rail.scrollWidth>this.rail.clientWidth+1){this.railTooWide=true;this.render()}},render(){const active=document.activeElement;const steps=this.visibleSteps();if(this.phase==="form"&&!steps.includes(this.step)){const from=this.step;this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(from))||steps[steps.length-1];this.reviewStale=true;this.emit("flowStepChanged",{from:from.id,to:this.step.id},"onStepChange")}
126
- const orientation=this.orientation();this.layout.className="cx-flow-layout cx-flow-"+orientation;this.root.classList.toggle("cx-flow-narrow",this.narrow);if(orientation==="stack"||orientation==="single"){this.rail.removeAttribute("role");this.rail.removeAttribute("aria-label")}
127
- else{this.rail.setAttribute("role","navigation");this.rail.setAttribute("aria-label",this.labels.flowSteps)}
128
- this.renderStage();this.renderRail();this.renderActions();if(active&&active!==document.activeElement&&active.isConnected&&this.root.contains(active))active.focus({preventScroll:true});this.fitRail()},stepState(step,steps){if(this.phase!=="form")return "complete";if(step===this.step)return "current";const errors=this.stepErrorCount(step);if(errors)return "error";if(this.completed.has(step.id))return "complete";return this.reachable(step,steps)?"incomplete":"locked"},reachable(step,steps=this.visibleSteps()){if(this.model.mode==="free")return true;for(const before of steps){if(before===step)return true;if(before.type==="form"&&!this.completed.has(before.id))return false}
123
+ root.setAttribute("aria-labelledby",title.id);root.setAttribute("role","group");this.layout=h("div","cx-flow-layout");this.rail=h("div","cx-flow-rail");this.railList=h("ol","cx-flow-marks");this.rail.appendChild(this.railList);this.main=h("div","cx-flow-main");this.stage=h("div","cx-flow-stage");this.actions=h("div","cx-flow-actions");this.main.append(this.stage,this.actions);this.live=h("div","cx-sr-only");this.live.setAttribute("aria-live","polite");this.layout.append(this.rail,this.main);root.append(head,this.layout,this.live);this.head=head;if(this.model.container!=="inline")this.buildContainer();else this.host.appendChild(root);this.sizer=this.dialog||this.host;const signal=this.lifetime.signal;root.addEventListener("input",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("change",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("focusout",event=>this.fieldEvent(event,"blur"),{signal});root.addEventListener("keydown",event=>this.key(event),{signal});this.narrow=false;if(global.ResizeObserver){this.resize=new ResizeObserver(()=>this.measure());this.resize.observe(this.sizer)}
124
+ this.buildPresets?.();this.measure(true);this.render();this.startHostFeatures?.()},fieldEvent(event,source){const wrapper=event.target.closest?.("[data-cx-field]");const field=wrapper&&this.model.fields.get(wrapper.dataset.cxField);if(!field||this.phase!=="form")return;if(source==="blur"){if(wrapper.contains(event.relatedTarget))return;this.changed(field,"blur");this.checkField(field);return}
125
+ this.changed(field,source)},measure(first){const width=this.sizer.clientWidth||global.innerWidth,narrow=width<FLOW_NARROW;if(!first&&width===this.width)return;const changed=narrow!==this.narrow||this.railTooWide&&width>this.width;this.width=width;this.narrow=narrow;if(changed){this.railTooWide=false;if(!first)this.render()}
126
+ else if(!first)this.fitRail()},orientation(){if(this.visibleSteps().length<=1)return "single";if(this.narrow)return "stack";return this.model.orientation==="auto"?(this.visibleSteps().length>6||this.railTooWide?"vertical":"horizontal"):this.model.orientation},fitRail(){if(this.model.orientation!=="auto"||this.railTooWide||this.orientation()!=="horizontal"||!this.rail.clientWidth)return;if(this.rail.scrollWidth>this.rail.clientWidth+1){this.railTooWide=true;this.render()}},render(){const active=document.activeElement;const steps=this.visibleSteps();if(this.phase==="form"&&this.step!==this.hub&&!steps.includes(this.step)){const from=this.step;this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(from))||steps[steps.length-1];this.reviewStale=true;this.emit("flowStepChanged",{from:from.id,to:this.step.id},"onStepChange")}
127
+ const orientation=this.orientation();this.layout.className="cx-flow-layout cx-flow-"+orientation;this.root.classList.toggle("cx-flow-narrow",this.narrow);if(orientation==="horizontal"||orientation==="vertical"){this.rail.setAttribute("role","navigation");this.rail.setAttribute("aria-label",this.labels.flowSteps)}
128
+ else{this.rail.removeAttribute("role");this.rail.removeAttribute("aria-label")}
129
+ this.renderStage();this.renderRail();this.renderActions();if(active&&active!==document.activeElement&&active.isConnected&&this.root.contains(active))active.focus({preventScroll:true});this.fitRail()},stepState(step,steps){if(this.phase!=="form")return "complete";if(step===this.step)return "current";const errors=this.stepErrorCount(step);if(errors)return "error";if(this.completed.has(step.id))return "complete";return this.reachable(step,steps)?"incomplete":"locked"},reachable(step,steps=this.visibleSteps()){if(this.model.mode==="free")return true;for(const before of steps){if(before===step)return true;if(before.asks&&!this.completed.has(before.id))return false}
129
130
  return false},renderRail(){if(!this.root)return;const steps=this.visibleSteps(),stack=this.orientation()==="stack",labels=this.labels;if(!steps.includes(this.step))this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(this.step))||steps[steps.length-1];const key=steps.map(step=>step.id).join()+(stack?"|stack":"");if(key!==this.railKey){this.railKey=key;this.railItems=new Map();this.railList.replaceChildren(...steps.map(step=>{const item=h("li","cx-flow-mark"),button=h("button","cx-flow-mark-btn");button.type="button";button.addEventListener("click",()=>{if(button.getAttribute("aria-disabled")!=="true"&&step!==this.step)this.goTo(step.id).catch(error=>this.report(error,"goTo"))});item.appendChild(button);this.railItems.set(step.id,item);return item}))}
130
131
  steps.forEach((step,index)=>{const state=this.stepState(step,steps),item=this.railItems.get(step.id),button=item.firstChild,number=h("span","cx-flow-num");const text=h("span","cx-flow-mark-text"),title=h("span","cx-flow-mark-title"),note=h("span","cx-flow-mark-state");item.dataset.cxState=state;setText(number,state==="complete"?"✓":state==="error"?"!":String(index+1));number.setAttribute("aria-hidden","true");setText(title,step.title||labels.flowReviewTitle);const errors=this.stepErrorCount(step);setText(note,state==="current"?labels.flowStepCurrent:state==="error"?fill(labels.flowStepErrors,{count:errors}):
131
132
  state==="complete"?labels.flowStepComplete:state==="locked"?labels.flowStepLocked:step.optional?labels.flowStepOptional:labels.flowStepNotStarted);note.className="cx-flow-mark-state" +(state==="locked"||state==="incomplete"?" cx-sr-only":"");text.append(title,note);const signature=state+"\u0000"+number.textContent+"\u0000"+note.textContent+"\u0000"+title.textContent;if(button._cxSignature!==signature){button._cxSignature=signature;button.replaceChildren(number,text)}
132
- if(state==="current")button.setAttribute("aria-current","step");else button.removeAttribute("aria-current");if(state==="locked"||this.phase!=="form")button.setAttribute("aria-disabled","true");else button.removeAttribute("aria-disabled");if(stack&&step===this.step&&this.phase==="form"&&this.main.parentNode!==item)item.appendChild(this.main)});if(!stack&&this.main.parentNode!==this.layout)this.layout.appendChild(this.main);if(stack&&this.phase!=="form")this.railList.after(this.main)},renderActions(){const labels=this.labels,steps=this.visibleSteps(),at=steps.indexOf(this.step),last=at===steps.length-1;this.actions.replaceChildren();this.actions.hidden=this.phase!=="form";if(this.phase!=="form")return;const back=h("button","cx-flow-btn cx-flow-back"),next=h("button","cx-flow-btn cx-primary cx-flow-next");back.type=next.type="button";setText(back,labels.flowBack);back.hidden=at<=0;back.addEventListener("click",()=>this.back().catch(error=>this.report(error,"back")));setText(next,last?this.model.submitLabel||labels.flowSubmit:labels.flowNext);next.addEventListener("click",()=>(last?this.submit():this.next()).catch(error=>this.report(error,last?"submit":"next")));if(this.busy){next.disabled=back.disabled=true;next.setAttribute("aria-busy","true");if(last)setText(next,labels.flowSubmitting)}
133
- const spacer=h("span","cx-flow-spacer");this.actions.append(back,spacer,next)},key(event){if(event.defaultPrevented||event.isComposing||this.phase!=="form")return;const target=event.target;if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)&&this.step.type==="review"){event.preventDefault();this.submit().catch(error=>this.report(error,"submit"));return}
133
+ if(state==="current")button.setAttribute("aria-current","step");else button.removeAttribute("aria-current");if(state==="locked"||this.phase!=="form")button.setAttribute("aria-disabled","true");else button.removeAttribute("aria-disabled");if(stack&&step===this.step&&this.phase==="form"&&this.main.parentNode!==item)item.appendChild(this.main)});if(!stack&&this.main.parentNode!==this.layout)this.layout.appendChild(this.main);if(stack&&this.phase!=="form")this.railList.after(this.main)},lastStep(){const steps=this.visibleSteps();return steps.indexOf(this.step)===steps.length-1},renderActions(){const labels=this.labels,at=this.visibleSteps().indexOf(this.step),last=this.lastStep();this.actions.replaceChildren();this.actions.hidden=this.phase!=="form";if(this.phase!=="form")return;const back=h("button","cx-flow-btn cx-flow-back"),next=h("button","cx-flow-btn cx-primary cx-flow-next");back.type=next.type="button";setText(back,labels.flowBack);back.hidden=at<=0;back.addEventListener("click",()=>this.back().catch(error=>this.report(error,"back")));setText(next,last?this.model.submitLabel||labels.flowSubmit:labels.flowNext);next.addEventListener("click",()=>(last?this.submit():this.next()).catch(error=>this.report(error,last?"submit":"next")));if(this.busy){next.disabled=back.disabled=true;next.setAttribute("aria-busy","true");if(last)setText(next,labels.flowSubmitting)}
134
+ const spacer=h("span","cx-flow-spacer");this.actions.append(back,...[this.draftButton?.(),this.cancelButton?.()].filter(Boolean),spacer,next)},key(event){if(event.defaultPrevented||event.isComposing||this.phase!=="form")return;const target=event.target;if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)&&this.step.type==="review"){event.preventDefault();this.submit().catch(error=>this.report(error,"submit"));return}
134
135
  if(event.key!=="Enter"||event.altKey||event.ctrlKey||event.shiftKey||target.tagName!=="INPUT"||/^(button|submit|checkbox|radio|file)$/.test(target.type) ||
135
- target.closest(".cx-flow-list")||!target.closest(".cx-flow-stage"))return;event.preventDefault();const steps=this.visibleSteps();(steps.indexOf(this.step)===steps.length-1?this.submit():this.next()).catch(error=>this.report(error,"next"))},announce(text){setText(this.live,"");setTimeout(()=>setText(this.live,text),30)}});Object.assign(FlowView.prototype,{renderStage(){const step=this.step;if(this.phase==="result"||this.phase==="progress"){this.stage.replaceChildren(this.panel);return}
136
+ target.closest(".cx-flow-list,.cx-flow-records,.cx-flow-pick,.cx-flow-confirm")||!target.closest(".cx-flow-stage"))return;event.preventDefault();(this.lastStep()?this.submit():this.next()).catch(error=>this.report(error,"next"))},announce(text){setText(this.live,"");setTimeout(()=>setText(this.live,text),30)}});Object.assign(FlowView.prototype,{renderStage(){const step=this.step;if(this.phase==="result"||this.phase==="progress"){this.stage.replaceChildren(this.panel);return}
136
137
  if(step.type==="review"){if(this.reviewStale||!this.reviewNode){this.reviewNode=this.reviewSection();this.reviewStale=false}
137
138
  if(this.stage.firstChild!==this.reviewNode)this.stage.replaceChildren(this.reviewNode);return}
138
139
  let section=this.sections.get(step.id);if(!section){section=step.type==="info"?this.stepShell(step):this.formSection(step);this.sections.set(step.id,section)}
139
140
  section.querySelector(":scope > .cx-flow-answer-banner")?.remove();if(this.banner&&step===this.visibleSteps().at(-1))section._cxSummary.after(this.answerBanner());if(this.stage.childNodes.length!==1||this.stage.firstChild!==section)this.stage.replaceChildren(section)},stepShell(step){const section=h("section","cx-flow-step"),heading=h("h3","cx-flow-step-title");heading.id=this.id+"-s-"+step.id;heading.tabIndex=-1;setText(heading,step.title||this.labels.flowReviewTitle);section.setAttribute("aria-labelledby",heading.id);section.appendChild(heading);if(step.description){const description=h("p","cx-flow-step-desc");setText(description,step.description);section.appendChild(description)}
140
141
  const summary=h("div","cx-flow-summary");summary.setAttribute("role","alert");summary.tabIndex=-1;summary.hidden=true;section.appendChild(summary);section._cxHeading=heading;section._cxSummary=summary;return section},formSection(step){const section=this.stepShell(step);section._cxBlocks=[];for(const block of step.blocks){const element=h("div",block.section?"cx-flow-section":"cx-flow-block"),grid=h("div","cx-flow-grid");if(block.section){const title=h("h4","cx-flow-section-title");title.id=this.id+"-x-"+block.section.id;setText(title,block.section.label);element.setAttribute("role","group");element.setAttribute("aria-labelledby",title.id);element.appendChild(title);if(block.section.description){const description=h("p","cx-flow-section-desc");setText(description,block.section.description);element.appendChild(description)}}
141
142
  for(const field of block.fields){const fv=flowFieldView(this,field);this.fields.set(field.id,fv);fv.mount(field.type==="secret"?null:flowCopy(this.values.get(field.id)??null));fv.element.hidden=!this.visible(field);if(this.errors.has(field.id))fv.message(this.errors.get(field.id));grid.appendChild(fv.element)}
142
- element.appendChild(grid);element.hidden=!block.fields.some(field=>this.visible(field));section._cxBlocks.push({element,fields:block.fields});section.appendChild(element)}
143
- return section},async go(target,direction,quiet){if(!target||this.destroyed)return false;const from=this.step,generation=this.generation;if(target!==from&&!this.emit("flowStepChanging",{from:from.id,to:target.id,direction},null,true))return false;if(this.destroyed||this.generation!==generation||this.step!==from)return false;for(const fv of this.fields.values())fv.instance?.conceal?.();this.step=target;this.reviewStale=true;for(const field of target.blocks?.flatMap(block=>block.fields)||[])this.fields.get(field.id)?.message(this.errors.get(field.id)??null);this.render();const steps=this.visibleSteps(),heading=this.stage.querySelector(".cx-flow-step-title");if(!quiet){heading?.focus({preventScroll:true});this.root.scrollIntoView?.({block:"nearest"});this.announce(fill(this.labels.flowStepAnnounce,{current:steps.indexOf(target)+ 1,count:steps.length,title:target.title||this.labels.flowReviewTitle}))}
144
- if(target!==from)this.emit("flowStepChanged",{from:from.id,to:target.id},"onStepChange");return true},async next(){if(this.busy||this.phase!=="form")return false;const step=this.step,steps=this.visibleSteps(),pressed=document.activeElement,generation=this.generation;if(step.type==="form"){const ticket=this.beginOperation();this.renderActions();let result;try{result=await ticket.wait(this.validateStep(step,true,ticket.current));if(!ticket.current())return false}finally{if(ticket.current())this.retireOperation()}
143
+ element.appendChild(grid);element.hidden=!block.section?.host&&!block.fields.some(field=>this.visible(field));section._cxBlocks.push({element,fields:block.fields,section:block.section});section.appendChild(element)}
144
+ flowEach("built",this,step,section);return section},async go(target,direction,quiet){if(!target||this.destroyed)return false;const from=this.step,generation=this.generation;if(target!==from&&!this.emit("flowStepChanging",{from:from.id,to:target.id,direction},null,true))return false;if(this.destroyed||this.generation!==generation||this.step!==from)return false;for(const fv of this.fields.values())fv.instance?.conceal?.();this.step=target;this.reviewStale=true;for(const field of target.blocks?.flatMap(block=>block.fields)||[])this.fields.get(field.id)?.message(this.errors.get(field.id)??null);this.render();const steps=this.visibleSteps(),heading=this.stage.querySelector(".cx-flow-step-title");if(!quiet){(target===this.hub&&this.taskButton(from)||heading)?.focus({preventScroll:true});this.root.scrollIntoView?.({block:"nearest"});this.announce(target===this.hub?this.hubAnnouncement():fill(this.labels.flowStepAnnounce,{current:steps.indexOf(target)+ 1,count:steps.length,title:target.title||this.labels.flowReviewTitle}))}
145
+ if(target!==from){this.emit("flowStepChanged",{from:from.id,to:target.id},"onStepChange");this.stepMoved?.()}
146
+ return true},async next(){if(this.busy||this.phase!=="form"||this.step===this.hub)return false;const step=this.step,steps=this.visibleSteps(),pressed=document.activeElement,generation=this.generation;if(step.asks){const ticket=this.beginOperation();this.renderActions();let result;try{result=await ticket.wait(this.validateStep(step,true,ticket.current));if(!ticket.current())return false}finally{if(ticket.current())this.retireOperation()}
145
147
  if(this.destroyed||generation!==this.generation||this.step!==step||result.moved)return false;if(!result.valid){const stayed=document.activeElement===pressed||!this.root.contains(document.activeElement);this.renderRail();this.renderActions();this.showSummary(step,stayed);return false}
146
- const summary=this.sections.get(step.id)?._cxSummary;if(summary)summary.hidden=true;this.completed.add(step.id)}else this.completed.add(step.id);const review=this.reviewStep();const target=this.returnToReview&&review&&this.stepVisible(review)?review:steps[steps.indexOf(step)+ 1];if(target===review)this.returnToReview=false;return this.go(target,"forward")},async back(){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),at=steps.indexOf(this.step);return at>0?this.go(steps[at-1],"back"):false},async goTo(id){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),target=steps.find(step=>step.id===id);if(!target)return false;const open=this.reachable(target,steps)?target:steps.find(step=>step.type==="form"&&!this.completed.has(step.id))||target;return this.go(open,steps.indexOf(open)<steps.indexOf(this.step)?"back":"forward")},change(field){this.returnToReview=true;return this.go(field.step,"back").then(moved=>{if(moved){const fv=this.fields.get(field.id);if(fv)fv.focus()}
148
+ const summary=this.sections.get(step.id)?._cxSummary;if(summary)summary.hidden=true;this.completed.add(step.id)}else this.completed.add(step.id);const review=this.reviewStep();const target=this.returnToReview&&review&&this.stepVisible(review)?review:this.hub||steps[steps.indexOf(step)+ 1];if(target===review)this.returnToReview=false;return this.go(target,"forward")},async back(){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),at=steps.indexOf(this.step);return at>0?this.go(steps[at-1],"back"):false},async goTo(id){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),target=steps.find(step=>step.id===id);if(!target)return false;const open=this.reachable(target,steps)?target:steps.find(step=>step.asks&&!this.completed.has(step.id))||target;return this.go(open,steps.indexOf(open)<steps.indexOf(this.step)?"back":"forward")},change(field){this.returnToReview=true;return this.go(field.step,"back").then(moved=>{if(moved){const fv=this.fields.get(field.id);if(fv)fv.focus()}
147
149
  return moved})}});const FLOW_ITEM_RULES={email:value=>/^[^\s@]+@[^\s@]+\.[^\s@.]+$/.test(value),
148
150
  host:value=>value.length<=253&&/^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.?$/.test(value),
149
151
  ip:value=>flowIPv4(value)||flowIPv6(value),cidr:value=>{const[address,prefix,extra]=value.split("/");return extra===undefined&&/^\d{1,3}$/.test(prefix || "") && (flowIPv4(address) && Number(prefix) <= 32 || flowIPv6(address) && Number(prefix) <= 128); },
150
152
  url:value=>{try{const url=new URL(value);return url.protocol==="https:"||url.protocol==="http:"}catch(error){return false}}};function flowIPv4(value){return /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(value); }
151
153
  function flowIPv6(value){if(!/^[0-9A-Fa-f:.]{2,45}$/.test(value) || !value.includes(":")) return false; try { return new URL("http://[" + value + "]/").hostname.length > 2; } catch (error) { return false; } }
152
154
  Object.assign(FlowView.prototype,{problemNow(field){const fv=this.fields.get(field.id),labels=this.labels,title=field.label;if(fv?.pending)return fv.pending;if(field.type==="secret"){const value=this.secrets.get(field.id)??null;if(value===null)return field.required&&!this.keeps(field)?fill(labels.gridEditRequired,{column:title}):null;return formCheck(field.spec,value,"text",title,labels,String)}
153
- const value=this.values.get(field.id)??null;if(field.type==="toggle")return field.required&&value!==true?fill(labels.gridEditRequired,{column:title}):null;if(field.kind==="list"){const items=value||[],rules=field.items;if(!items.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(rules.min!==undefined&&items.length<rules.min)return fill(labels.flowItemsMin,{field:title,min:rules.min});if(rules.max!==undefined&&items.length>rules.max)return fill(labels.flowItemsMax,{field:title,max:rules.max});const name=rules.rule&&"flowItem"+rules.rule[0].toUpperCase()+ rules.rule.slice(1);for(const item of items){if(rules.rule&&!FLOW_ITEM_RULES[rules.rule](item))return field.spec.rules.message||fill(labels[name],{item});if(rules.pattern&&!rules.pattern.test(item))return field.spec.rules.message||fill(labels.flowItemPattern,{item})}
155
+ const value=this.values.get(field.id)??null;if(field.type==="toggle")return field.required&&value!==true?fill(labels.gridEditRequired,{column:title}):null;if(field.kind==="records")return flowHost("records").problem(this,field,value||[]);if(field.kind==="list"||field.kind==="pick"){const items=value||[],rules=field.items;if(!items.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(rules.min!==undefined&&items.length<rules.min)return fill(labels.flowItemsMin,{field:title,min:rules.min});if(rules.max!==undefined&&items.length>rules.max)return fill(labels.flowItemsMax,{field:title,max:rules.max});const name=rules.rule&&"flowItem"+rules.rule[0].toUpperCase()+ rules.rule.slice(1);for(const item of items){if(rules.rule&&!FLOW_ITEM_RULES[rules.rule](item))return field.spec.rules.message||fill(labels[name],{item});if(rules.pattern&&!rules.pattern.test(item))return field.spec.rules.message||fill(labels.flowItemPattern,{item})}
154
156
  return null}
155
157
  if(field.kind==="duration")return formCheck(field.spec,value===null?null:flowSeconds(value),"number",title,labels,seconds=>flowWords(this,seconds));const shown=bound=>field.type==="datetime"?flowDisplay(this,field,bound):typeof bound==="number"?numberFormat(this.lang,0,20).format(bound):String(bound);return formCheck(field.spec,value,field.kind==="number"||field.kind==="datetime"?field.kind:"text",title,labels,shown)},async problem(field){const own=this.problemNow(field);if(own||field.type==="secret")return own;const validator=this.hooks.validators&&Object.hasOwn(this.hooks.validators,field.id)?this.hooks.validators[field.id]:null;if(typeof validator!=="function")return null;const answer=await this.hook("validators."+field.id,()=>validator(flowCopy(this.values.get(field.id)??null),this.getValues()));return answer.failed||(typeof answer.value==="string"&&/\S/.test(answer.value) ? answer.value : null);
156
158
  },async checkField(field){const revision=this.answerRevision,generation=this.generation;const ticket=(this.checks||=new Map()).set(field.id,{}).get(field.id),message=await this.problem(field);if(this.checks.get(field.id)!==ticket||this.destroyed||revision!==this.answerRevision||generation!==this.generation)return message;this.setError(field,message||this.hostErrors.get(field.id)||null);this.renderRail();return message},setError(field,message){if(message)this.errors.set(field.id,message);else this.errors.delete(field.id);const fv=this.fields.get(field.id);fv?.message(message);if(field.type==="list"&&fv?.instance.flag)fv.instance.flag(item=>!!message&&!!item&&this.itemFails(field,item))},itemFails(field,item){const rules=field.items;return!!rules.rule&&!FLOW_ITEM_RULES[rules.rule](item)||!!rules.pattern&&!rules.pattern.test(item)},answerKey(step){return JSON.stringify([this.answerRevision,this.stepFields(step).map(field=>field.type==="secret"?this.secrets.has(field.id):[this.values.get(field.id)??null,this.fields.get(field.id)?.pending||null])])},stepErrorCount(step){return this.stepFields(step).filter(field=>this.errors.has(field.id)).length +(this.stepErrors.has(step.id)||this.hostStepErrors.has(step.id)?1:0)},bucket(answer){const result={errors:[],warnings:[],fields:Object.create(null)},text=value=>typeof value==="string"&&/\S/.test(value);
@@ -158,19 +160,20 @@ if(answer.failed){result.errors.push(answer.failed);return result}
158
160
  const value=answer.value;if(text(value))result.errors.push(value);else if(flowIs(value)){for(const[name,key]of[["errors","errors"],["warnings","warnings"]])if(Array.isArray(value[name]))result[key].push(...value[name].filter(text));if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))result.fields[id]=message}
159
161
  return result},async validateStep(step,show,current=()=>true){const generation=this.generation;let fields,messages,result,moved=false,stable=false;for(let round=0;round<4;round++,moved=true){const before=this.answerKey(step);fields=this.stepFields(step);messages=await Promise.all(fields.map(field=>this.problem(field)));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true};result={errors:[],warnings:[],fields:Object.create(null)};if(!messages.some(Boolean)&&typeof this.hooks.validateStep==="function")
160
162
  result=this.bucket(await this.hook("validateStep",()=>this.hooks.validateStep(step.id,this.getValues())));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true};if(this.answerKey(step)===before){stable=true;break}}
161
- if(this.destroyed||!current())return{valid:false,errors:[],warnings:[]};if(!stable)return{valid:false,errors:[{step:step.id,message:this.labels.flowNotSubmitted}],warnings:[],moved:true};const failing=[];fields.forEach((field,index)=>{const local=messages[index]||result.fields[field.id]||null,message=local||this.hostErrors.get(field.id)||null;if(show||!message)this.setError(field,message);else this.errors.set(field.id,message);if(local)failing.push({field:field.id,step:step.id,message:local})});if(result.errors.length)this.stepErrors.set(step.id,result.errors.join(" "));else this.stepErrors.delete(step.id);const errors=failing;for(const message of result.errors)errors.push({step:step.id,message});return{valid:stable&&!errors.length,errors,warnings:result.warnings.map(message=>({step:step.id,message})),moved}},async validateAll(current=()=>true){const generation=this.generation;for(let round=0;round<4;round++){const revision=this.answerRevision,result=await this.validateAllOnce(generation,current);if(this.destroyed||generation!==this.generation||!current())return{valid:false,errors:[],warnings:[]};if(revision===this.answerRevision)return result}
162
- return{valid:false,errors:[{message:this.labels.flowNotSubmitted}],warnings:[]}},async validateAllOnce(generation,current){const revision=this.answerRevision,results=[],steps=this.visibleSteps().filter(step=>step.type==="form");for(const step of steps){results.push(await this.validateStep(step,false,current));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]}}
163
+ if(this.destroyed||!current())return{valid:false,errors:[],warnings:[]};if(!stable)return{valid:false,errors:[{step:step.id,message:this.labels.flowNotSubmitted}],warnings:[],moved:true};const failing=[];fields.forEach((field,index)=>{const local=messages[index]||result.fields[field.id]||null,message=local||this.hostErrors.get(field.id)||null;if(show||!message)this.setError(field,message);else this.errors.set(field.id,message);if(local)failing.push({field:field.id,step:step.id,message:local})});if(step.checks.length){result.errors.push(...await this.checkGate(step,show&&!failing.length&&!result.errors.length));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true}}
164
+ if(result.errors.length)this.stepErrors.set(step.id,result.errors.join(" "));else this.stepErrors.delete(step.id);const errors=failing;for(const message of result.errors)errors.push({step:step.id,message});return{valid:stable&&!errors.length,errors,warnings:result.warnings.map(message=>({step:step.id,message})),moved}},async validateAll(current=()=>true){const generation=this.generation;for(let round=0;round<4;round++){const revision=this.answerRevision,result=await this.validateAllOnce(generation,current);if(this.destroyed||generation!==this.generation||!current())return{valid:false,errors:[],warnings:[]};if(revision===this.answerRevision)return result}
165
+ return{valid:false,errors:[{message:this.labels.flowNotSubmitted}],warnings:[]}},async validateAllOnce(generation,current){const revision=this.answerRevision,results=[],steps=this.visibleSteps().filter(step=>step.asks);for(const step of steps){results.push(await this.validateStep(step,false,current));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]}}
163
166
  if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]};const errors=results.flatMap(result=>result.errors),warnings=results.flatMap(result=>result.warnings);steps.forEach((step,index)=>{if(results[index].valid)this.completed.add(step.id);else this.completed.delete(step.id)});if(!errors.length&&typeof this.hooks.validateFlow==="function"){const result=this.bucket(await this.hook("validateFlow",()=>this.hooks.validateFlow(this.getValues())));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]};for(const[id,message]of Object.entries(result.fields))if(this.visible(this.model.fields.get(id))){this.setError(this.model.fields.get(id),message);errors.push({field:id,step:this.model.fields.get(id).step.id,message})}
164
167
  errors.push(...result.errors.map(message=>({message})));warnings.push(...result.warnings.map(message=>({message})))}
165
168
  return{valid:results.every(result=>result.valid)&&!errors.length,errors,warnings}},async validate(stepId){if(stepId===undefined){const result=await this.validateAll();this.renderRail();return result}
166
- const step=this.model.steps.find(item=>item.id===stepId);if(!step)flowFail("there is no step '"+stepId+"'");const result=step.type==="form"&&this.stepVisible(step)?await this.validateStep(step,true):{valid:true,errors:[],warnings:[]};this.renderRail();return{valid:result.valid,errors:result.errors,warnings:result.warnings}},setFieldError(id,message){const field=this.model.fields.get(id);if(!field||message!=null&&(typeof message!=="string"||!/\S/.test(message))) flowFail("setFieldError takes a field id and a message or null");
169
+ const step=this.model.steps.find(item=>item.id===stepId);if(!step)flowFail("there is no step '"+stepId+"'");const result=step.asks&&this.stepVisible(step)?await this.validateStep(step,true):{valid:true,errors:[],warnings:[]};this.renderRail();return{valid:result.valid,errors:result.errors,warnings:result.warnings}},setFieldError(id,message){const field=this.model.fields.get(id);if(!field||message!=null&&(typeof message!=="string"||!/\S/.test(message))) flowFail("setFieldError takes a field id and a message or null");
167
170
  if(message)this.hostErrors.set(id,message);else this.hostErrors.delete(id);this.setError(field,message??null);this.renderRail();this.refreshReview()},showSummary(step,focus=true){const section=this.sections.get(step.id),summary=section?._cxSummary;if(!summary)return;const list=h("ul"),title=h("p","cx-flow-summary-title");setText(title,this.labels.flowErrorSummary);for(const field of this.stepFields(step))if(this.errors.has(field.id)){const item=h("li"),link=h("button","cx-flow-link");link.type="button";setText(link,this.errors.get(field.id));link.addEventListener("click",()=>this.fields.get(field.id)?.focus());item.appendChild(link);list.appendChild(item)}
168
171
  const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(stepMessage){const item=h("li");setText(item,stepMessage);list.appendChild(item)}
169
172
  summary.replaceChildren(title,list);summary.hidden=!list.children.length;if(!summary.hidden&&focus)summary.focus({preventScroll:false})}});const flowNormal=value=>flowEmpty(value)?null:value;Object.assign(FlowView.prototype,{initialOf(field){return this.model.initial&&Object.hasOwn(this.model.initial,field.id)?this.model.initial[field.id]:null},isChanged(field){if(field.type==="secret")return this.secrets.has(field.id);return!flowSameValue(flowNormal(this.values.get(field.id)),flowNormal(this.initialOf(field)))},secretText(field){return this.secrets.has(field.id)?this.labels.flowSecretSet:this.keeps(field)?this.labels.flowSecretKept:this.labels.flowSecretNotSet},reviewSection(){const step=this.step,labels=this.labels,section=this.stepShell(step),edit=!!this.model.initial;section.classList.add("cx-flow-review");const status=h("div","cx-flow-banner");status.setAttribute("role","status");section.appendChild(status);if(this.banner)section.appendChild(this.answerBanner());let changesOnly=edit&&(this.showAll===undefined?step.changesOnly:!this.showAll);const lists=h("div","cx-flow-review-lists"),tools=h("div","cx-flow-review-tools");if(edit){const toggle=h("label","cx-flow-switch"),box=h("input"),text=h("span"),count=h("span","cx-flow-count");box.type="checkbox";box.checked=changesOnly;box.setAttribute("role","switch");setText(text,labels.flowChangesOnly);box.addEventListener("change",()=>{this.showAll=!box.checked;changesOnly=box.checked;this.fillReview(lists,changesOnly)});toggle.append(box,text);const changed=[...this.model.fields.values()].filter(field=>this.visible(field)&&this.isChanged(field)).length;setText(count,fill(labels.flowChangedCount,{count:changed}));tools.append(toggle,count);section.appendChild(tools)}
170
- section.appendChild(lists);this.fillReview(lists,changesOnly);if(step.declaration){const declaration=h("p","cx-flow-declaration");setText(declaration,step.declaration);section.appendChild(declaration)}
171
- const ticket=this.reviewTicket={};setText(status,"…");if(this.banner)status.hidden=true;this.reviewChecking=true;this.validateAll().finally(()=>{if(this.reviewTicket===ticket)this.reviewChecking=false}).then(result=>{if(this.reviewTicket!==ticket||this.destroyed)return;this.reviewResult=result;this.fillBanner(status,result);status.hidden=!!this.banner&&result.valid;this.fillReview(lists,changesOnly);this.renderRail()},error=>this.report(error,"validate"));return section},fillBanner(status,result){const labels=this.labels;status.replaceChildren();status.className="cx-flow-banner " +(result.valid?"cx-flow-ready":"cx-flow-problems");const title=h("p","cx-flow-banner-title");setText(title,result.valid?labels.flowReady:fill(labels.flowNotReady,{count:result.errors.length}));status.appendChild(title);const list=h("ul","cx-flow-banner-list");for(const error of result.errors){const item=h("li"),step=this.model.steps.find(entry=>entry.id===error.step),field=error.field&&this.model.fields.get(error.field);if(step){const link=h("button","cx-flow-link");link.type="button";setText(link,(step.title?step.title+": ":"")+ error.message);link.addEventListener("click",()=>(field?this.change(field):(this.returnToReview=true,this.goTo(step.id))).catch(failure=>this.report(failure,"goTo")));item.appendChild(link)}else setText(item,error.message);list.appendChild(item)}
173
+ section.appendChild(lists);this.fillReview(lists,changesOnly);flowEach("review",this,section);if(step.declaration){const declaration=h("p","cx-flow-declaration");setText(declaration,step.declaration);section.appendChild(declaration)}
174
+ flowEach("confirm",this,section,step);const recheck=this.reviewRecheck=()=>{const ticket=this.reviewTicket={};setText(status,"…");if(this.banner)status.hidden=true;this.reviewChecking=true;return this.validateAll().finally(()=>{if(this.reviewTicket===ticket)this.reviewChecking=false}).then(result=>{if(this.reviewTicket!==ticket||this.destroyed)return;this.reviewResult=result;this.fillBanner(status,result);status.hidden=!!this.banner&&result.valid;this.fillReview(lists,changesOnly);this.renderRail()},error=>this.report(error,"validate"))};recheck();return section},fillBanner(status,result){const labels=this.labels;status.replaceChildren();status.className="cx-flow-banner " +(result.valid?"cx-flow-ready":"cx-flow-problems");const title=h("p","cx-flow-banner-title");setText(title,result.valid?labels.flowReady:fill(labels.flowNotReady,{count:result.errors.length}));status.appendChild(title);const list=h("ul","cx-flow-banner-list");for(const error of result.errors){const item=h("li"),step=this.model.steps.find(entry=>entry.id===error.step),field=error.field&&this.model.fields.get(error.field);if(step){const link=h("button","cx-flow-link");link.type="button";setText(link,(step.title?step.title+": ":"")+ error.message);link.addEventListener("click",()=>(field?this.change(field):(this.returnToReview=true,this.goTo(step.id))).catch(failure=>this.report(failure,"goTo")));item.appendChild(link)}else setText(item,error.message);list.appendChild(item)}
172
175
  if(result.warnings.length){const warnings=h("p","cx-flow-banner-warn");setText(warnings,fill(labels.flowWarnings,{count:result.warnings.length}));status.appendChild(warnings);for(const warning of result.warnings){const item=h("li","cx-flow-warning");setText(item,warning.message);list.appendChild(item)}}
173
- if(list.children.length)status.appendChild(list)},fillReview(lists,changesOnly){const labels=this.labels;lists.replaceChildren();let shown=0;for(const step of this.visibleSteps()){if(step.type!=="form")continue;const fields=this.stepFields(step).filter(field=>!changesOnly||this.isChanged(field));const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(!fields.length&&!stepMessage)continue;const group=h("section","cx-flow-review-group"),heading=h("h4","cx-flow-review-title"),list=h("dl","cx-flow-answers");heading.id=this.id+"-r-"+step.id;setText(heading,step.title);group.setAttribute("aria-labelledby",heading.id);group.appendChild(heading);if(stepMessage){const error=h("p","cx-flow-error");setText(error,stepMessage);group.appendChild(error)}
176
+ if(list.children.length)status.appendChild(list)},fillReview(lists,changesOnly){const labels=this.labels;lists.replaceChildren();let shown=0;for(const step of this.visibleSteps()){if(!step.asks)continue;const fields=this.stepFields(step).filter(field=>!changesOnly||this.isChanged(field));const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(!fields.length&&!stepMessage)continue;const group=h("section","cx-flow-review-group"),heading=h("h4","cx-flow-review-title"),list=h("dl","cx-flow-answers");heading.id=this.id+"-r-"+step.id;setText(heading,step.title);group.setAttribute("aria-labelledby",heading.id);group.appendChild(heading);if(stepMessage){const error=h("p","cx-flow-error");setText(error,stepMessage);group.appendChild(error)}
174
177
  for(const field of fields){const row=h("div","cx-flow-answer"),term=h("dt"),value=h("dd","cx-flow-answer-value"),action=h("dd","cx-flow-answer-action");setText(term,field.label);const current=field.type==="secret"?this.secretText(field):flowDisplay(this,field,this.values.get(field.id)??null),now=h("span","cx-flow-now");setText(now,current);if(flowEmpty(this.values.get(field.id))&&field.type!=="secret")now.classList.add("cx-flow-empty");value.appendChild(now);if(this.model.initial&&field.type!=="secret"&&this.isChanged(field)){const was=h("span","cx-flow-was");setText(was,fill(labels.flowWas,{value:flowDisplay(this,field,this.initialOf(field))}));value.appendChild(was);row.classList.add("cx-flow-changed")}
175
178
  if(this.errors.has(field.id)){const error=h("span","cx-flow-error");setText(error,this.errors.get(field.id));value.appendChild(error);row.classList.add("cx-flow-invalid")}
176
179
  if(!field.readOnly){const change=h("button","cx-flow-link cx-flow-change");change.type="button";setText(change,labels.flowChange);change.setAttribute("aria-label",fill(labels.flowChangeField,{field:field.label}));change.addEventListener("click",()=>this.change(field).catch(error=>this.report(error,"change")));action.appendChild(change)}
@@ -179,7 +182,7 @@ group.appendChild(list);lists.appendChild(group)}
179
182
  if(!shown&&changesOnly){const none=h("p","cx-flow-none");setText(none,labels.flowNoChanges);lists.appendChild(none)}}});const flowPointer=path=>"/"+path.split(".").map(part=>part.replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
180
183
  Object.assign(FlowView.prototype,{submission(){const model=this.model,edit=!!model.initial,values={},secrets={},changes=[],patch=[];for(const field of model.fields.values()){if(!this.visible(field))continue;if(field.type==="secret"){if(this.secrets.has(field.id))secrets[field.id]={state:"set"};else if(this.keeps(field))secrets[field.id]={state:"keep"};continue}
181
184
  const value=flowCopy(this.values.get(field.id)??null),from=flowNormal(this.initialOf(field)),to=flowNormal(value);values[field.id]=value;if(edit&&this.isChanged(field))changes.push(Object.assign({field:field.id},field.path?{path:field.path}:{},{from:flowCopy(from),to:flowCopy(to)}));if(!field.path)continue;if(edit){if(!this.isChanged(field))continue;patch.push(to===null?{op:"remove",path:flowPointer(field.path)}:{op:from===null?"add":"replace",path:flowPointer(field.path),value:flowCopy(to)})}else if(to!==null)patch.push({op:"add",path:flowPointer(field.path),value:flowCopy(to)})}
182
- const proposal={kind:"flowSubmission",flow:model.id,version:1,mode:edit?"edit":"create"};if(model.baseline!==null)proposal.baseline=model.baseline;return Object.assign(proposal,{values,secrets,changes,patch})},async submit(){if(this.busy||this.phase!=="form")return null;const ticket=this.beginOperation(),current=ticket.current,wait=ticket.wait;this.banner=null;this.hostErrors.clear();this.hostStepErrors.clear();this.renderActions();try{const result=await wait(this.validateAll(current));if(!current())return null;if(!result.valid){const review=this.reviewStep();if(review&&this.stepVisible(review)){if(this.step!==review)await this.go(review,"forward");else{this.reviewStale=true;this.render()}}
185
+ const proposal={kind:"flowSubmission",flow:model.id,version:1,mode:edit?"edit":"create"};if(model.baseline!==null)proposal.baseline=model.baseline;Object.assign(proposal,{values,secrets,changes,patch});flowEach("proposal",this,proposal);return proposal},async submit(){if(this.busy||this.phase!=="form")return null;const ticket=this.beginOperation(),current=ticket.current,wait=ticket.wait;this.banner=null;this.hostErrors.clear();this.hostStepErrors.clear();this.renderActions();try{const result=await wait(this.validateAll(current));if(!current())return null;if(!result.valid){const review=this.reviewStep();if(review&&this.stepVisible(review)){if(this.step!==review)await this.go(review,"forward");else{this.reviewStale=true;this.render()}}
183
186
  else{const first=result.errors.find(error=>error.step);await this.go(this.model.steps.find(step=>step.id===first?.step)||this.step,"back");if(first?.step)this.showSummary(this.step)}
184
187
  return null}
185
188
  const proposal=this.submission(),secrets=new Map([...this.secrets].filter(([id])=>this.visible(this.model.fields.get(id)))),read=new Set();let answer,answered=false,open=true;const close=ticket.close=()=>{open=false;secrets.clear()};const detail=Object.assign(flowCopy(proposal),{respondWith:value=>{if(!answered&&open){answered=true;answer=Promise.resolve(value);answer.catch(()=>{})}},secret:id=>{if(!open||read.has(id)||!secrets.has(id))return null;read.add(id);return secrets.get(id)}});Object.freeze(detail);const go=this.emit("flowSubmit",detail,null,true);if(!go||!current()){close();return null}
@@ -193,7 +196,7 @@ if(!current()){close();return null}
193
196
  let settled;if(!answered||answer===undefined)settled={status:"error",message:this.labels.flowNoHost};else{const value=await wait(this.hook("respondWith",()=>answer));if(!current()){close();return null}
194
197
  settled=value.failed?{status:"error",message:value.failed}:this.answerOf(value.value)}
195
198
  close();if(!current())return null;this.retireOperation(false);this.settle(settled);return settled}finally{if(current())this.retireOperation()}},answerOf(value){const text=item=>typeof item==="string"&&/\S/.test(item) ? item : null;
196
- if(!flowIs(value)||!["saved","accepted","error","conflict"].includes(value.status))return{status:"error",message:this.labels.flowNotSubmitted};const answer={status:value.status,message:text(value.message),fieldErrors:{},stepErrors:{},result:[],links:[]};if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))answer.fieldErrors[id]=message;if(flowIs(value.stepErrors))for(const[id,message]of Object.entries(value.stepErrors))if(this.model.steps.some(step=>step.id===id)&&text(message))answer.stepErrors[id]=message;if(Array.isArray(value.result))for(const row of value.result.slice(0,50))if(flowIs(row)&&text(row.label)&&flowScalar(row.value))answer.result.push({label:row.label,value:String(row.value),once:row.once===true});if(Array.isArray(value.links))for(const link of value.links.slice(0,10))if(flowIs(link)&&text(link.label)&&typeof link.href==="string"&&flowSafeHref(link.href))answer.links.push({label:link.label,href:link.href});return answer},settle(answer){answer=Object.assign({message:null,fieldErrors:{},stepErrors:{},result:[],links:[]},answer);if(answer.status==="saved"||answer.status==="accepted"){this.phase=answer.status==="saved"?"result":"progress";this.panel=answer.status==="saved"?this.resultPanel(answer):this.progressPanel(answer);this.render();this.panel.querySelector("h3")?.focus();this.emit("flowSubmitted",flowCopy(answer),"onSubmitted");return}
199
+ if(!flowIs(value)||!["saved","accepted","error","conflict"].includes(value.status))return{status:"error",message:this.labels.flowNotSubmitted};const answer={status:value.status,message:text(value.message),fieldErrors:{},stepErrors:{},result:[],links:[]};if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))answer.fieldErrors[id]=message;if(flowIs(value.stepErrors))for(const[id,message]of Object.entries(value.stepErrors))if(this.model.steps.some(step=>step.id===id)&&text(message))answer.stepErrors[id]=message;if(Array.isArray(value.result))for(const row of value.result.slice(0,50))if(flowIs(row)&&text(row.label)&&flowScalar(row.value))answer.result.push({label:row.label,value:String(row.value),once:row.once===true});if(Array.isArray(value.links))for(const link of value.links.slice(0,10))if(flowIs(link)&&text(link.label)&&typeof link.href==="string"&&flowSafeHref(link.href))answer.links.push({label:link.label,href:link.href});return answer},settle(answer){answer=Object.assign({message:null,fieldErrors:{},stepErrors:{},result:[],links:[]},answer);flowEach("settled",this,answer);if(answer.status==="saved"||answer.status==="accepted"){this.phase=answer.status==="saved"?"result":"progress";this.panel=answer.status==="saved"?this.resultPanel(answer):this.progressPanel(answer);this.render();this.panel.querySelector("h3")?.focus();this.emit("flowSubmitted",flowCopy(answer),"onSubmitted");return}
197
200
  this.phase="form";for(const[id,message]of Object.entries(answer.fieldErrors)){this.hostErrors.set(id,message);this.setError(this.model.fields.get(id),message)}
198
201
  for(const[id,message]of Object.entries(answer.stepErrors))this.hostStepErrors.set(id,message);this.reviewStale=true;this.banner=answer;const review=this.reviewStep();const target=review&&this.stepVisible(review)?review:this.visibleSteps().find(step=>this.hostStepErrors.has(step.id)||this.stepFields(step).some(field=>this.hostErrors.has(field.id)))||this.step;if(target===this.step){this.render();if(!review)this.showSummary(this.step);this.stage.querySelector(".cx-flow-answer-banner")?.focus()}
199
202
  else this.go(target,"forward").then(()=>{if(!review)this.showSummary(target);else this.stage.querySelector(".cx-flow-answer-banner")?.focus()});this.emit("flowSubmitted",flowCopy(answer),"onSubmitted")},answerBanner(){const answer=this.banner,labels=this.labels,banner=h("div","cx-flow-answer-banner cx-flow-problems"),text=h("p","cx-flow-banner-title");banner.setAttribute("role","alert");banner.tabIndex=-1;setText(text,answer.message||(answer.status==="conflict"?labels.flowConflict:labels.flowNotSubmitted));banner.appendChild(text);if(answer.status==="conflict"){const reload=h("button","cx-flow-btn");reload.type="button";setText(reload,labels.flowReload);reload.addEventListener("click",()=>this.emit("flowReload",{baseline:this.model.baseline},"onReload"));banner.appendChild(reload)}
@@ -210,5 +213,5 @@ item.append(term,value);list.appendChild(item)}
210
213
  panel.appendChild(list)}
211
214
  if(answer.links.length){const links=h("ul","cx-flow-links");for(const link of answer.links){const item=h("li"),anchor=h("a","cx-flow-link");anchor.href=link.href;setText(anchor,link.label);item.appendChild(anchor);links.appendChild(item)}
212
215
  panel.appendChild(links)}
213
- const actions=h("div","cx-flow-actions cx-flow-panel-actions"),again=h("button","cx-flow-btn");again.type="button";setText(again,labels.flowStartAgain);const acknowledged=()=>{again.disabled=[...panel.querySelectorAll(".cx-flow-saved-it")].some(box=>!box.checked)};panel.addEventListener("change",acknowledged);acknowledged();again.addEventListener("click",()=>{this.panel=null;this.reset();this.stage.querySelector(".cx-flow-step-title")?.focus()});actions.appendChild(again);panel.appendChild(actions);return panel},progressPanel(answer){const panel=this.panelShell("progress",this.labels.flowAccepted),text=h("p","cx-flow-panel-text"),bar=h("progress","cx-flow-bar");setText(text,answer.message||"");bar.setAttribute("aria-labelledby",panel.firstChild.id);panel.append(text,bar);return panel}});function flowCreate(host,options,editors){const view=new FlowView(host,options,editors);const impl={ready:Promise.resolve().then(()=>{if(!view.destroyed)host.setAttribute("data-cx-ready","");view.emit("flowReady",{flow:view.model.id,step:view.step.id},"onReady");return impl}),next:()=>view.next(),back:()=>view.back(),goTo:id=>view.goTo(id),getStep:()=>view.getStep(),getValues:()=>view.getValues(),setValues:values=>view.setValues(values),getState:()=>view.getState(),setState:state=>view.setState(state),isDirty:()=>view.isDirty(),reset:()=>view.reset(),validate:id=>view.validate(id),setFieldError:(id,message)=>view.setFieldError(id,message),submit:()=>view.submit(),setProgress:progress=>view.setProgress(progress),complete:answer=>view.complete(answer),destroy:()=>view.destroy()};return impl}
214
- FLOW_IMPL.create=flowCreate;installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow",part])})(typeof window!=="undefined"?window:globalThis);
216
+ const actions=h("div","cx-flow-actions cx-flow-panel-actions"),again=h("button","cx-flow-btn");again.type="button";setText(again,labels.flowStartAgain);const acknowledged=()=>{again.disabled=[...panel.querySelectorAll(".cx-flow-saved-it")].some(box=>!box.checked)};panel.addEventListener("change",acknowledged);acknowledged();again.addEventListener("click",()=>{this.panel=null;this.reset();this.stage.querySelector(".cx-flow-step-title")?.focus()});actions.appendChild(again);panel.appendChild(actions);return panel},progressPanel(answer){const panel=this.panelShell("progress",this.labels.flowAccepted),text=h("p","cx-flow-panel-text"),bar=h("progress","cx-flow-bar");setText(text,answer.message||"");bar.setAttribute("aria-labelledby",panel.firstChild.id);panel.append(text,bar);return panel}});function flowCreate(host,options,editors){const view=new FlowView(host,options,editors);const impl={ready:Promise.resolve().then(()=>{if(!view.destroyed)host.setAttribute("data-cx-ready","");view.emit("flowReady",{flow:view.model.id,step:view.step.id},"onReady");return impl}),next:()=>view.next(),back:()=>view.back(),goTo:id=>view.goTo(id),getStep:()=>view.getStep(),getValues:()=>view.getValues(),setValues:values=>view.setValues(values),getState:()=>view.getState(),setState:state=>view.setState(state),isDirty:()=>view.isDirty(),reset:()=>view.reset(),validate:id=>view.validate(id),setFieldError:(id,message)=>view.setFieldError(id,message),submit:()=>view.submit(),setProgress:progress=>view.setProgress(progress),complete:answer=>view.complete(answer),destroy:()=>view.destroy()};const part=(name,none)=>(...args)=>view[name]?view[name](...args):none??flowFail("this flow does not use "+name);return Object.assign(impl,{runCheck:part("runCheck"),refreshOptions:part("refreshOptions"),setStepState:part("setStepState"),setPreset:part("setPreset"),saveDraft:()=>part("saveDraft",false)(true),open:()=>part("open",false)(),close:()=>part("close",false)()})}
217
+ Object.assign(FLOW_IMPL,{create:flowCreate,core:{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow",part])})(typeof window!=="undefined"?window:globalThis);