@evotecit/canopyx 0.3.4 → 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.
- package/CanopyX.Html/Assets/canopyx-flow-checks.css +24 -0
- package/CanopyX.Html/Assets/canopyx-flow-checks.js +13 -0
- package/CanopyX.Html/Assets/canopyx-flow-command.css +14 -0
- package/CanopyX.Html/Assets/canopyx-flow-command.js +19 -0
- package/CanopyX.Html/Assets/canopyx-flow-consent.css +6 -0
- package/CanopyX.Html/Assets/canopyx-flow-consent.js +8 -0
- package/CanopyX.Html/Assets/canopyx-flow-dialog.css +14 -0
- package/CanopyX.Html/Assets/canopyx-flow-dialog.js +3 -0
- package/CanopyX.Html/Assets/canopyx-flow-drafts.css +17 -0
- package/CanopyX.Html/Assets/canopyx-flow-drafts.js +21 -0
- package/CanopyX.Html/Assets/canopyx-flow-endpoint.js +6 -0
- package/CanopyX.Html/Assets/canopyx-flow-entry.css +1 -0
- package/CanopyX.Html/Assets/canopyx-flow-entry.js +6 -2
- package/CanopyX.Html/Assets/canopyx-flow-host.css +5 -0
- package/CanopyX.Html/Assets/canopyx-flow-host.js +4 -0
- package/CanopyX.Html/Assets/canopyx-flow-links.js +6 -0
- package/CanopyX.Html/Assets/canopyx-flow-module.mjs +6 -2
- package/CanopyX.Html/Assets/canopyx-flow-options.js +25 -0
- package/CanopyX.Html/Assets/canopyx-flow-pick.css +19 -0
- package/CanopyX.Html/Assets/canopyx-flow-pick.js +9 -0
- package/CanopyX.Html/Assets/canopyx-flow-preview.css +11 -0
- package/CanopyX.Html/Assets/canopyx-flow-preview.js +9 -0
- package/CanopyX.Html/Assets/canopyx-flow-records.css +9 -0
- package/CanopyX.Html/Assets/canopyx-flow-records.js +12 -0
- package/CanopyX.Html/Assets/canopyx-flow-tasks.css +29 -0
- package/CanopyX.Html/Assets/canopyx-flow-tasks.js +20 -0
- package/CanopyX.Html/Assets/canopyx-flow.js +48 -45
- package/CanopyX.Html/Assets/canopyx-grid-module.mjs +11 -6
- package/CanopyX.Html/Assets/canopyx-grid.css +1 -0
- package/CanopyX.Html/Assets/canopyx-grid.js +11 -6
- package/CanopyX.Html/Assets/canopyx-interactive.js +1 -1
- package/CanopyX.Html/Assets/canopyx-labels-de.js +74 -0
- package/CanopyX.Html/Assets/canopyx-labels-es.js +74 -0
- package/CanopyX.Html/Assets/canopyx-labels-fr.js +74 -0
- package/CanopyX.Html/Assets/canopyx-labels-it.js +74 -0
- package/CanopyX.Html/Assets/canopyx-labels-pl.js +74 -0
- package/CanopyX.Html/Assets/canopyx-module.mjs +1 -1
- package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +11 -6
- package/CanopyX.Html/Assets/canopyx-reporting.css +1 -0
- package/CanopyX.Html/Assets/canopyx-reporting.js +11 -6
- package/CanopyX.Html/Assets/canopyx-tree-tools.js +4 -3
- package/CanopyX.Html/Assets/canopyx.js +1 -1
- package/JavaScript/flow.d.ts +180 -9
- package/JavaScript/grid.d.ts +2 -0
- package/JavaScript/locales/de.mjs +74 -0
- package/JavaScript/locales/es.mjs +74 -0
- package/JavaScript/locales/fr.mjs +74 -0
- package/JavaScript/locales/it.mjs +74 -0
- package/JavaScript/locales/pl.mjs +74 -0
- package/README.md +2 -2
- package/package.json +3 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* The flow-checks part: a button,its state in words with a mark,the host's message and details; the review page's list. */
|
|
2
|
+
.cx-flow-checks{padding-top:var(--cx-size-4px);border-top:var(--cx-size-1px) solid var(--cx-line);}
|
|
3
|
+
.cx-flow-step > .cx-flow-checks{margin-top:var(--cx-flow-gap);}
|
|
4
|
+
.cx-flow-check-item{margin-top:var(--cx-size-10px);}
|
|
5
|
+
.cx-flow-check-head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-6px) var(--cx-size-12px);}
|
|
6
|
+
.cx-flow-check-status{display:inline-flex;align-items:center;gap:var(--cx-size-6px);font-size:var(--cx-size-13d5px);color:var(--cx-text-2);}
|
|
7
|
+
.cx-flow-check-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;}
|
|
8
|
+
.cx-flow-check-item[data-cx-state="pass"] .cx-flow-check-mark{color:var(--cx-tone-success);background:var(--cx-tone-success-bg);}
|
|
9
|
+
.cx-flow-check-item[data-cx-state="warning"] .cx-flow-check-mark{color:var(--cx-tone-warning);background:var(--cx-tone-warning-bg);}
|
|
10
|
+
.cx-flow-check-item[data-cx-state="warning"] .cx-flow-check-status{color:var(--cx-tone-warning);}
|
|
11
|
+
.cx-flow-check-item[data-cx-state="fail"] .cx-flow-check-mark{color:var(--cx-tone-danger);background:var(--cx-tone-danger-bg);}
|
|
12
|
+
.cx-flow-check-item[data-cx-state="fail"] .cx-flow-check-status{color:var(--cx-tone-danger);}
|
|
13
|
+
.cx-flow-check-item[data-cx-state="stale"] .cx-flow-check-status{font-style:italic;}
|
|
14
|
+
.cx-flow-check-message{margin-top:var(--cx-size-4px);font-size:var(--cx-size-13d5px);}
|
|
15
|
+
.cx-flow-check-details{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:var(--cx-size-2px) var(--cx-size-14px);margin-top:var(--cx-size-6px);padding:var(--cx-size-8px) var(--cx-size-10px);border-radius:var(--cx-radius-sm);background:var(--cx-head-bg);font-size:var(--cx-size-13px);}
|
|
16
|
+
.cx-flow-check-details > div{display:contents;}
|
|
17
|
+
.cx-flow-check-details dt{color:var(--cx-muted);}
|
|
18
|
+
.cx-flow-check-details dd{margin:0;overflow-wrap:anywhere;}
|
|
19
|
+
.cx-flow-check-ack{margin-top:var(--cx-size-6px);}
|
|
20
|
+
.cx-flow-review-checks{margin-top:var(--cx-size-20px);}
|
|
21
|
+
.cx-flow-answer[data-cx-state="fail"] .cx-flow-answer-value{color:var(--cx-tone-danger);}
|
|
22
|
+
.cx-flow-answer[data-cx-state="warning"] .cx-flow-answer-value{color:var(--cx-tone-warning);}
|
|
23
|
+
@media (forced-colors:active){.cx-flow-check-mark{forced-color-adjust:none;border:var(--cx-size-1px) solid CanvasText;}}
|
|
24
|
+
@media print{.cx-flow-check-head button,.cx-flow-check-again{display:none;}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"ec2451183901867b",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={flowRunAgain:"Run again",flowRunCheckAgain:"Run “{check}” again",flowChecks:"Checks",flowCheckAcknowledge:"Continue despite the warning",flowCheckRunning:"Checking…",flowCheckNotRun:"Not run",flowCheckStale:"The answers changed since the check",flowCheckPassed:"Passed at {time}",flowCheckWarning:"Warning at {time}",flowCheckFailed:"Failed at {time}",flowCheckInvalid:"The check gave no usable answer.",flowCheckNeeded:"Run “{check}” on these answers.",flowCheckNotPassed:"“{check}” did not pass.",flowCheckConfirmWarning:"Confirm the warning of “{check}”."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_CHECK_MARKS={pass:"✓",warning:"!",fail:"×",running:"…"};FLOW_HOST.checks={read(raw,step,model){if(step.type!=="form"||!Array.isArray(raw)||!raw.length||raw.length>8)flowFail("step "+step.id+" has 1 to 8 checks, as a form step only");for(const entry of raw){flowKeys(entry,["id","label","description","fields","required"],"A check");const id=flowName(entry.id,"A check id");if(model.checks.has(id))flowFail("the check id '"+id+"' is used twice");if(!Array.isArray(entry.fields)||!entry.fields.length||entry.fields.length>64||entry.fields.some(name=>typeof name!=="string"))flowFail("check "+id+" needs 1 to 64 field ids");if(entry.required!==undefined&&!["pass","run"].includes(entry.required))flowFail("check "+id+" requires pass or run");const check=Object.freeze({id,step,label:flowString(entry.label,200,"A check label",true),description:flowString(entry.description,1000,"A check description"),fields:Object.freeze([...new Set(entry.fields)]),required:entry.required??null});model.checks.set(id,check);step.checks.push(check)}},finish(model){for(const check of model.checks.values())for(const id of check.fields)if(!model.fields.has(id))flowFail("check "+check.id+" names the unknown field '"+id+"'")},built(view,step,section){if(step.checks.length){section.appendChild(view.checkBlock(step));queueMicrotask(()=>step.checks.forEach(check=>view.paintCheck(check)))}},review:(view,section)=>view.reviewChecks(section),proposal(view,proposal){const checks=view.checkSummary();if(Object.keys(checks).length)proposal.checks=checks}};Object.assign(FlowView.prototype,{checkBlock(step){const block=h("div","cx-flow-checks"),title=h("h4","cx-flow-section-title");title.id=this.id+"-c-"+step.id;setText(title,this.labels.flowChecks);block.setAttribute("role","group");block.setAttribute("aria-labelledby",title.id);block.appendChild(title);for(const check of step.checks){const item=h("div","cx-flow-check-item"),head=h("div","cx-flow-check-head"),run=h("button","cx-flow-btn"),cancel=h("button","cx-flow-btn cx-flow-quiet");const status=h("span","cx-flow-check-status"),mark=h("span","cx-flow-check-mark"),text=h("span"),message=h("p","cx-flow-check-message"),details=h("dl","cx-flow-check-details");const ack=h("label","cx-flow-switch cx-flow-check-ack"),box=h("input"),ackText=h("span");item.dataset.cxCheck=check.id;run.type=cancel.type="button";setText(run,check.label);setText(cancel,this.labels.flowCancel);cancel.hidden=true;status.setAttribute("role","status");mark.setAttribute("aria-hidden","true");status.append(mark,text);status.id=this.id+"-cs-"+check.id;run.setAttribute("aria-describedby",status.id);run.addEventListener("click",()=>this.runCheck(check.id).catch(error=>this.report(error,"check")));cancel.addEventListener("click",()=>this.checkState.get(check.id)?.controller?.abort());box.type="checkbox";setText(ackText,this.labels.flowCheckAcknowledge);ack.append(box,ackText);ack.hidden=true;box.addEventListener("change",()=>{const state=this.checkState.get(check.id);if(state)state.acknowledged=box.checked;this.paintCheck(check);this.renderRail()});head.append(run,status,cancel);item.appendChild(head);if(check.description){const description=h("p","cx-flow-help");setText(description,check.description);item.appendChild(description)}
|
|
2
|
+
item.append(message,details,ack);item._cx={run,cancel,mark,text,message,details,ack,box};block.appendChild(item)}
|
|
3
|
+
return block},paintCheck(check){const section=this.sections.get(check.step.id),item=section?.querySelector('[data-cx-check="'+check.id+'"]');if(!item)return;const parts=item._cx,state=this.checkState.get(check.id),labels=this.labels,result=state?.result;const phase=state?.running?"running":!result?"none":state.stale?"stale":result.status;item.dataset.cxState=phase;setText(parts.mark,FLOW_CHECK_MARKS[phase]||"");setText(parts.text,phase==="running"?labels.flowCheckRunning:phase==="none"?labels.flowCheckNotRun:phase==="stale"?labels.flowCheckStale:
|
|
4
|
+
fill(phase==="pass"?labels.flowCheckPassed:phase==="warning"?labels.flowCheckWarning:labels.flowCheckFailed,{time:this.checkTime(state.at)}));parts.run.disabled=!!state?.running;parts.cancel.hidden=!state?.running;parts.message.hidden=!result?.message||phase==="running";setText(parts.message,result?.message||"");parts.details.replaceChildren(...(phase==="running"?[]:result?.details||[]).map(row=>{const line=h("div"),term=h("dt"),value=h("dd");setText(term,row.label);setText(value,row.value);line.append(term,value);return line}));parts.details.hidden=!parts.details.children.length;parts.ack.hidden=!(check.required==="run"&&phase==="warning");parts.box.checked=!!state?.acknowledged},checkTime(at){try{return new Intl.DateTimeFormat(this.lang||undefined,{timeStyle:"short"}).format(new Date(at))}catch(error){return at}},checksChanged(field){const affected=new Map([[field.id,true]]);const changed=id=>{if(!affected.has(id))affected.set(id,[...this.model.fields.get(id).refs].some(changed));return affected.get(id)};let invalidated=false;for(const check of this.model.checks.values()){if(!check.fields.some(changed))continue;const state=this.checkState.get(check.id);if(state){state.stale=true;state.acknowledged=false;state.controller?.abort();this.paintCheck(check);invalidated=true}}
|
|
5
|
+
if(invalidated)this.paintReview()},async runCheck(id){const check=this.model.checks.get(id);if(!check)flowFail("there is no check '"+id+"'");const state=this.checkState.get(id)||{},controller=new AbortController(),ticket={};state.controller?.abort();Object.assign(state,{controller,ticket,running:true});this.checkState.set(id,state);this.paintCheck(check);this.paintReview();const secrets=new Map(check.fields.map(name=>this.model.fields.get(name)).filter(field=>field.type==="secret"&&this.visible(field)&&this.secrets.has(field.id)).map(field=>[field.id,this.secrets.get(field.id)]));const revoke=()=>secrets.clear();controller.signal.addEventListener("abort",revoke,{once:true});const run=typeof this.hooks.check==="function"?signal=>this.hooks.check(id,this.getValues(),{signal,secret:name=>{const value=secrets.get(name)??null;secrets.delete(name);return value}}):()=>{throw new Error(this.labels.flowNoHostHook)};const answer=await this.timed("check",run,controller);revoke();controller.signal.removeEventListener("abort",revoke);if(this.checkState.get(id)!==state||state.ticket!==ticket||this.destroyed)return null;state.running=false;if(answer.cancelled){this.paintCheck(check);this.paintReview();return null}
|
|
6
|
+
const result=answer.failed?{status:"fail",message:answer.failed,details:[],fieldErrors:{}}:this.checkResult(answer.value);for(const[field,message]of Object.entries(state.result?.fieldErrors||{}))if(!Object.hasOwn(result.fieldErrors,field)&&this.hostErrors.get(field)===message){this.hostErrors.delete(field);if(this.errors.get(field)===message)this.setError(this.model.fields.get(field),null)}
|
|
7
|
+
Object.assign(state,{result,at:new Date().toISOString(),stale:false,acknowledged:false});for(const[field,message]of Object.entries(result.fieldErrors)){this.hostErrors.set(field,message);this.setError(this.model.fields.get(field),message)}
|
|
8
|
+
this.paintCheck(check);this.renderRail();this.reviewStale=true;this.paintReview(true);const detail=flowCopy({check:id,result:{status:result.status,message:result.message,details:result.details,at:state.at}});this.emit("flowCheck",detail,"onCheck");return detail.result},checkResult(value){const text=item=>typeof item==="string"&&/\S/.test(item) ? item.slice(0, 1000) : null;
|
|
9
|
+
if(!flowIs(value)||!["pass","warning","fail"].includes(value.status))return{status:"fail",message:this.labels.flowCheckInvalid,details:[],fieldErrors:{}};const result={status:value.status,message:text(value.message),details:[],fieldErrors:{}};if(Array.isArray(value.details))for(const row of value.details.slice(0,20))if(flowIs(row)&&text(row.label)&&flowScalar(row.value))result.details.push({label:text(row.label),value:String(row.value).slice(0,1000)});if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))result.fieldErrors[id]=text(message);return result},async checkGate(step,run){const problems=[];for(const check of step.checks){if(!check.required||!check.fields.some(id=>this.visible(this.model.fields.get(id))))continue;let state=this.checkState.get(check.id);if(run&&(!state?.result||state.stale)&&!state?.running){await this.runCheck(check.id);state=this.checkState.get(check.id)}
|
|
10
|
+
const result=state?.result,labels=this.labels;if(!result||state.stale||state.running)problems.push(fill(labels.flowCheckNeeded,{check:check.label}));else if(result.status==="fail"||check.required==="pass"&&result.status==="warning")problems.push(fill(labels.flowCheckNotPassed,{check:check.label}));else if(result.status==="warning"&&!state.acknowledged)problems.push(fill(labels.flowCheckConfirmWarning,{check:check.label}))}
|
|
11
|
+
return problems},paintReview(ended){if(this.step.type!=="review"||this.phase!=="form")return;this.reviewCheckRows?.();if(ended)this.reviewRecheck?.()},reviewChecks(section){const labels=this.labels,checks=[...this.model.checks.values()].filter(check=>this.stepVisible(check.step));this.reviewCheckRows=null;if(!checks.length)return;const group=h("section","cx-flow-review-group cx-flow-review-checks"),heading=h("h4","cx-flow-review-title"),list=h("dl","cx-flow-answers");heading.id=this.id+"-rc";setText(heading,labels.flowChecks);group.setAttribute("aria-labelledby",heading.id);const paint=this.reviewCheckRows=()=>{const focused=list.contains(document.activeElement)?document.activeElement.dataset.cxCheck:null;list.replaceChildren(...checks.map(check=>{const state=this.checkState.get(check.id),result=state?.result,row=h("div","cx-flow-answer"),term=h("dt"),value=h("dd","cx-flow-answer-value"),action=h("dd","cx-flow-answer-action");const phase=state?.running?"running":!result?"none":state.stale?"stale":result.status;setText(term,check.label);setText(value,phase==="running"?labels.flowCheckRunning:phase==="none"?labels.flowCheckNotRun:phase==="stale"?labels.flowCheckStale:
|
|
12
|
+
fill(phase==="pass"?labels.flowCheckPassed:phase==="warning"?labels.flowCheckWarning:labels.flowCheckFailed,{time:this.checkTime(state.at)})+(result.message?" · "+result.message:""));row.dataset.cxState=phase;const again=h("button","cx-flow-link cx-flow-check-again");again.type="button";again.dataset.cxCheck=check.id;setText(again,labels.flowRunAgain);again.setAttribute("aria-label",fill(labels.flowRunCheckAgain,{check:check.label}));if(phase==="running")again.setAttribute("aria-disabled","true");again.addEventListener("click",()=>{if(again.getAttribute("aria-disabled")!=="true"&&!this.busy)this.runCheck(check.id).catch(error=>this.report(error,"check"))});action.appendChild(again);row.append(term,value,action);return row}));if(focused)list.querySelector('[data-cx-check="'+CSS.escape(focused)+ '"]')?.focus()};paint();group.append(heading,list);section.appendChild(group)},checkSummary(){const checks={};for(const check of this.model.checks.values()){const state=this.checkState.get(check.id);if(this.stepVisible(check.step)&&state?.result&&!state.stale&&!state.running)checks[check.id]={status:state.result.status,at:state.at}}
|
|
13
|
+
return checks}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-checks",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* The flow-command part: the equivalent command with its tabs and Copy. */
|
|
2
|
+
.cx-flow-command{border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-radius);}
|
|
3
|
+
.cx-flow-command > summary{padding:var(--cx-size-8px) var(--cx-size-12px);font-weight:600;cursor:pointer;}
|
|
4
|
+
.cx-flow-command[open] > summary{border-bottom:var(--cx-size-1px) solid var(--cx-line);}
|
|
5
|
+
.cx-flow-command-tools{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--cx-size-8px);padding:var(--cx-size-8px) var(--cx-size-12px) 0;}
|
|
6
|
+
.cx-flow-tabs{display:flex;flex-wrap:wrap;gap:var(--cx-size-4px);}
|
|
7
|
+
.cx-flow-tab{min-height:var(--cx-size-28px);padding:0 var(--cx-size-10px);border:var(--cx-size-1px) solid transparent;border-radius:var(--cx-radius-sm);background:transparent;color:var(--cx-text-2);font:inherit;font-size:var(--cx-size-13px);cursor:pointer;}
|
|
8
|
+
.cx-flow-tab[aria-selected="true"]{border-color:var(--cx-line-strong);background:var(--cx-surface);color:var(--cx-text);font-weight:600;}
|
|
9
|
+
.cx-flow-tab:focus-visible,.cx-flow-code:focus-visible,.cx-flow-command > summary:focus-visible{outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-1px);}
|
|
10
|
+
.cx-flow-code{margin:var(--cx-size-8px) var(--cx-size-12px) var(--cx-size-12px);padding:var(--cx-size-10px) var(--cx-size-12px);border-radius:var(--cx-radius-sm);background:var(--cx-head-bg);
|
|
11
|
+
font:var(--cx-size-12d5px)/1.5 var(--cx-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:var(--cx-size-320px);overflow:auto;}
|
|
12
|
+
.cx-flow-step > .cx-flow-command{margin-top:var(--cx-flow-gap);}
|
|
13
|
+
@media (forced-colors:active){.cx-flow-tab[aria-selected="true"]{border-color:Highlight;}}
|
|
14
|
+
@media print{.cx-flow-command-tools{display:none;}.cx-flow-command{break-inside:avoid;}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"39472118ca80ef02",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={flowCommandTab:"Command",flowPatchTab:"JSON patch",flowCommandTitle:"Equivalent command"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_PLAIN_CHARACTERS={powershell:"-_./:\\=+%",posix:"-_./:=@%+,",windows:"-_./:\\=@%+,"};const FLOW_COMMAND_STYLES=["powershell","posix","windows"];FLOW_HOST.command={read(raw){flowKeys(raw,["name","style","variants"],"The command");const name=flowString(raw.name,200,"The command name",true),style=raw.style??"powershell";if(!FLOW_COMMAND_STYLES.includes(style)||/[\u0000-\u001f]/.test(name)) flowFail("the command has an unknown style or a control character");
|
|
2
|
+
const variants=raw.variants??[];if(!Array.isArray(variants)||variants.length>8)flowFail("a command has at most 8 variants");const ids=new Set(["command","patch"]);return{name,style,variants:variants.map(variant=>{flowKeys(variant,["id","label"],"A command variant");const id=flowName(variant.id,"A variant id");if(ids.has(id))flowFail("the command variant '"+id+"' is used twice");ids.add(id);return Object.freeze({id,label:flowString(variant.label,60,"A variant label",true)})})}},line:view=>flowCommandLine(view),document(doc,model){if(doc.command!==undefined)model.command=this.read(doc.command)},review(view,section){if(view.model.command)section.appendChild(view.commandPanel())},proposal(view,proposal){if(view.model.command)proposal.command=flowCommandLine(view)}};function flowCommandParameter(name,style){const bare=name.replace(/^[-/]+/, "");
|
|
3
|
+
return style==="posix"?(name.startsWith("-")?name:(bare.length===1?"-":"--")+ bare):style==="windows"?"/"+bare:"-"+bare}
|
|
4
|
+
function flowCommandQuote(value,style){const allowed=FLOW_PLAIN_CHARACTERS[style];let plain=value.length>0;for(let index=0;plain&&index<value.length;index++)plain=/[\p{L}\p{Nd}]/u.test(value[index]) || allowed.includes(value[index]);
|
|
5
|
+
if(plain)return value;return style==="posix"?"'"+value.replace(/'/g, "'\\''") + "'" : style === "windows" ?
|
|
6
|
+
"\""+value.replace(/(\\*)"/g, (_, slashes) => slashes + slashes + "\\\"").replace(/(\\+)$/g, "$1$1") + "\"" : "'" + value.replace(/'/g, "''") + "'";
|
|
7
|
+
}
|
|
8
|
+
function flowCommandArgument(name,value,style){const parameter=flowCommandParameter(name,style);if(value===null||value===undefined)return "";if(typeof value==="boolean")return value?parameter:"";let text;if(typeof value==="string"){if(!value.length)return "";text=flowCommandQuote(value,style)}
|
|
9
|
+
else if(Array.isArray(value)){const parts=value.filter(item=>item!==null&&item!==undefined).map(item=>flowCommandQuote(String(item),style));if(!parts.length)return "";text=parts.join(",")}
|
|
10
|
+
else text=flowCommandQuote(String(value),style);return parameter +(style==="windows"?":":" ")+ text}
|
|
11
|
+
function flowCommandLine(view){const model=view.model,command=model.command;if(!command)return null;const style=command.style,args=[];for(const field of model.fields.values()){if(!field.parameter||!view.visible(field)||field.type==="records")continue;if(field.type==="secret"){if(view.secrets.has(field.id))args.push(flowCommandParameter(field.parameter,style)+(style==="windows"?":":" ")+ "<secret>");continue}
|
|
12
|
+
const value=view.values.get(field.id)??null,base=model.initial?(Object.hasOwn(model.initial,field.id)?model.initial[field.id]:null):field.start;if(flowEmpty(value)||flowSameValue(flowNormal(value),flowNormal(base)))continue;if(value===false){if(base===true)args.push(style==="powershell"?flowCommandParameter(field.parameter,style)+ ":$false":flowCommandArgument("no-"+field.parameter,true,style));continue}
|
|
13
|
+
args.push(flowCommandArgument(field.parameter,field.type==="pick"?value.map(item=>item.id):value,style))}
|
|
14
|
+
return[command.name,...args.filter(Boolean)].join(" ")}
|
|
15
|
+
Object.assign(FlowView.prototype,{commandPanel(){const labels=this.labels,proposal=this.submission(),tabs=[{id:"command",label:labels.flowCommandTab,text:()=>Promise.resolve(flowCommandLine(this))}];for(const variant of this.model.command.variants){const format=this.hooks.commands?.[variant.id];if(typeof format==="function")tabs.push({id:variant.id,label:variant.label,text:()=>this.hook("commands."+variant.id,()=>format(this.getValues())).then(answer=>answer.failed||(typeof answer.value==="string"?answer.value:""))})}
|
|
16
|
+
if(proposal.patch.length)tabs.push({id:"patch",label:labels.flowPatchTab,text:()=>Promise.resolve(JSON.stringify(proposal.patch,null,2))});const panel=h("details","cx-flow-command"),summary=h("summary"),list=h("div","cx-flow-tabs"),code=h("pre","cx-flow-code"),copy=h("button","cx-flow-btn");setText(summary,labels.flowCommandTitle);list.setAttribute("role","tablist");list.setAttribute("aria-label",labels.flowCommandTitle);code.id=this.id+"-command";code.tabIndex=0;code.setAttribute("role","tabpanel");let current=null,ticket=0;const show=async tab=>{current=tab;const mine=++ticket;for(const button of list.children){const on=button.dataset.cxTab===tab.id;button.setAttribute("aria-selected",String(on));button.tabIndex=on?0:-1;if(on)code.setAttribute("aria-labelledby",button.id)}
|
|
17
|
+
const text=await tab.text();if(mine===ticket)setText(code,text)};for(const tab of tabs){const button=h("button","cx-flow-tab");button.type="button";button.id=this.id+"-tab-"+tab.id;button.dataset.cxTab=tab.id;button.setAttribute("role","tab");button.setAttribute("aria-controls",code.id);setText(button,tab.label);button.addEventListener("click",()=>show(tab));list.appendChild(button)}
|
|
18
|
+
list.addEventListener("keydown",event=>{const buttons=[...list.children],at=buttons.indexOf(document.activeElement),step=event.key==="ArrowRight"?1:event.key==="ArrowLeft"?-1:0;if(!step||at<0)return;event.preventDefault();const rtl=getComputedStyle(list).direction==="rtl",next=buttons[(at +(rtl?-step:step)+ buttons.length)%buttons.length];next.focus();next.click()});copy.type="button";setText(copy,labels.flowCopy);copy.addEventListener("click",()=>{const text=code.textContent,done=()=>{setText(copy,labels.flowCopied);this.announce(labels.flowCopied);setTimeout(()=>setText(copy,labels.flowCopy),1500)};if(!global.navigator?.clipboard){global.getSelection?.().selectAllChildren(code);return}
|
|
19
|
+
global.navigator.clipboard.writeText(text).then(done,error=>{global.getSelection?.().selectAllChildren(code);this.report(error,"copy")})});const tools=h("div","cx-flow-command-tools");tools.append(list,copy);panel.append(summary,tools,code);show(tabs[0]);panel._cxShow=()=>current&&show(current);return panel}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-command",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* The flow-consent part: the typed confirmation and the acknowledgement. */
|
|
2
|
+
.cx-flow{--cx-size-560px:560px;}
|
|
3
|
+
.cx-flow-confirm .cx-flow-input{margin-top:var(--cx-size-6px);}
|
|
4
|
+
.cx-flow-acknowledge{align-items:flex-start;max-width:var(--cx-size-560px);}
|
|
5
|
+
.cx-flow-acknowledge input{margin-top:var(--cx-size-2px);}
|
|
6
|
+
.cx-flow-step > :is(.cx-flow-confirm,.cx-flow-acknowledge){margin-top:var(--cx-flow-gap);}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"22121778bc0d07c5",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={flowConfirmMismatch:"Type {text} exactly to continue.",flowAcknowledgeNeeded:"Confirm this before you submit."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const{begin:flowBegin,submit:flowSubmit}=FlowView.prototype;FLOW_HOST.consent={confirm:(view,section,step)=>view.reviewConsent(section,step),read(raw,step){if(raw.confirm!==undefined){flowKeys(raw.confirm,["label","text","field"],"The confirmation");if((raw.confirm.text===undefined)===(raw.confirm.field===undefined))flowFail("a confirmation names the text to type or the field that holds it");step.confirm={label:flowString(raw.confirm.label,200,"The confirmation label",true),text:flowString(raw.confirm.text,200,"The confirmation text"),field:raw.confirm.field}}
|
|
2
|
+
if(raw.acknowledge!==undefined){flowKeys(raw.acknowledge,["label","when"],"The acknowledgement");step.acknowledge={label:flowString(raw.acknowledge.label,400,"The acknowledgement label",true),raw:raw.acknowledge.when}}},finish(model){const review=model.steps.at(-1);if(review.type!=="review")return;if(review.acknowledge?.raw!==undefined)review.acknowledge.when=flowCondition(review.acknowledge.raw,model,{groups:0,filters:0});if(review.confirm?.field!==undefined){const field=model.fields.get(review.confirm.field);if(!field)flowFail("the confirmation names the unknown field '"+review.confirm.field+"'");if(!["text","select","choice","lookup"].includes(field.type))flowFail("the confirmation reads a text or choice field")}}};Object.assign(FlowView.prototype,{begin(values){flowBegin.call(this,values);this.confirmText="";this.acknowledged=false},async submit(){return!this.busy&&this.phase==="form"&&this.consentProblem()?null:flowSubmit.call(this)},reviewConsent(section,step){const labels=this.labels;if(step.confirm){const box=h("div","cx-flow-confirm"),label=h("label","cx-flow-label"),input=h("input","cx-flow-input"),message=h("p","cx-flow-error"),target=this.confirmTarget(step);input.id=this.id+"-confirm";label.htmlFor=input.id;setText(label,fill(step.confirm.label,{text:target}));input.type="text";input.autocomplete="off";input.spellcheck=false;input.value=this.confirmText;message.id=input.id+"-error";message.hidden=true;input.addEventListener("input",()=>{this.confirmText=input.value;if(!message.hidden&&this.confirmed(step)){message.hidden=true;input.removeAttribute("aria-invalid");input.removeAttribute("aria-describedby")}});box.append(label,input,message);section.appendChild(box)}
|
|
3
|
+
if(step.acknowledge&&flowTest(step.acknowledge.when,id=>this.read(id))){const label=h("label","cx-flow-switch cx-flow-acknowledge"),box=h("input"),text=h("span"),message=h("p","cx-flow-error");box.type="checkbox";box.checked=this.acknowledged;box.id=this.id+"-ack";setText(text,step.acknowledge.label);message.id=box.id+"-error";message.hidden=true;box.addEventListener("change",()=>{this.acknowledged=box.checked;if(box.checked){message.hidden=true;box.removeAttribute("aria-invalid")}});label.append(box,text);section.append(label,message)}},confirmTarget(step){return step.confirm.text??String(this.read(step.confirm.field)??"")},confirmed(step){const target=this.confirmTarget(step);return!!target&&this.confirmText.trim()===target},consentProblem(){const step=this.reviewStep();if(!step||!this.stepVisible(step))return false;const labels=this.labels,missing=step.confirm&&!this.confirmed(step)||step.acknowledge&&flowTest(step.acknowledge.when,id=>this.read(id))&&!this.acknowledged;if(missing&&this.step!==step){this.go(step,"forward").catch(error=>this.report(error,"goTo"));return true}
|
|
4
|
+
if(step.confirm&&!this.confirmed(step)){const input=this.stage.querySelector("#"+CSS.escape(this.id+"-confirm")),message=this.stage.querySelector("#"+CSS.escape(this.id+"-confirm-error"));if(message){setText(message,fill(labels.flowConfirmMismatch,{text:this.confirmTarget(step)}));message.hidden=false;input.setAttribute("aria-invalid","true");input.setAttribute("aria-describedby",message.id)}
|
|
5
|
+
input?.focus();return true}
|
|
6
|
+
if(step.acknowledge&&flowTest(step.acknowledge.when,id=>this.read(id))&&!this.acknowledged){const box=this.stage.querySelector("#"+CSS.escape(this.id+"-ack")),message=this.stage.querySelector("#"+CSS.escape(this.id+"-ack-error"));if(message){setText(message,labels.flowAcknowledgeNeeded);message.hidden=false;box.setAttribute("aria-invalid","true");box.setAttribute("aria-describedby",message.id)}
|
|
7
|
+
box?.focus();return true}
|
|
8
|
+
return false}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-consent",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* The flow-dialog part: dialogs and drawers. */
|
|
2
|
+
.cx-flow{--cx-size-560px:560px;}
|
|
3
|
+
.cx-flow-launcher{display:contents;}
|
|
4
|
+
.cx-flow-launcher > .cx-flow-launch{height:auto;min-height:var(--cx-size-36px);padding:0 var(--cx-size-16px);}
|
|
5
|
+
.cx-flow-dialog{box-sizing:border-box;width:min(var(--cx-size-640px),calc(var(--cx-size-100vw) - var(--cx-size-32px)));max-height:calc(var(--cx-size-100vh) - var(--cx-size-32px));padding:0;border:0;border-radius:var(--cx-size-8px);
|
|
6
|
+
background:transparent;color:var(--cx-text);overflow:auto;box-shadow:0 var(--cx-size-24px) var(--cx-size-64px) color-mix(in srgb,var(--cx-shadow-ink,#000) 32%,transparent);}
|
|
7
|
+
.cx-flow-dialog::backdrop{background:color-mix(in srgb,var(--cx-shadow-ink,#000) 38%,transparent);}
|
|
8
|
+
.cx-flow-dialog.cx-flow-drawer{margin:0 0 0 auto;inset-inline-end:0;inset-inline-start:auto;width:min(var(--cx-size-560px,560px),var(--cx-size-100vw));height:var(--cx-size-100vh);max-height:none;border-radius:0;}
|
|
9
|
+
.cx-flow-dialog.cx-flow-drawer > .cx-flow{min-height:100%;border-radius:0;border-block:0;border-inline-end:0;}
|
|
10
|
+
.cx-flow-head-close{position:relative;padding-inline-end:calc(var(--cx-flow-pad) + var(--cx-size-36px));}
|
|
11
|
+
.cx-flow-close{position:absolute;inset-block-start:var(--cx-size-10px);inset-inline-end:var(--cx-size-10px);border-color:transparent;background:transparent;}
|
|
12
|
+
.cx-flow-discard{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-8px) var(--cx-size-12px);padding:var(--cx-size-10px) var(--cx-flow-pad);border-bottom:var(--cx-size-1px) solid var(--cx-line);background:var(--cx-tone-warning-bg);}
|
|
13
|
+
.cx-flow-discard p{flex:1;font-weight:600;}
|
|
14
|
+
@media (max-width:639.98px){.cx-flow-dialog,.cx-flow-dialog.cx-flow-drawer{width:var(--cx-size-100vw);max-width:none;height:var(--cx-size-100vh);max-height:none;margin:0;border-radius:0;}.cx-flow-dialog > .cx-flow{min-height:100%;border-radius:0;border:0;}}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"ebb432cd6e34aba6",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={flowClose:"Close",flowDiscardQuestion:"Discard your answers?",flowDiscard:"Discard",flowKeepEditing:"Keep editing"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const flowRelease=FlowView.prototype.destroy;FLOW_HOST.dialog={document(doc,model){const raw=doc.layout;if(!raw)return;model.container=raw.container??"inline";if(!["inline","dialog","drawer"].includes(model.container))flowFail("the flow layout has an unknown container");if(raw.launch!==undefined&&model.container==="inline")flowFail("only a dialog or drawer flow has a launch button");model.launch=flowString(raw.launch,80,"The launch label")}};Object.assign(FlowView.prototype,{destroy(){flowRelease.call(this);this.shell?.remove()},buildContainer(){const labels=this.labels,shell=this.shell=h("div","cx-flow-launcher"),launch=this.launcher=h("button","cx-flow-btn cx-primary cx-flow-launch");launch.type="button";setText(launch,this.model.launch||this.model.title);launch.setAttribute("aria-haspopup","dialog");const dialog=this.dialog=h("dialog","cx-flow-dialog" +(this.model.container==="drawer"?" cx-flow-drawer":""));dialog.setAttribute("aria-labelledby",this.id+"-title");const close=h("button","cx-flow-icon-btn cx-flow-close");close.type="button";setText(close,"×");close.setAttribute("aria-label",labels.flowClose);this.head.classList.add("cx-flow-head-close");this.head.appendChild(close);const confirm=this.discardBar=h("div","cx-flow-discard"),question=h("p"),discard=h("button","cx-flow-btn"),keep=h("button","cx-flow-btn cx-primary");confirm.setAttribute("role","alertdialog");confirm.setAttribute("aria-labelledby",this.id+"-discard");confirm.hidden=true;question.id=this.id+"-discard";setText(question,labels.flowDiscardQuestion);discard.type=keep.type="button";setText(discard,labels.flowDiscard);setText(keep,labels.flowKeepEditing);confirm.append(question,keep,discard);this.root.insertBefore(confirm,this.layout);dialog.appendChild(this.root);shell.append(launch,dialog);this.host.appendChild(shell);const signal=this.lifetime.signal;launch.addEventListener("click",()=>this.open(),{signal});close.addEventListener("click",()=>this.close(),{signal});dialog.addEventListener("cancel",event=>{event.preventDefault();this.close()},{signal});keep.addEventListener("click",()=>{confirm.hidden=true;this.stage.querySelector(".cx-flow-step-title")?.focus()},{signal});discard.addEventListener("click",()=>this.close(true),{signal})},cancelButton(){if(!this.dialog)return null;const cancel=h("button","cx-flow-btn cx-flow-quiet");cancel.type="button";setText(cancel,this.labels.flowCancel);cancel.addEventListener("click",()=>this.close());return cancel},open(){if(!this.dialog||this.dialog.open)return!!this.dialog;this.discardBar.hidden=true;this.dialog.showModal();this.measure();this.render();(this.stage.querySelector(".cx-flow-step-title")||this.root).focus({preventScroll:true});return true},close(force){if(!this.dialog?.open)return false;if(!force&&this.phase==="form"&&this.isDirty()){if(!this.emit("flowLeaving",{reason:"close"},"onLeaving",true))return false;this.discardBar.hidden=false;this.discardBar.querySelector(".cx-primary").focus();return false}
|
|
2
|
+
this.discardBar.hidden=true;this.dialog.close();if(force||this.phase!=="form"){this.panel=null;this.reset()}
|
|
3
|
+
this.launcher.focus();return true}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-dialog",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* The flow-drafts part: drafts,presets and the changed marks. */
|
|
2
|
+
.cx-flow-mark-changed{margin-inline-start:var(--cx-size-8px);padding:0 var(--cx-size-6px);border-radius:var(--cx-size-999px);border:var(--cx-size-1px) solid var(--cx-line);
|
|
3
|
+
font-size:var(--cx-size-11d5px);font-weight:500;color:var(--cx-text-2);vertical-align:1px;}
|
|
4
|
+
.cx-flow-presets{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-6px) var(--cx-size-12px);margin-top:var(--cx-size-10px);font-size:var(--cx-size-13d5px);}
|
|
5
|
+
.cx-flow-presets-label{color:var(--cx-text-2);font-weight:600;}
|
|
6
|
+
.cx-flow .cx-flow-preset{width:auto;min-width:var(--cx-size-180px);max-width:var(--cx-size-320px);}
|
|
7
|
+
.cx-flow-preset-name{display:flex;flex-wrap:wrap;gap:var(--cx-size-6px);flex-basis:100%;}
|
|
8
|
+
.cx-flow-preset-name .cx-flow-input{max-width:var(--cx-size-280px);}
|
|
9
|
+
.cx-flow-draft-note{margin-top:var(--cx-size-6px);font-size:var(--cx-size-12px);color:var(--cx-muted);}
|
|
10
|
+
.cx-flow-resume{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-6px) var(--cx-size-12px);margin:var(--cx-size-12px) var(--cx-flow-pad) 0;padding:var(--cx-size-10px) var(--cx-size-14px);
|
|
11
|
+
border-radius:var(--cx-radius);border-inline-start:var(--cx-size-3px) solid var(--cx-accent);background:var(--cx-accent-soft);}
|
|
12
|
+
.cx-flow-resume.cx-flow-resume-stale{border-inline-start-color:var(--cx-tone-warning);background:var(--cx-tone-warning-bg);}
|
|
13
|
+
.cx-flow-resume > p{flex-basis:100%;}
|
|
14
|
+
.cx-flow-resume-tools{display:flex;flex-wrap:wrap;gap:var(--cx-size-8px);}
|
|
15
|
+
.cx-flow-preset-line{color:var(--cx-text-2);font-size:var(--cx-size-13d5px);}
|
|
16
|
+
.cx-flow-step > .cx-flow-preset-line{margin-top:var(--cx-flow-gap);}
|
|
17
|
+
@media print{.cx-flow-presets,.cx-flow-resume{display:none;}}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"4f112f64736ca8e3",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={flowChangedMark:"Changed",flowSaveDraft:"Save draft",flowDraftSaved:"Draft saved",flowDraftFailed:"The draft was not saved.",flowDraftFound:"Saved draft",flowDraftFrom:"You have a draft saved {time}.",flowDraftStale:"The settings changed after this draft was saved.",flowDraftResume:"Resume where you left off",flowDraftRestart:"Start again",flowStartFrom:"Start from",flowPresetSave:"Save as preset",flowPresetNone:"Defaults",flowPresetsSaved:"Saved presets",flowPresetApplied:"Started from {preset}",flowPresetName:"Preset name",flowPresetFailed:"The preset was not saved.",flowPresetSaved:"Saved the preset {preset}",flowPresetChanges:"[one]Based on {preset}, {count} change[other]Based on {preset}, {count} changes",flowPresetCurrent:"Current settings"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_DRAFT_DELAY=800;function flowPreset(raw,model){flowKeys(raw,["id","label","description","values"],"A preset");const preset={id:flowName(raw.id,"A preset id"),label:flowString(raw.label,200,"A preset label",true),description:flowString(raw.description,1000,"A preset description"),values:{}};if(!flowIs(raw.values))flowFail("preset "+preset.id+" needs an object of values");for(const[id,value]of Object.entries(raw.values)){const field=model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))flowFail("preset "+preset.id+" has no value for '"+id+"' that fits a field");preset.values[id]=value}
|
|
2
|
+
return Object.freeze(preset)}
|
|
3
|
+
function flowMark(view,fv,on){if(on&&!fv.mark){fv.mark=h("span","cx-flow-mark-changed");setText(fv.mark,view.labels.flowChangedMark)}
|
|
4
|
+
if(on&&fv.mark.parentNode!==fv.label)fv.label.appendChild(fv.mark);else if(!on)fv.mark?.remove()}
|
|
5
|
+
const flowStateOf=FlowView.prototype.getState,flowDraftReset=FlowView.prototype.reset;FLOW_HOST.drafts={presets(raw,model){if(!Array.isArray(raw)||raw.length>32)flowFail("a flow has at most 32 presets");const presets=raw.map(item=>flowPreset(item,model));if(new Set(presets.map(preset=>preset.id)).size!==presets.length)flowFail("two presets share an id");return presets},document(doc,model){if(doc.presets!==undefined)model.presets=this.presets(doc.presets,model);model.draft=doc.draft??"none";if(!["none","session","local","host"].includes(model.draft))flowFail("the flow's draft store is unknown")},review:(view,section)=>view.reviewPreset(section),proposal(view,proposal){if(view.preset)proposal.preset=view.preset.id},settled:(view,answer)=>view.draftSettled(answer)};Object.assign(FlowView.prototype,{getState(){const state=flowStateOf.call(this);if(this.preset)state.preset=this.preset.id;return state},reset(){const result=flowDraftReset.call(this);this.resumeBanner?.remove();this.resumeBanner=null;this.draftPending=false;this.clearDraft();return result},draftButton(){if(!this.draftStore())return null;const save=h("button","cx-flow-btn cx-flow-quiet");save.type="button";setText(save,this.labels.flowSaveDraft);save.addEventListener("click",()=>this.saveDraft(true).catch(error=>this.report(error,"draft")));return save},draftStore(){if(this.storeCache!==undefined)return this.storeCache;const model=this.model,key="canopyx:flow-draft:"+model.id;let store=null;if(model.draft==="session"||model.draft==="local"){const area=()=>{try{return model.draft==="local"?global.localStorage:global.sessionStorage}catch(error){return null}};store={load:()=>{const raw=area()?.getItem(key);return raw?JSON.parse(raw):null},save:(id,draft)=>area()?.setItem(key,JSON.stringify(draft)),clear:()=>area()?.removeItem(key)}}else if(model.draft==="host"){const host=this.hooks.draftStore;if(host&&["load","save","clear"].every(name=>typeof host[name]==="function"))store={load:()=>host.load(model.id),save:(id,draft)=>host.save(model.id,draft),clear:()=>host.clear(model.id)}}
|
|
6
|
+
return this.storeCache=store},draftDocument(){const values={};for(const[id,value]of this.values){const field=this.model.fields.get(id);if(field&&!field.sensitive&&field.type!=="secret")values[id]=flowCopy(value)}
|
|
7
|
+
const draft={kind:"flowDraft",version:1,flow:this.model.id,step:this.step.id,completed:[...this.completed],values,saved:new Date().toISOString()};if(this.model.baseline!==null)draft.baseline=this.model.baseline;if(this.preset)draft.preset=this.preset.id;return draft},async saveDraft(explicit){const store=this.draftStore();clearTimeout(this.draftTimer);if(!store||this.draftPending||this.phase!=="form"||this.destroyed)return false;const draft=this.draftDocument(),generation=this.generation,version=this.draftVersion;const answer=await this.draftWrite("draftStore.save",async()=>{if(this.destroyed||generation!==this.generation||version!==this.draftVersion||this.phase!=="form")return false;await store.save(this.model.id,draft);return true});if(this.destroyed||generation!==this.generation||version!==this.draftVersion)return false;if(answer.failed||!answer.value){if(explicit)this.announce(this.labels.flowDraftFailed);return false}
|
|
8
|
+
if(explicit){this.announce(this.labels.flowDraftSaved);this.noteDraft(this.labels.flowDraftSaved)}
|
|
9
|
+
return true},draftWrite(name,run){return this.draftWork=(this.draftWork||Promise.resolve()).then(()=>this.hook(name,run))},clearDraft(){const store=this.draftStore();clearTimeout(this.draftTimer);this.draftVersion=(this.draftVersion||0)+ 1;return store?this.draftWrite("draftStore.clear",()=>store.clear(this.model.id)):Promise.resolve()},noteDraft(text){let note=this.head.querySelector(".cx-flow-draft-note");if(!note){note=h("p","cx-flow-draft-note");this.head.appendChild(note)}
|
|
10
|
+
setText(note,text)},draftChanged(now){if(!this.draftStore()||this.draftPending)return;clearTimeout(this.draftTimer);this.draftTimer=setTimeout(()=>this.saveDraft(false),now?0:FLOW_DRAFT_DELAY)},draftSettled(answer){if(answer.status==="saved"||answer.status==="accepted")this.clearDraft()},async loadDraft(){const store=this.draftStore();if(!store)return;this.draftPending=true;const generation=this.generation,answer=await this.hook("draftStore.load",()=>store.load(this.model.id)),draft=answer.value;if(this.destroyed||generation!==this.generation)return;if(answer.failed||!flowIs(draft)||draft.kind!=="flowDraft"||draft.version!==1||draft.flow!==this.model.id||!flowIs(draft.values)){this.draftPending=false;return}
|
|
11
|
+
const labels=this.labels,banner=this.resumeBanner=h("div","cx-flow-resume"),text=h("p","cx-flow-banner-title"),resume=h("button","cx-flow-btn cx-primary"),restart=h("button","cx-flow-btn");banner.setAttribute("role","region");banner.setAttribute("aria-label",labels.flowDraftFound);setText(text,fill(labels.flowDraftFrom,{time:typeof draft.saved==="string"?flowDate(this,draft.saved,{dateStyle:"medium",timeStyle:"short"}):""}));banner.appendChild(text);if((draft.baseline??null)!==this.model.baseline){const stale=h("p","cx-flow-banner-warn");setText(stale,labels.flowDraftStale);banner.appendChild(stale);banner.classList.add("cx-flow-resume-stale")}
|
|
12
|
+
resume.type=restart.type="button";setText(resume,labels.flowDraftResume);setText(restart,labels.flowDraftRestart);const tools=h("div","cx-flow-resume-tools");tools.append(resume,restart);banner.appendChild(tools);resume.addEventListener("click",()=>this.resumeDraft(draft).catch(error=>this.report(error,"draft")));restart.addEventListener("click",()=>{this.closeResume();this.clearDraft()});this.head.after(banner)},closeResume(){this.resumeBanner?.remove();this.resumeBanner=null;this.draftPending=false;(this.stage.querySelector(".cx-flow-step-title")||this.root).focus?.({preventScroll:true})},async resumeDraft(draft){for(const[id,value]of Object.entries(draft.values)){const field=this.model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))continue;this.setValues({[id]:value})}
|
|
13
|
+
this.preset=[...this.model.presets,...this.userPresets||[]].find(preset=>preset.id===draft.preset)||null;this.closeResume();await this.setState({step:draft.step,completed:Array.isArray(draft.completed)?draft.completed.filter(id=>typeof id==="string"):[]});this.markPreset()},reviewPreset(section){if(!this.preset)return;const line=h("p","cx-flow-preset-line"),count=this.presetChanges();setText(line,fill(plural(this.labels.flowPresetChanges,count,this.lang),{preset:this.preset.label,count}));section.appendChild(line)},buildPresets(){const store=this.hooks.presetStore;if(!this.model.presets.length&&!(store&&typeof store.list==="function"))return;const labels=this.labels,bar=this.presetBar=h("div","cx-flow-presets"),label=h("label","cx-flow-presets-label"),select=this.presetSelect=h("select","cx-flow-input cx-flow-preset");select.id=this.id+"-preset";label.htmlFor=select.id;setText(label,labels.flowStartFrom);select.addEventListener("change",()=>this.applyPreset(select.value||null),{signal:this.lifetime.signal});bar.append(label,select);if(store&&typeof store.save==="function"){const save=h("button","cx-flow-link");save.type="button";setText(save,labels.flowPresetSave);save.addEventListener("click",()=>this.askPresetName(),{signal:this.lifetime.signal});bar.appendChild(save)}
|
|
14
|
+
this.head.appendChild(bar);this.fillPresets()},fillPresets(){const select=this.presetSelect;if(!select)return;const option=(value,text)=>{const item=h("option");item.value=value;setText(item,text);return item};const groups=[option("",this.model.initial?this.labels.flowPresetCurrent:this.labels.flowPresetNone),...this.model.presets.map(preset=>option(preset.id,preset.label))];if(this.userPresets?.length){const group=h("optgroup");group.label=this.labels.flowPresetsSaved;group.append(...this.userPresets.map(preset=>option(preset.id,preset.label)));groups.push(group)}
|
|
15
|
+
select.replaceChildren(...groups);select.value=this.preset?.id??""},async loadPresets(){const store=this.hooks.presetStore;if(!store||typeof store.list!=="function")return;const answer=await this.hook("presetStore.list",()=>store.list(this.model.id));if(this.destroyed||answer.failed||!Array.isArray(answer.value))return;this.userPresets=answer.value.slice(0,100).map(raw=>{try{return flowPreset(raw,this.model)}catch(error){this.report(error,"presetStore.list");return null}})
|
|
16
|
+
.filter(preset=>preset&&!this.model.presets.some(own=>own.id===preset.id));this.fillPresets()},presetBase(field){return this.preset&&Object.hasOwn(this.preset.values,field.id)?this.preset.values[field.id]:this.startValues.get(field.id)??null},setPreset(id){this.applyPreset(id??null)},applyPreset(id){const preset=id===null?null:[...this.model.presets,...this.userPresets||[]].find(item=>item.id===id);if(id!==null&&!preset)flowFail("there is no preset '"+id+"'");this.preset=preset;for(const field of[...this.model.fields.values()]){if(field.type==="secret"||!this.values.has(field.id))continue;const value=this.presetBase(field);if(!flowSameValue(this.values.get(field.id)??null,value))this.setAnswer(field,value,"preset")}
|
|
17
|
+
if(this.presetSelect)this.presetSelect.value=preset?.id??"";this.markPreset();this.refreshReview();this.draftChanged();this.announce(preset?fill(this.labels.flowPresetApplied,{preset:preset.label}):this.model.initial?this.labels.flowPresetCurrent:this.labels.flowPresetNone)},markPreset(){for(const fv of this.fields.values())flowMark(this,fv,!!this.preset&&fv.field.type!=="secret"&&!flowSameValue(flowNormal(this.values.get(fv.field.id)),flowNormal(this.presetBase(fv.field))))},presetChanges(){return[...this.model.fields.values()].filter(field=>field.type!=="secret"&&this.visible(field)&&!flowSameValue(flowNormal(this.values.get(field.id)),flowNormal(this.presetBase(field)))).length},askPresetName(){if(this.presetBar.querySelector("input")){this.presetBar.querySelector("input").focus();return}
|
|
18
|
+
const labels=this.labels,name=h("input","cx-flow-input"),save=h("button","cx-flow-btn"),cancel=h("button","cx-flow-btn cx-flow-quiet"),box=h("div","cx-flow-preset-name");name.type="text";name.maxLength=200;name.setAttribute("aria-label",labels.flowPresetName);name.placeholder=labels.flowPresetName;save.type=cancel.type="button";setText(save,labels.flowPresetSave);setText(cancel,labels.flowCancel);cancel.addEventListener("click",()=>box.remove());const store=async()=>{const label=name.value.trim();if(!label){name.focus();return}
|
|
19
|
+
const values={};for(const[id,value]of Object.entries(this.getValues()))if(!this.model.fields.get(id).sensitive)values[id]=value;const preset={id:"saved-"+Date.now().toString(36),label,values};const answer=await this.hook("presetStore.save",()=>this.hooks.presetStore.save(this.model.id,preset));if(answer.failed){this.announce(labels.flowPresetFailed);return}
|
|
20
|
+
box.remove();await this.loadPresets();if(this.userPresets?.some(item=>item.id===preset.id)){this.preset=this.userPresets.find(item=>item.id===preset.id);this.fillPresets();this.markPreset()}
|
|
21
|
+
this.announce(fill(labels.flowPresetSaved,{preset:label}))};save.addEventListener("click",()=>store().catch(error=>this.report(error,"presetStore.save")));name.addEventListener("keydown",event=>{if(event.key==="Enter"){event.preventDefault();store().catch(error=>this.report(error,"presetStore.save"))}});box.append(name,save,cancel);this.presetBar.appendChild(box);name.focus()}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-drafts",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"f5cddfc9545e0920",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_ROUTES=["options","fields","check","validate","preview","status","draft","submit"];FLOW_HOST.endpoint={read(raw){flowKeys(raw,["url","routes"],"The endpoint");const url=raw.url,routes=raw.routes;if(typeof url!=="string"||url.length>512||!/^\/(?!\/)[\w\-./~%]*$/.test(url)) flowFail("the endpoint is a path on the page's own origin");
|
|
2
|
+
if(!Array.isArray(routes)||!routes.length||routes.some(route=>!FLOW_ROUTES.includes(route))||new Set(routes).size!==routes.length)flowFail("the endpoint lists some of its routes: "+FLOW_ROUTES.join(", "));return Object.freeze({url:url.replace(/\/+$/, ""), routes: Object.freeze(routes.slice()) });
|
|
3
|
+
},hooks:(model,hooks,headers)=>flowEndpointHooks(model,hooks,headers),document(doc,model){if(doc.endpoint!==undefined)model.endpoint=this.read(doc.endpoint)}};function flowEndpointHooks(model,hooks,headers){const endpoint=model.endpoint,has=route=>endpoint.routes.includes(route),merged=Object.assign({},hooks);const request=async(method,route,body,signal)=>{const response=await fetch(endpoint.url+"/"+route,{method,credentials:"same-origin",signal,cache:"no-store",headers:Object.assign(body===undefined?{}:{"Content-Type":"application/json"},{Accept:"application/json"},headers),body:body===undefined?undefined:JSON.stringify(Object.assign({flow:model.id},body))});const text=await response.text();let data=null;try{data=text?JSON.parse(text):null}catch(error){data=null}
|
|
4
|
+
if(!response.ok)throw new Error(data&&typeof data.message==="string"?data.message:"The server answered "+response.status+".");return data};const post=(route,body,signal)=>request("POST",route,body,signal);const secretsOf=(ids,read)=>{const secrets={};for(const id of ids){const value=read(id);if(typeof value==="string"&&value)secrets[id]=value}return secrets};if(has("options")&&!hooks.options)merged.options=(field,values,{signal})=>post("options",{field,values},signal).then(data=>data?.options);if(has("fields")&&!hooks.fields)merged.fields=(section,values,{signal})=>post("fields",{section,values},signal).then(data=>data?.fields);if(has("check")&&!hooks.check)merged.check=(check,values,{signal,secret})=>post("check",{check,values,secrets:secretsOf(model.checks.get(check)?.fields||[],secret)},signal);if(has("validate")){if(!hooks.validateStep)merged.validateStep=(step,values)=>post("validate",{step,values});if(!hooks.validateFlow)merged.validateFlow=values=>post("validate",{values})}
|
|
5
|
+
if(has("preview")&&!hooks.preview)merged.preview=(values,{signal})=>post("preview",{values},signal);if(has("status")&&!hooks.status)merged.status=(values,{signal})=>post("status",{values},signal);if(has("draft")&&!hooks.draftStore)merged.draftStore={load:()=>request("GET","draft"),save:(id,draft)=>request("PUT","draft",{draft}),clear:()=>request("DELETE","draft")};if(has("submit")&&!hooks.submit)merged.submit=detail=>{const submission=JSON.parse(JSON.stringify(detail)),ids=Object.keys(submission.secrets||{}).filter(id=>submission.secrets[id].state==="set");return post("submit",{submission,secrets:secretsOf(ids,detail.secret)})};return merged}
|
|
6
|
+
installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-endpoint",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -176,3 +176,4 @@ overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
|
|
|
176
176
|
}
|
|
177
177
|
.cx-host.cx-flow-host{min-height:0;display:block;background:transparent;}
|
|
178
178
|
.cx-host:where(.cx-flow-host:not([data-cx-height])){height:auto;}
|
|
179
|
+
body.cx-page > .cx-host.cx-flow-host:not([data-cx-height]),body.cx-page > .cx-main > .cx-host.cx-flow-host:not([data-cx-height]){height:auto;padding:0;}
|
|
@@ -52,7 +52,7 @@ function gridDateInputMoment(text,local){const match=/^(\d{4})-(\d{2})-(\d{2})T(
|
|
|
52
52
|
if(!match)return null;const moment=gridMomentFromParts(Number(match[1]),Number(match[2]),Number(match[3]),Number(match[4]),Number(match[5]),Number(match[6]||0),local);if(moment)moment.setTime(moment.getTime()+ Number((match[7]||"").padEnd(3,"0")));return moment}
|
|
53
53
|
function gridFreeze(value){if(value&&typeof value==="object"&&!Object.isFrozen(value)){Object.values(value).forEach(gridFreeze);Object.freeze(value)}
|
|
54
54
|
return value}
|
|
55
|
-
const PART_URL=typeof document!=="undefined"&&document.currentScript?.src;const PARTS={"forms":["a8c60f205d9babca","canopyx-forms.16987de1cdf9bda9.js","canopyx-forms.29058ce3c88af96e.css"],"flow":["
|
|
55
|
+
const PART_URL=typeof document!=="undefined"&&document.currentScript?.src;const PARTS={"forms":["a8c60f205d9babca","canopyx-forms.16987de1cdf9bda9.js","canopyx-forms.29058ce3c88af96e.css"],"flow":["842e1f1956f6213f","canopyx-flow.efc6250968e8b10a.js","canopyx-flow.3152e616c5557452.css"],"flow-host":["de4f9b11ab881eca","canopyx-flow-host.aea5ed35816df2af.js","canopyx-flow-host.1301fcf3454a723a.css"],"flow-checks":["ec2451183901867b","canopyx-flow-checks.29c4586a80f04b6a.js","canopyx-flow-checks.4502d6f7f7487b73.css"],"flow-options":["60450342239ded26","canopyx-flow-options.d31458a54aa4b1e7.js"],"flow-pick":["426718e2fa1f3cd0","canopyx-flow-pick.379205e09325bca3.js","canopyx-flow-pick.771b06ac50c141a1.css"],"flow-records":["f3c09661bdffb8d0","canopyx-flow-records.fddf130ace0764b1.js","canopyx-flow-records.e362111fb5f85732.css"],"flow-drafts":["4f112f64736ca8e3","canopyx-flow-drafts.09b88e275c7bc3b9.js","canopyx-flow-drafts.2dd86fb31fc10458.css"],"flow-preview":["f4fb5c42743873c0","canopyx-flow-preview.5778d5fce9ae0d78.js","canopyx-flow-preview.7928e22901da2088.css"],"flow-command":["39472118ca80ef02","canopyx-flow-command.78f4f931c52b1bfc.js","canopyx-flow-command.0c1247a371b7e775.css"],"flow-tasks":["147dd31638a9cb94","canopyx-flow-tasks.f012a521560f7075.js","canopyx-flow-tasks.524df90f908f81dd.css"],"flow-dialog":["ebb432cd6e34aba6","canopyx-flow-dialog.b00addd6a176198e.js","canopyx-flow-dialog.e6192e827eca25d2.css"],"flow-links":["2c14b19c9236e90d","canopyx-flow-links.7b6cfe42d28b6067.js"],"flow-endpoint":["f5cddfc9545e0920","canopyx-flow-endpoint.b31146202bd76aac.js"],"flow-consent":["22121778bc0d07c5","canopyx-flow-consent.749b97f13039bc71.js","canopyx-flow-consent.9f6580d3673ae8bf.css"]},PART_CODE=new Map(),partName=name=>typeof name==="string"&&/^[a-z][a-z\d-]{0,31}$/.test(name);
|
|
56
56
|
function registerPart(name,part){if(!partName(name)||!part||typeof part.version!=="string"||typeof part.install!=="function"||
|
|
57
57
|
part.requires!==undefined&&!(Array.isArray(part.requires)&&part.requires.every(partName)))throw new Error("CanopyX: invalid part.");PART_CODE.set(name,part)}
|
|
58
58
|
function definePart(View,name,label,context){const parts=View.prototype.parts||={};return parts[name]={name,label,context,parts}}
|
|
@@ -63,7 +63,11 @@ const load=(element,attribute,file,kind)=>new Promise((resolve,reject)=>{const u
|
|
|
63
63
|
.finally(()=>script.remove()).catch(error=>{part.scriptReady=null;throw error}));await Promise.all([style,code])}}
|
|
64
64
|
const code=PART_CODE.get(part.name);if(code?.version!==part.version)throw new Error("CanopyX: "+part.label+" and runtime must come from the same build.");for(const name of code.requires||[])await loadPart(part.parts[name]);code.install(part.context())}).catch(error=>{part.ready=null;throw error})}
|
|
65
65
|
const partQueue=global.CanopyXParts=global.CanopyXParts||[];partQueue.forEach(entry=>registerPart(...entry));partQueue.length=0;partQueue.push=(...entries)=>{entries.forEach(entry=>registerPart(...entry));return 0};class FlowHost{}
|
|
66
|
-
const FORMS_IMPL={},FLOW_IMPL={},activeFlows=new WeakMap();const FORMS_PART=definePart(FlowHost,"forms","forms",()=>({FORMS_IMPL,h,setText,fill,gridFreeze,gridMomentFromParts,boundedPattern,foldText}));const FLOW_PART=definePart(FlowHost,"flow","flow view",()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}));
|
|
66
|
+
const FORMS_IMPL={},FLOW_IMPL={},activeFlows=new WeakMap();const FORMS_PART=definePart(FlowHost,"forms","forms",()=>({FORMS_IMPL,h,setText,fill,gridFreeze,gridMomentFromParts,boundedPattern,foldText}));const FLOW_VIEWS=typeof FLOW_GRID_VIEWS==="object"?FLOW_GRID_VIEWS:Object.freeze({grid:null,tree:null,reporting:false});const FLOW_PART=definePart(FlowHost,"flow","flow view",()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}));const FLOW_FEATURES={};for(const name of["host","checks","options","pick","records","drafts","preview","command","tasks","dialog","links","endpoint","consent"])
|
|
67
|
+
FLOW_FEATURES[name]=definePart(FlowHost,"flow-"+name,"flow "+name,()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat,FLOW_VIEWS}));if(typeof DataGrid==="function")DataGrid.prototype.parts.forms=FORMS_PART;const FLOW_DOC_PARTS={command:"command",presets:"drafts",draft:"drafts",deepLinks:"links",endpoint:"endpoint"},FLOW_DOC_PLAIN={draft:"none",deepLinks:false};function flowFeatures(doc,hooks){const names=new Set(),add=(name,on)=>on&&names.add(name),list=value=>Array.isArray(value)?value:[];const object=value=>value!==null&&typeof value==="object"&&!Array.isArray(value),has=(value,...keys)=>object(value)&&keys.some(key=>value[key]!==undefined);for(const[key,name]of Object.entries(FLOW_DOC_PARTS))add(name,doc[key]!==undefined&&doc[key]!==FLOW_DOC_PLAIN[key]);add("tasks",doc.mode==="tasks");add("dialog",has(doc.layout,"launch")||has(doc.layout,"container")&&doc.layout.container!=="inline");add("preview",list(doc.endpoint?.routes).includes("preview"));for(const step of list(doc.steps).filter(object)){add("checks",step.checks!==undefined);add("pick",step.type==="pick");add("preview",step.preview===true);add("consent",has(step,"confirm","acknowledge"));for(const block of[step,...list(step.sections)]){if(block!==step)add("options",has(block,"source","dependsOn"));for(const field of object(block)?list(block.fields):[]){add("options",has(field,"dependsOn")||has(field,"source")&&field.source!=="document");add("records",object(field)&&field.type==="records")}}}
|
|
68
|
+
hooks=object(hooks)?hooks:{};return{required:[...names],optional:[typeof hooks.preview==="function"&&"preview",object(hooks.presetStore)&&"drafts"].filter(name=>name&&!names.has(name))}}
|
|
69
|
+
const FLOW_ASYNC_METHODS=["next","back","goTo","setState","validate","submit","runCheck","refreshOptions","saveDraft","setPreset"];const FLOW_SYNC_METHODS=["getStep","getValues","setValues","getState","isDirty","reset","setFieldError","setProgress","complete","setStepState","open","close"];function createFlow(host,options){if(!host||!options||!options.flow||typeof options.flow!=="object")throw new Error("CanopyX: createFlow needs a host element and a flow document.");const existing=activeFlows.get(host);if(existing)return existing;let impl=null,destroyed=false,mountError=null;const destroyedError=()=>new Error("CanopyX: this flow has been destroyed.");const requireImpl=name=>{if(destroyed)throw destroyedError();if(!impl)throw new Error("CanopyX: flow is not ready; await api.ready before calling "+name+"().");return impl};const editors=Object.assign({},...[...PLUGINS.values()].map(item=>item.editors||{}));const{required,optional}=flowFeatures(options.flow,options.hooks);const ready=Promise.all([loadPart(FLOW_PART),...required.map(name=>loadPart(FLOW_FEATURES[name])),...optional.map(name=>loadPart(FLOW_FEATURES[name])
|
|
70
|
+
.catch(error=>host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error,hook:"part"},bubbles:true}))))]).then(async()=>{if(destroyed)throw destroyedError();impl=FLOW_IMPL.create(host,options,editors);return impl.ready});const api={element:host,flow:options.flow,view:options.flow};for(const name of FLOW_SYNC_METHODS)api[name]=(...args)=>requireImpl(name)[name](...args);for(const name of FLOW_ASYNC_METHODS)api[name]=(...args)=>{if(destroyed)return Promise.reject(destroyedError());return ready.then(()=>requireImpl(name)[name](...args))};api.ready=ready.then(()=>{if(destroyed)throw destroyedError();return api});api.ready.catch(()=>{});api.destroy=()=>{if(destroyed)return;destroyed=true;try{impl?.destroy()}
|
|
67
71
|
finally{impl=null;mountError?.remove();if(activeFlows.get(host)===api){activeFlows.delete(host);if(host.canopyx===api)delete host.canopyx;host.removeAttribute("data-canopyx-mounted");host.removeAttribute("data-cx-ready");host.removeAttribute("data-cx-error");host.classList.remove("cx-flow-host")}}};ready.catch(error=>{if(destroyed)return;const notice=mountError=h("div","cx-mount-error");notice.setAttribute("role","alert");setText(notice,error&&error.message?error.message:String(error));host.appendChild(notice);host.setAttribute("data-cx-error",notice.textContent);host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error},bubbles:true}))});Object.freeze(api);activeFlows.set(host,api);host.classList.add("cx-host","cx-flow-host");host.setAttribute("data-canopyx-mounted","true");host.canopyx=api;return api}
|
|
68
72
|
const FLOW_API={createFlow};const configured=new Map();const flowId=value=>typeof value==="string"&&/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value);
|
|
69
73
|
function configure(id,options){if(!flowId(id)||!options||typeof options!=="object")throw new Error("CanopyX: configure needs a flow id and an options object.");if([...document.querySelectorAll("[data-canopyx-mounted]")].some(host=>host.canopyx?.view?.id===id))
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* Flow-host part: what the host-connected parts share,loading lines and quiet buttons. Each feature part brings its own */
|
|
2
|
+
.cx-flow-loading{margin-top:var(--cx-size-4px);font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
3
|
+
.cx-flow-loading.cx-flow-error{color:var(--cx-tone-danger);}
|
|
4
|
+
.cx-flow-btn.cx-flow-quiet{border-color:transparent;background:transparent;color:var(--cx-text-2);}
|
|
5
|
+
.cx-flow-btn.cx-flow-quiet:hover{background:var(--cx-hover-bg);color:var(--cx-text);}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"de4f9b11ab881eca",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 FLOW_PART_LABELS={flowTimedOut:"The host did not answer in time.",flowNoHostHook:"No host answers this.",flowRetry:"Try again",flowCancel:"Cancel"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_HOST_HOOKS=["options","fields","check","preview","status"],FLOW_HOST_GROUPS=["commands","draftStore","presetStore","pickers"];const FLOW_HOST_CALLBACKS=["onCheck","onLeaving","formatLink","parseLink"];const flowEmit=FlowView.prototype.emit,flowRestore=FlowView.prototype.setState;Object.assign(FlowView.prototype,{emit(name,detail,callback,cancelable){const go=flowEmit.call(this,name,detail,callback,cancelable);if(name==="flowValueChanged")this.afterChange(this.model.fields.get(detail.field));return go},setState(state){const before=new Map(this.values),moved=flowRestore.call(this,state);for(const field of[...this.model.fields.values()])if(!flowSameValue(before.get(field.id),this.values.get(field.id)))this.afterChange(field);this.refreshVisibility();return moved},hostState(){if(!this.checkHostOptions(this.options,this.options.hooks??{}))flowFail("invalid flow options");this.optionState=new Map();this.checkState=new Map();this.pickers=new Map();this.stepStatus=new Map();this.timeout=this.options.timeout??30000},checkHostOptions(options,hooks){return!(FLOW_HOST_HOOKS.some(name=>hooks[name]!=null&&typeof hooks[name]!=="function")||FLOW_HOST_GROUPS.some(name=>hooks[name]!=null&&!flowIs(hooks[name]))||
|
|
2
|
+
FLOW_HOST_CALLBACKS.some(name=>options[name]!=null&&typeof options[name]!=="function")||options.timeout!=null&&!(Number.isInteger(options.timeout)&&options.timeout>=100&&options.timeout<=600000)||
|
|
3
|
+
options.headers!=null&&!(flowIs(options.headers)&&Object.values(options.headers).every(value=>typeof value==="string")))},timed(name,run,controller=new AbortController()){const relay=()=>controller.abort(),lifetime=this.lifetime.signal;lifetime.addEventListener("abort",relay,{once:true});let timer=0,cancel;const cancelled=new Promise(resolve=>{cancel=()=>resolve({cancelled:true})});controller.signal.addEventListener("abort",cancel,{once:true});if(lifetime.aborted)controller.abort();if(controller.signal.aborted)cancel();const expired=new Promise(resolve=>{timer=setTimeout(()=>{const error=new Error(this.labels.flowTimedOut);this.report(error,name);resolve({failed:error.message,timedOut:true});controller.abort()},this.timeout)});const work=Promise.resolve().then(()=>controller.signal.aborted?undefined:run(controller.signal)).then(value=>({value}),error=>{if(controller.signal.aborted)return{cancelled:true};this.report(error,name);return{failed:error&&error.message?error.message:String(error)}});return Promise.race([work,cancelled,expired]).finally(()=>{clearTimeout(timer);lifetime.removeEventListener("abort",relay);controller.signal.removeEventListener("abort",cancel)})},afterChange(field){if(!field||this.destroyed||!this.checkState)return;this.checksChanged?.(field);for(const other of[...this.model.fields.values()])if(other.dependsOn.includes(field.id))this.dependentChanged?.(other);for(const step of this.model.steps)for(const block of step.blocks)if(block.section?.host&&block.section.dependsOn.includes(field.id)&&(this.sections.has(step.id)||this.sectionState?.has(block.section.id)))this.loadFields(step,block);this.markPreset?.();this.draftChanged?.()},setAnswer(field,value,source){const previous=this.values.get(field.id)??null;this.answersChanged();this.values.set(field.id,flowCopy(value));this.forgetHost(field);this.fields.get(field.id)?.mount(flowCopy(value));this.errors.delete(field.id);this.fields.get(field.id)?.message(null);this.emit("flowValueChanged",{field:field.id,value:flowCopy(value),previous:flowCopy(previous),source},"onValueChange");this.refreshVisibility();this.refreshReview()},startHostFeatures(){this.loadPresets?.();this.loadDraft?.();this.startLinks?.()},stepMoved(){if(this.checkState){this.draftChanged?.(true);this.linkMoved?.()}},releaseHostWork(){if(!this.checkState)return;for(const state of this.optionState.values())state.controller?.abort();for(const state of this.sectionState?.values()||[])state.controller?.abort();for(const state of this.checkState.values())state.controller?.abort();this.previewController?.abort();this.statusController?.abort();for(const picker of this.pickers.values()){try{picker.api?.destroy()}catch(error){this.report(error,"picker")}}
|
|
4
|
+
this.optionState.clear();this.sectionState?.clear();this.checkState.clear();this.pickers.clear();clearTimeout(this.draftTimer);this.preset=null}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-host",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"2c14b19c9236e90d",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;FLOW_HOST.links={document(doc,model){model.deepLinks=flowFlag(doc.deepLinks,"deepLinks")}};Object.assign(FlowView.prototype,{startLinks(){if(!this.model.deepLinks)return;const read=()=>this.followLink();global.addEventListener("hashchange",read,{signal:this.lifetime.signal});queueMicrotask(read)},linkFor(step){if(typeof this.options.formatLink==="function"){try{const link=this.options.formatLink(step.id);return typeof link==="string"?link.replace(/^#/, "") : null; } catch (error) { this.report(error, "formatLink"); return null; }
|
|
2
|
+
}
|
|
3
|
+
return step===this.hub?encodeURIComponent(this.model.id):encodeURIComponent(this.model.id)+ "/"+encodeURIComponent(step.id)},stepOfLink(fragment){if(typeof this.options.parseLink==="function"){try{const id=this.options.parseLink(fragment);return typeof id==="string"?id:null}catch(error){this.report(error,"parseLink");return null}}
|
|
4
|
+
const[flow,step,extra]=fragment.split("/").map(part=>{try{return decodeURIComponent(part)}catch(error){return null}});return flow===this.model.id&&extra===undefined?step??(this.hub?this.hub.id:null):null},followLink(){if(this.destroyed||this.phase!=="form")return;const fragment=global.location.hash.replace(/^#/, ""), id = fragment && this.stepOfLink(fragment);
|
|
5
|
+
if(!id||id===this.step.id)return;if(this.dialog&&!this.dialog.open)this.open();this.goTo(id).catch(error=>this.report(error,"goTo"))},linkMoved(){if(!this.model.deepLinks||this.phase!=="form")return;const link=this.linkFor(this.step);if(link===null||global.location.hash==="#"+link)return;try{global.history.replaceState(global.history.state,"",global.location.href.replace(/#.*$/, "") + "#" + link); } catch (error) { return; }
|
|
6
|
+
}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-links",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -54,7 +54,7 @@ function gridDateInputMoment(text,local){const match=/^(\d{4})-(\d{2})-(\d{2})T(
|
|
|
54
54
|
if(!match)return null;const moment=gridMomentFromParts(Number(match[1]),Number(match[2]),Number(match[3]),Number(match[4]),Number(match[5]),Number(match[6]||0),local);if(moment)moment.setTime(moment.getTime()+ Number((match[7]||"").padEnd(3,"0")));return moment}
|
|
55
55
|
function gridFreeze(value){if(value&&typeof value==="object"&&!Object.isFrozen(value)){Object.values(value).forEach(gridFreeze);Object.freeze(value)}
|
|
56
56
|
return value}
|
|
57
|
-
const PART_URL=typeof document!=="undefined"&&document.currentScript?.src;const PARTS={"forms":["a8c60f205d9babca","canopyx-forms.16987de1cdf9bda9.js","canopyx-forms.29058ce3c88af96e.css"],"flow":["
|
|
57
|
+
const PART_URL=typeof document!=="undefined"&&document.currentScript?.src;const PARTS={"forms":["a8c60f205d9babca","canopyx-forms.16987de1cdf9bda9.js","canopyx-forms.29058ce3c88af96e.css"],"flow":["842e1f1956f6213f","canopyx-flow.efc6250968e8b10a.js","canopyx-flow.3152e616c5557452.css"],"flow-host":["de4f9b11ab881eca","canopyx-flow-host.aea5ed35816df2af.js","canopyx-flow-host.1301fcf3454a723a.css"],"flow-checks":["ec2451183901867b","canopyx-flow-checks.29c4586a80f04b6a.js","canopyx-flow-checks.4502d6f7f7487b73.css"],"flow-options":["60450342239ded26","canopyx-flow-options.d31458a54aa4b1e7.js"],"flow-pick":["426718e2fa1f3cd0","canopyx-flow-pick.379205e09325bca3.js","canopyx-flow-pick.771b06ac50c141a1.css"],"flow-records":["f3c09661bdffb8d0","canopyx-flow-records.fddf130ace0764b1.js","canopyx-flow-records.e362111fb5f85732.css"],"flow-drafts":["4f112f64736ca8e3","canopyx-flow-drafts.09b88e275c7bc3b9.js","canopyx-flow-drafts.2dd86fb31fc10458.css"],"flow-preview":["f4fb5c42743873c0","canopyx-flow-preview.5778d5fce9ae0d78.js","canopyx-flow-preview.7928e22901da2088.css"],"flow-command":["39472118ca80ef02","canopyx-flow-command.78f4f931c52b1bfc.js","canopyx-flow-command.0c1247a371b7e775.css"],"flow-tasks":["147dd31638a9cb94","canopyx-flow-tasks.f012a521560f7075.js","canopyx-flow-tasks.524df90f908f81dd.css"],"flow-dialog":["ebb432cd6e34aba6","canopyx-flow-dialog.b00addd6a176198e.js","canopyx-flow-dialog.e6192e827eca25d2.css"],"flow-links":["2c14b19c9236e90d","canopyx-flow-links.7b6cfe42d28b6067.js"],"flow-endpoint":["f5cddfc9545e0920","canopyx-flow-endpoint.b31146202bd76aac.js"],"flow-consent":["22121778bc0d07c5","canopyx-flow-consent.749b97f13039bc71.js","canopyx-flow-consent.9f6580d3673ae8bf.css"]},PART_CODE=new Map(),partName=name=>typeof name==="string"&&/^[a-z][a-z\d-]{0,31}$/.test(name);
|
|
58
58
|
function registerPart(name,part){if(!partName(name)||!part||typeof part.version!=="string"||typeof part.install!=="function"||
|
|
59
59
|
part.requires!==undefined&&!(Array.isArray(part.requires)&&part.requires.every(partName)))throw new Error("CanopyX: invalid part.");PART_CODE.set(name,part)}
|
|
60
60
|
function definePart(View,name,label,context){const parts=View.prototype.parts||={};return parts[name]={name,label,context,parts}}
|
|
@@ -65,7 +65,11 @@ const load=(element,attribute,file,kind)=>new Promise((resolve,reject)=>{const u
|
|
|
65
65
|
.finally(()=>script.remove()).catch(error=>{part.scriptReady=null;throw error}));await Promise.all([style,code])}}
|
|
66
66
|
const code=PART_CODE.get(part.name);if(code?.version!==part.version)throw new Error("CanopyX: "+part.label+" and runtime must come from the same build.");for(const name of code.requires||[])await loadPart(part.parts[name]);code.install(part.context())}).catch(error=>{part.ready=null;throw error})}
|
|
67
67
|
const partQueue=global.CanopyXParts=global.CanopyXParts||[];partQueue.forEach(entry=>registerPart(...entry));partQueue.length=0;partQueue.push=(...entries)=>{entries.forEach(entry=>registerPart(...entry));return 0};class FlowHost{}
|
|
68
|
-
const FORMS_IMPL={},FLOW_IMPL={},activeFlows=new WeakMap();const FORMS_PART=definePart(FlowHost,"forms","forms",()=>({FORMS_IMPL,h,setText,fill,gridFreeze,gridMomentFromParts,boundedPattern,foldText}));const FLOW_PART=definePart(FlowHost,"flow","flow view",()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}));
|
|
68
|
+
const FORMS_IMPL={},FLOW_IMPL={},activeFlows=new WeakMap();const FORMS_PART=definePart(FlowHost,"forms","forms",()=>({FORMS_IMPL,h,setText,fill,gridFreeze,gridMomentFromParts,boundedPattern,foldText}));const FLOW_VIEWS=typeof FLOW_GRID_VIEWS==="object"?FLOW_GRID_VIEWS:Object.freeze({grid:null,tree:null,reporting:false});const FLOW_PART=definePart(FlowHost,"flow","flow view",()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}));const FLOW_FEATURES={};for(const name of["host","checks","options","pick","records","drafts","preview","command","tasks","dialog","links","endpoint","consent"])
|
|
69
|
+
FLOW_FEATURES[name]=definePart(FlowHost,"flow-"+name,"flow "+name,()=>({FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat,FLOW_VIEWS}));if(typeof DataGrid==="function")DataGrid.prototype.parts.forms=FORMS_PART;const FLOW_DOC_PARTS={command:"command",presets:"drafts",draft:"drafts",deepLinks:"links",endpoint:"endpoint"},FLOW_DOC_PLAIN={draft:"none",deepLinks:false};function flowFeatures(doc,hooks){const names=new Set(),add=(name,on)=>on&&names.add(name),list=value=>Array.isArray(value)?value:[];const object=value=>value!==null&&typeof value==="object"&&!Array.isArray(value),has=(value,...keys)=>object(value)&&keys.some(key=>value[key]!==undefined);for(const[key,name]of Object.entries(FLOW_DOC_PARTS))add(name,doc[key]!==undefined&&doc[key]!==FLOW_DOC_PLAIN[key]);add("tasks",doc.mode==="tasks");add("dialog",has(doc.layout,"launch")||has(doc.layout,"container")&&doc.layout.container!=="inline");add("preview",list(doc.endpoint?.routes).includes("preview"));for(const step of list(doc.steps).filter(object)){add("checks",step.checks!==undefined);add("pick",step.type==="pick");add("preview",step.preview===true);add("consent",has(step,"confirm","acknowledge"));for(const block of[step,...list(step.sections)]){if(block!==step)add("options",has(block,"source","dependsOn"));for(const field of object(block)?list(block.fields):[]){add("options",has(field,"dependsOn")||has(field,"source")&&field.source!=="document");add("records",object(field)&&field.type==="records")}}}
|
|
70
|
+
hooks=object(hooks)?hooks:{};return{required:[...names],optional:[typeof hooks.preview==="function"&&"preview",object(hooks.presetStore)&&"drafts"].filter(name=>name&&!names.has(name))}}
|
|
71
|
+
const FLOW_ASYNC_METHODS=["next","back","goTo","setState","validate","submit","runCheck","refreshOptions","saveDraft","setPreset"];const FLOW_SYNC_METHODS=["getStep","getValues","setValues","getState","isDirty","reset","setFieldError","setProgress","complete","setStepState","open","close"];function createFlow(host,options){if(!host||!options||!options.flow||typeof options.flow!=="object")throw new Error("CanopyX: createFlow needs a host element and a flow document.");const existing=activeFlows.get(host);if(existing)return existing;let impl=null,destroyed=false,mountError=null;const destroyedError=()=>new Error("CanopyX: this flow has been destroyed.");const requireImpl=name=>{if(destroyed)throw destroyedError();if(!impl)throw new Error("CanopyX: flow is not ready; await api.ready before calling "+name+"().");return impl};const editors=Object.assign({},...[...PLUGINS.values()].map(item=>item.editors||{}));const{required,optional}=flowFeatures(options.flow,options.hooks);const ready=Promise.all([loadPart(FLOW_PART),...required.map(name=>loadPart(FLOW_FEATURES[name])),...optional.map(name=>loadPart(FLOW_FEATURES[name])
|
|
72
|
+
.catch(error=>host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error,hook:"part"},bubbles:true}))))]).then(async()=>{if(destroyed)throw destroyedError();impl=FLOW_IMPL.create(host,options,editors);return impl.ready});const api={element:host,flow:options.flow,view:options.flow};for(const name of FLOW_SYNC_METHODS)api[name]=(...args)=>requireImpl(name)[name](...args);for(const name of FLOW_ASYNC_METHODS)api[name]=(...args)=>{if(destroyed)return Promise.reject(destroyedError());return ready.then(()=>requireImpl(name)[name](...args))};api.ready=ready.then(()=>{if(destroyed)throw destroyedError();return api});api.ready.catch(()=>{});api.destroy=()=>{if(destroyed)return;destroyed=true;try{impl?.destroy()}
|
|
69
73
|
finally{impl=null;mountError?.remove();if(activeFlows.get(host)===api){activeFlows.delete(host);if(host.canopyx===api)delete host.canopyx;host.removeAttribute("data-canopyx-mounted");host.removeAttribute("data-cx-ready");host.removeAttribute("data-cx-error");host.classList.remove("cx-flow-host")}}};ready.catch(error=>{if(destroyed)return;const notice=mountError=h("div","cx-mount-error");notice.setAttribute("role","alert");setText(notice,error&&error.message?error.message:String(error));host.appendChild(notice);host.setAttribute("data-cx-error",notice.textContent);host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error},bubbles:true}))});Object.freeze(api);activeFlows.set(host,api);host.classList.add("cx-host","cx-flow-host");host.setAttribute("data-canopyx-mounted","true");host.canopyx=api;return api}
|
|
70
74
|
const FLOW_API={createFlow};const configured=new Map();const flowId=value=>typeof value==="string"&&/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value);
|
|
71
75
|
function configure(id,options){if(!flowId(id)||!options||typeof options!=="object")throw new Error("CanopyX: configure needs a flow id and an options object.");if([...document.querySelectorAll("[data-canopyx-mounted]")].some(host=>host.canopyx?.view?.id===id))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"60450342239ded26",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={flowOptionsLoading:"Loading options…",flowOptionsFailed:"The options could not load: {message}",flowFieldsLoading:"Loading fields…",flowFieldsFailed:"The fields could not load: {message}"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_DYNAMIC_TYPES=["text","number","date","datetime","time","select","toggle","lookup","choice","multiChoice","textarea","list","duration"];FLOW_HOST.options={section(section,raw){section.host=raw.source==="host";if(raw.source!==undefined&&!section.host)flowFail("section "+section.id+" has an unknown source");if(section.host?raw.fields!==undefined&&(!Array.isArray(raw.fields)||raw.fields.length):!Array.isArray(raw.fields))flowFail("section "+section.id+" needs an array of fields, or source \"host\" and none");if(raw.dependsOn!==undefined&&(!section.host||!Array.isArray(raw.dependsOn)||!raw.dependsOn.length||raw.dependsOn.length>16||raw.dependsOn.some(name=>typeof name!=="string")))
|
|
2
|
+
flowFail("section "+section.id+" depends on 1 to 16 fields, as a host section only");section.dependsOn=raw.dependsOn?[...new Set(raw.dependsOn)]:[]},field(field,raw,choosing,bad){if(raw.source!==undefined&&(!["document","host"].includes(raw.source)||!choosing))bad("takes a source as a field that chooses only");field.host=raw.source==="host";if(raw.dependsOn!==undefined&&(!Array.isArray(raw.dependsOn)||!raw.dependsOn.length||raw.dependsOn.length>16||raw.dependsOn.some(name=>typeof name!=="string")))bad("needs 1 to 16 field ids in dependsOn");field.dependsOn=raw.dependsOn?[...new Set(raw.dependsOn)]:[]},finish(model){const known=(id,what)=>{const field=model.fields.get(id);if(!field)flowFail(what+" names the unknown field '"+id+"'");return field};for(const field of model.fields.values())for(const id of field.dependsOn){if(id===field.id||known(id,"field "+field.id).type==="secret")flowFail("field "+field.id+" cannot depend on itself or on a secret")}
|
|
3
|
+
const state=new Map(),visit=field=>{if(state.get(field.id)===2)return;if(state.get(field.id)===1)flowFail("field "+field.id+" depends on itself through dependsOn");state.set(field.id,1);field.dependsOn.forEach(id=>visit(model.fields.get(id)));state.set(field.id,2)};model.fields.forEach(visit);for(const step of model.steps)for(const block of step.blocks)if(block.section?.host)for(const id of block.section.dependsOn)if(known(id,"section "+block.section.id).type==="secret")flowFail("section "+block.section.id+" cannot depend on a secret")},built(view,step){for(const block of step.blocks)if(block.section?.host)queueMicrotask(()=>view.destroyed||view.sectionState?.has(block.section.id)||view.loadFields(step,block));queueMicrotask(()=>view.destroyed||view.prepareVisible())}};function flowNote(view,fv,text,retry,failed){let note=fv.note;if(!note){if(!text)return;note=fv.note=h("p","cx-flow-loading");note.id=view.id+"-f-"+fv.field.id+"-note";note.setAttribute("role","status");fv.element.querySelector(":scope > .cx-flow-error").before(note)}
|
|
4
|
+
note.replaceChildren();note.hidden=!text;note.classList.toggle("cx-flow-error",!!failed);if(text){note.append(text);if(retry){const again=h("button","cx-flow-link");again.type="button";setText(again,view.labels.flowRetry);again.addEventListener("click",retry);note.append(" ",again)}}
|
|
5
|
+
if(fv.instance)fv.describe()}
|
|
6
|
+
Object.assign(FlowView.prototype,{dependentChanged(field){if(field.host){this.loadOptions(field);return}
|
|
7
|
+
const empty=field.kind==="list"||field.kind==="records"||field.kind==="pick"?[]:null;if(!flowSameValue(this.values.get(field.id)??null,empty))this.setAnswer(field,empty,"depends")},prepareVisible(){for(const section of this.sections.values())for(const block of section._cxBlocks||[])if(block.section?.host&&!block.fields.length&&flowTest(block.section.when,id=>this.read(id)))block.element.hidden=false;for(const field of this.model.fields.values())if(field.host&&this.fields.has(field.id)&&this.visible(field)&&!this.optionState.has(field.id))this.loadOptions(field)},loadOptions(field){const state=this.optionState.get(field.id)||{},controller=new AbortController(),ticket={};state.controller?.abort();Object.assign(state,{controller,ticket,loading:true,error:null});this.optionState.set(field.id,state);this.paintOptions(field);const run=typeof this.hooks.options==="function"?signal=>this.hooks.options(field.id,this.getValues(),{signal}):()=>{throw new Error(this.labels.flowNoHostHook)};return this.timed("options",run,controller).then(answer=>{if(state.ticket!==ticket||this.destroyed||answer.cancelled)return;state.loading=false;let options=null,problem=answer.failed||null;if(!problem){try{options=Array.isArray(answer.value)?flowOptions(answer.value,message=>flowFail("the options of "+field.id+" "+message)):flowFail("the options hook answers a list of options")}
|
|
8
|
+
catch(error){this.report(error,"options");problem=error.message}}
|
|
9
|
+
if(problem){state.error=problem;this.paintOptions(field);return}
|
|
10
|
+
field.options=options;state.loaded=true;const value=this.values.get(field.id)??null,allowed=item=>options.some(option=>option.value===item&&!option.disabledReason);const next=Array.isArray(value)?value.filter(allowed):value!==null&&!allowed(value)?null:value;if(!flowSameValue(next,value))this.setAnswer(field,next,"options");else this.fields.get(field.id)?.mount(flowCopy(value));this.paintOptions(field);this.reviewStale=true})},paintOptions(field){const fv=this.fields.get(field.id),state=this.optionState.get(field.id);if(!fv||!state)return;flowNote(this,fv,state.loading?this.labels.flowOptionsLoading:state.error?fill(this.labels.flowOptionsFailed,{message:state.error}):null,state.error?()=>this.loadOptions(field):null,!!state.error)},refreshOptions(id){const field=this.model.fields.get(id);if(!field||!field.host)flowFail("refreshOptions takes the id of a field whose options come from the host");return this.loadOptions(field)},loadFields(step,block){const section=block.section,state=(this.sectionState||=new Map()).get(section.id)||{},controller=new AbortController(),ticket={};state.controller?.abort();Object.assign(state,{controller,ticket,loading:true,error:null});this.sectionState.set(section.id,state);this.paintSection(block,this.labels.flowFieldsLoading);const run=typeof this.hooks.fields==="function"?signal=>this.hooks.fields(section.id,this.getValues(),{signal}):()=>{throw new Error(this.labels.flowNoHostHook)};return state.work=this.timed("fields",run,controller).then(answer=>{if(state.ticket!==ticket||this.destroyed||answer.cancelled)return;state.loading=false;let fields=null,problem=answer.failed||null;if(!problem){try{fields=this.readDynamic(step,block,answer.value)}catch(error){this.report(error,"fields");problem=error.message}}
|
|
11
|
+
if(problem){state.error=problem;this.paintSection(block,fill(this.labels.flowFieldsFailed,{message:problem}),()=>this.loadFields(step,block));return}
|
|
12
|
+
this.placeDynamic(step,block,fields);this.paintSection(block,null)})},readDynamic(step,block,raw){if(!Array.isArray(raw)||raw.length>64)flowFail("the fields hook answers a list of at most 64 fields");const model=this.model,old=new Set(block.fields.map(field=>field.id)),scratch={fields:new Map([...model.fields].filter(([id])=>!old.has(id))),sections:model.sections,checks:model.checks};const fields=raw.map(entry=>{if(!flowIs(entry)||!FLOW_DYNAMIC_TYPES.includes(entry.type)||entry.path!==undefined||entry.parameter!==undefined||entry.source!==undefined||entry.dependsOn!==undefined)flowFail("a dynamic field has a type, path, parameter, source or dependsOn the host cannot supply");return flowField(entry,step,block.section,scratch)});for(const field of fields){field.when=field.raw.when===undefined?null:flowCondition(field.raw.when,scratch,{groups:0,filters:0});if([...flowRefs(field.when)].some(id=>fields.some(other=>other.id===id)))flowFail("a dynamic field's condition reads another dynamic field");field.refs=flowRefs(step.when,flowRefs(block.section.when,flowRefs(field.when)));field.dynamic=true}
|
|
13
|
+
return fields},placeDynamic(step,block,fields){const model=this.model;for(const field of block.fields){model.fields.delete(field.id);this.fields.get(field.id)?.destroy();this.fields.delete(field.id)}
|
|
14
|
+
for(const field of fields){model.fields.set(field.id,field);const kept=this.values.get(field.id);if(kept===undefined||!flowFits(field,kept))this.values.set(field.id,flowCopy(model.initial&&Object.hasOwn(model.initial,field.id)&&flowFits(field,model.initial[field.id])?model.initial[field.id]:field.start))}
|
|
15
|
+
block.fields.splice(0,block.fields.length,...fields);this.answersChanged();this.visibility.clear();this.reviewStale=true;const element=this.sections.get(step.id)?._cxBlocks?.find(entry=>entry.fields===block.fields)?.element,grid=element?.querySelector(".cx-flow-grid");if(grid){grid.replaceChildren(...fields.map(field=>{const fv=flowFieldView(this,field);this.fields.set(field.id,fv);fv.mount(flowCopy(this.values.get(field.id)??null));fv.element.hidden=!this.visible(field);return fv.element}))}
|
|
16
|
+
this.refreshVisibility()},paintSection(block,text,retry){const element=[...this.sections.values()].flatMap(section=>section._cxBlocks||[]).find(entry=>entry.fields===block.fields)?.element;if(!element)return;let note=element.querySelector(":scope > .cx-flow-loading");if(!text){note?.remove();return}
|
|
17
|
+
if(!note){note=h("p","cx-flow-loading");note.setAttribute("role","status");element.querySelector(".cx-flow-grid").before(note)}
|
|
18
|
+
note.replaceChildren(text);note.classList.toggle("cx-flow-error",!!retry);if(retry){const again=h("button","cx-flow-link");again.type="button";setText(again,this.labels.flowRetry);again.addEventListener("click",retry);note.append(" ",again)}}});const{next:flowOptionsNext,submit:flowOptionsSubmit,validateStep:flowOptionsValidate}=FlowView.prototype;Object.assign(FlowView.prototype,{async sectionPrerequisites(steps){const generation=this.generation,errors=[];for(const step of steps)if(this.stepVisible(step))for(const block of step.blocks){if(!block.section?.host||!flowTest(block.section.when,id=>this.read(id)))continue;let state=this.sectionState?.get(block.section.id);if(!state){this.loadFields(step,block);state=this.sectionState.get(block.section.id)}
|
|
19
|
+
while(state.loading){await state.work;if(this.destroyed||this.generation!==generation)return{cancelled:true,errors};state=this.sectionState.get(block.section.id);if(!state)return{cancelled:true,errors}}
|
|
20
|
+
if(!this.stepVisible(step)||!flowTest(block.section.when,id=>this.read(id)))continue;if(state.error)errors.push({step:step.id,message:fill(this.labels.flowFieldsFailed,{message:state.error})})}
|
|
21
|
+
return{errors}},async next(){if(this.busy||this.destroyed||this.phase!=="form")return false;const step=this.step,generation=this.generation,result=await this.sectionPrerequisites([step]);if(result.cancelled||this.destroyed||generation!==this.generation||step!==this.step)return false;if(result.errors.length){this.stepErrors.set(step.id,result.errors[0].message);this.showSummary(step);return false}
|
|
22
|
+
return flowOptionsNext.call(this)},async submit(){if(this.busy||this.destroyed||this.phase!=="form")return null;const generation=this.generation,result=await this.sectionPrerequisites(this.visibleSteps());if(result.cancelled||this.destroyed||generation!==this.generation)return null;if(result.errors.length){for(const error of result.errors)this.stepErrors.set(error.step,error.message);return null}
|
|
23
|
+
return flowOptionsSubmit.call(this)},async validateStep(step,show,current=()=>true){const result=await this.sectionPrerequisites([step]);return result.cancelled||result.errors.length||!current()
|
|
24
|
+
?{valid:false,errors:result.errors,warnings:[],moved:!!result.cancelled}
|
|
25
|
+
:flowOptionsValidate.call(this,step,show,current)}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-options",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* The flow-pick part: the view,and the tray beside it on wide forms. */
|
|
2
|
+
.cx-flow{--cx-size-260px:260px;}
|
|
3
|
+
.cx-flow-type-pick{max-width:none;}
|
|
4
|
+
.cx-flow .cx-flow-label-quiet{position:absolute;width:var(--cx-size-1px);height:var(--cx-size-1px);overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
|
|
5
|
+
.cx-flow-label-quiet + .cx-flow-help{margin-top:var(--cx-size-4px);}
|
|
6
|
+
.cx-flow-step:has(.cx-flow-type-pick){max-width:none;}
|
|
7
|
+
.cx-flow-pick{display:grid;gap:var(--cx-size-12px);}
|
|
8
|
+
@container (min-width:860px){.cx-flow-pick.cx-flow-pick-tray{grid-template-columns:minmax(0,1fr) var(--cx-size-260px);align-items:start;}}
|
|
9
|
+
.cx-flow-pick-view{min-width:0;border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-radius);overflow:hidden;}
|
|
10
|
+
.cx-flow-pick-view > .cx-host{height:100%;}
|
|
11
|
+
.cx-flow-tray{display:flex;flex-direction:column;gap:var(--cx-size-6px);padding:var(--cx-size-10px) var(--cx-size-12px);border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-radius);background:var(--cx-head-bg);}
|
|
12
|
+
.cx-flow-tray-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--cx-size-8px);}
|
|
13
|
+
.cx-flow-tray-title{font-size:var(--cx-size-13d5px);font-weight:600;}
|
|
14
|
+
.cx-flow-tray-list{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--cx-size-4px);max-height:var(--cx-size-320px);overflow:auto;}
|
|
15
|
+
.cx-flow-tray-item{display:flex;align-items:center;gap:var(--cx-size-6px);min-height:var(--cx-size-28px);padding-inline-start:var(--cx-size-8px);border-radius:var(--cx-radius-sm);background:var(--cx-surface);}
|
|
16
|
+
.cx-flow-tray-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--cx-size-13d5px);}
|
|
17
|
+
.cx-flow-tray .cx-flow-icon-btn{width:var(--cx-size-28px);height:var(--cx-size-28px);border-color:transparent;background:transparent;font-size:var(--cx-size-16px);}
|
|
18
|
+
.cx-flow-tray-more,.cx-flow-tray-empty{font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
19
|
+
@media print{.cx-flow-tray .cx-flow-icon-btn{display:none;}}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"426718e2fa1f3cd0",requires:["flow","flow-host"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat,FLOW_VIEWS}=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={flowPickMore:"and {count} more",flowPickCount:"[one]{count} chosen[other]{count} chosen",flowPickChosen:"Chosen",flowPickNone:"Nothing chosen yet.",flowPickOneByOne:"Choose the records one by one or a page at a time."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_PICK_VISIBLE=60;FLOW_HOST.pick={read(raw,step,model){flowKeys(raw,["field","view","label","tray","height"],"The pick of step "+step.id);if(!flowIs(raw.view)||raw.view.protocol!=="canopyx/2"||!["grid","view"].includes(raw.view.type))flowFail("the pick of step "+step.id+" needs a grid or tree view document");if(raw.tray!==undefined&&typeof raw.tray!=="boolean")flowFail("the tray of step "+step.id+" is true or false");flowKeys(raw.field,["id","label","help","required","when","parameter","path","sensitive","validation","default"],"The field of pick step "+step.id);const field=flowField(Object.assign({},raw.field,{type:"pick"}),step,null,model),columns=Array.isArray(raw.view.columns)?raw.view.columns:[];const label=raw.label??(columns[0]&&columns[0].id);if(typeof label!=="string"||!columns.some(column=>column&&column.id===label))flowFail("the pick of step "+step.id+" needs a label column the view declares");if(raw.height!==undefined&&(!Number.isInteger(raw.height)||raw.height<160||raw.height>1200))flowFail("the pick of step "+step.id+" needs a height of 160 to 1200 pixels");step.pick=Object.freeze({view:raw.view,kind:raw.view.type==="grid"?"grid":"tree",label,tray:raw.tray!==false,height:raw.height??420,field});step.blocks.push({section:null,fields:[field]})},built(view,step,section){if(step.pick?.field.label===step.title)section.querySelector(".cx-flow-label")?.classList.add("cx-flow-label-quiet")},input:(context,field,view)=>flowPickInput(context,field,view),fits:(field,value)=>Array.isArray(value)&&value.length<=1000&&value.every(item=>flowIs(item)&&Object.keys(item).every(key=>key==="id"||key==="label")&&
|
|
2
|
+
typeof item.id==="string"&&item.id.length>0&&item.id.length<=256&&typeof item.label==="string"&&item.label.length<=400)&&new Set(value.map(item=>item.id)).size===value.length,display:(view,value)=>value.slice(0,12).map(item=>item.label||item.id).join("\n")+(value.length>12?"\n"+fill(view.labels.flowPickMore,{count:value.length-12}):"")};const flowAnswer=FlowView.prototype.read;Object.assign(FlowView.prototype,{read(id){const value=flowAnswer.call(this,id);return this.model.fields.get(id)?.type==="pick"&&Array.isArray(value)?value.map(item=>item.id):value}});function flowPickText(cell,fallback){if(cell!==null&&typeof cell==="object")return typeof cell.text==="string"?cell.text:cell.value==null?fallback:String(cell.value);return cell==null?fallback:String(cell)}
|
|
3
|
+
function flowPickInput(context,field,view){const pick=field.step.pick,labels=view.labels,box=h("div","cx-flow-pick" +(pick.tray?" cx-flow-pick-tray":""));let items=(Array.isArray(context.value)?context.value:[]).map(item=>({id:item.id,label:item.label}));let picker=view.pickers.get(field.id);if(!picker)picker=flowPickView(view,field,pick);const changed=()=>box.dispatchEvent(new Event("change",{bubbles:true}));const tray=pick.tray?h("section","cx-flow-tray"):null,trayList=h("ul","cx-flow-tray-list"),trayHead=h("h4","cx-flow-tray-title"),clear=h("button","cx-flow-link");const count=h("p","cx-flow-count");count.setAttribute("aria-live","polite");const paint=()=>{setText(count,fill(plural(labels.flowPickCount,items.length,view.lang),{count:numberFormat(view.lang,0,0).format(items.length)}));if(!tray)return;trayList.replaceChildren(...items.slice(0,FLOW_PICK_VISIBLE).map(item=>{const row=h("li","cx-flow-tray-item"),text=h("span","cx-flow-tray-label"),remove=h("button","cx-flow-icon-btn");setText(text,item.label||item.id);text.title=item.label||item.id;remove.type="button";setText(remove,"×");remove.hidden=context.readOnly;remove.setAttribute("aria-label",fill(labels.flowRemoveItem,{item:item.label||item.id}));remove.addEventListener("click",()=>{const at=items.findIndex(entry=>entry.id===item.id);items=items.filter(entry=>entry.id!==item.id);picker.select(items.map(entry=>entry.id));paint();changed();(trayList.children[Math.min(at,trayList.children.length-1)]?.querySelector("button")||clear).focus()});row.append(text,remove);return row}));if(items.length>FLOW_PICK_VISIBLE){const more=h("li","cx-flow-tray-more");setText(more,fill(labels.flowPickMore,{count:items.length-FLOW_PICK_VISIBLE}));trayList.appendChild(more)}
|
|
4
|
+
clear.hidden=!items.length||context.readOnly;const none=!items.length;trayList.hidden=none;count.hidden=none;tray.querySelector(".cx-flow-tray-empty").hidden=!none};if(tray){setText(trayHead,labels.flowPickChosen);clear.type="button";setText(clear,labels.flowSelectNone);clear.addEventListener("click",()=>{items=[];picker.select([]);paint();changed()});const head=h("div","cx-flow-tray-head"),empty=h("p","cx-flow-tray-empty");setText(empty,labels.flowPickNone);head.append(trayHead,clear);tray.setAttribute("aria-label",labels.flowPickChosen);tray.append(head,count,trayList,empty)}
|
|
5
|
+
picker.onChange=(ids,lookup)=>{const known=new Map(items.map(item=>[item.id,item]));const next=ids.slice(0,1000).map(id=>known.get(id)||{id,label:lookup(id).slice(0,400)});picker.wanted=next.map(item=>item.id);if(flowSameValue(picker.wanted,items.map(item=>item.id)))return;items=next;paint();changed()};picker.select(items.map(item=>item.id));box.append(picker.holder);if(tray)box.appendChild(tray);else box.appendChild(count);paint();return{element:box,control:picker.holder,group:true,focus:()=>picker.api?.focus?.(),getValue:()=>items.map(item=>({id:item.id,label:item.label})),destroy:()=>{picker.onChange=null}}}
|
|
6
|
+
function flowPickView(view,field,pick){const factory=pick.kind==="grid"?FLOW_VIEWS.grid:FLOW_VIEWS.tree;if(typeof factory!=="function")flowFail("the pick step "+field.step.id+" needs the data-grid or reporting entry point, which hold grids and trees");const holder=h("div","cx-flow-pick-view"),max=field.items.max;holder.style.height=pick.height+"px";const doc=Object.assign(flowCopy(pick.view),{selection:max===1?"single":"multiple"});if(pick.kind==="grid"&&FLOW_VIEWS.reporting)doc.reporting=Object.assign({},doc.reporting,{selectionControls:true});const picker={holder,api:null,wanted:[],onChange:null};const options=Object.assign({},view.hooks.pickers?.[field.step.id],{view:doc});if(pick.kind==="tree")delete options.onSelect;picker.api=factory(holder,options);view.pickers.set(field.id,picker);const label=pick.label;const labelsOf=(ids,nodes)=>{if(pick.kind==="grid"){const rows=new Map((picker.api.getLoadedRows?.(ids)||[]).map(row=>[row.id,row]));return id=>flowPickText(rows.get(id)?.cells?.[label],id)}
|
|
7
|
+
const byId=new Map(nodes.map(node=>[node.id,node]));return id=>{const node=byId.get(id);return node?flowPickText(node.cells?.[label],node.label||id):id}};holder.addEventListener("canopyx:select",event=>{event.stopPropagation();if(picker.applying)return;if(event.detail?.selection?.kind==="query"){view.announce(view.labels.flowPickOneByOne);picker.select(picker.wanted);return}
|
|
8
|
+
const nodes=event.detail?.nodes||[],ids=pick.kind==="grid"?[...(event.detail?.ids||[])]:nodes.map(node=>node.id);const known=new Set(picker.wanted);picker.onChange?.(ids,labelsOf(ids.filter(id=>!known.has(id)),nodes))});picker.select=ids=>{picker.wanted=ids;picker.api.ready.then(()=>{if(picker.wanted!==ids)return;picker.applying=true;try{picker.api.setSelection(ids)}catch(error){view.report(error,"picker")}finally{picker.applying=false}},error=>view.report(error,"picker"))};return picker}
|
|
9
|
+
installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-pick",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* The flow-preview part: the host's preview on the review page. */
|
|
2
|
+
.cx-flow-preview{padding:var(--cx-size-10px) var(--cx-size-14px) var(--cx-size-12px);border-radius:var(--cx-radius);border:var(--cx-size-1px) solid var(--cx-line);}
|
|
3
|
+
.cx-flow-preview .cx-flow-review-title{border-bottom:0;padding-bottom:0;}
|
|
4
|
+
.cx-flow-preview-title{margin-top:var(--cx-size-6px);font-weight:600;}
|
|
5
|
+
.cx-flow-preview-rows .cx-flow-answer{grid-template-columns:minmax(var(--cx-size-140px),36%) minmax(0,1fr);}
|
|
6
|
+
.cx-flow-preview-note{margin-top:var(--cx-size-6px);font-size:var(--cx-size-13px);color:var(--cx-text-2);}
|
|
7
|
+
.cx-flow-tone-success{color:var(--cx-tone-success);}
|
|
8
|
+
.cx-flow-tone-info{color:var(--cx-tone-info);}
|
|
9
|
+
.cx-flow-tone-warning{color:var(--cx-tone-warning);font-weight:600;}
|
|
10
|
+
.cx-flow-tone-danger{color:var(--cx-tone-danger);font-weight:600;}
|
|
11
|
+
.cx-flow-step > .cx-flow-preview{margin-top:var(--cx-flow-gap);}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"f4fb5c42743873c0",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={flowPreview:"Preview",flowPreviewLoading:"Loading the preview…",flowPreviewFailed:"The preview could not load: {message}",flowPreviewNone:"Nothing to preview."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_PREVIEW_TONES=["info","success","warning","danger"];FLOW_HOST.preview={review(view,section){if(typeof view.hooks.preview==="function")section.appendChild(view.previewBlock())}};Object.assign(FlowView.prototype,{previewBlock(){const labels=this.labels,block=h("section","cx-flow-preview"),heading=h("h4","cx-flow-review-title"),body=h("div","cx-flow-preview-body");heading.id=this.id+"-rp";setText(heading,labels.flowPreview);block.setAttribute("aria-labelledby",heading.id);block.setAttribute("aria-busy","true");const loading=h("p","cx-flow-loading");setText(loading,labels.flowPreviewLoading);body.appendChild(loading);block.append(heading,body);this.previewController?.abort();const controller=this.previewController=new AbortController();this.timed("preview",signal=>this.hooks.preview(this.getValues(),{signal}),controller).then(answer=>{if(this.previewController!==controller||this.destroyed||answer.cancelled)return;block.removeAttribute("aria-busy");if(answer.failed){const failed=h("p","cx-flow-error");setText(failed,fill(labels.flowPreviewFailed,{message:answer.failed}));body.replaceChildren(failed);return}
|
|
2
|
+
body.replaceChildren(...this.previewContent(answer.value))});return block},previewContent(value){const text=item=>typeof item==="string"&&/\S/.test(item) ? item.slice(0, 1000) : null, nodes = [];
|
|
3
|
+
const content=Array.isArray(value)?{rows:value}:flowIs(value)?value:null;if(!content){const none=h("p","cx-flow-none");setText(none,this.labels.flowPreviewNone);return[none]}
|
|
4
|
+
if(text(content.title)){const title=h("p","cx-flow-preview-title");setText(title,text(content.title));nodes.push(title)}
|
|
5
|
+
const rows=(Array.isArray(content.rows)?content.rows:[]).slice(0,50).filter(row=>flowIs(row)&&text(row.label)&&flowScalar(row.value));if(rows.length){const list=h("dl","cx-flow-answers cx-flow-preview-rows");for(const row of rows){const item=h("div","cx-flow-answer"),term=h("dt"),data=h("dd","cx-flow-answer-value");setText(term,text(row.label));setText(data,String(row.value).slice(0,1000));if(FLOW_PREVIEW_TONES.includes(row.tone))data.classList.add("cx-flow-tone-"+row.tone);item.append(term,data);list.appendChild(item)}
|
|
6
|
+
nodes.push(list)}
|
|
7
|
+
for(const note of(Array.isArray(content.notes)?content.notes:[]).slice(0,10).map(text).filter(Boolean)){const line=h("p","cx-flow-preview-note");setText(line,note);nodes.push(line)}
|
|
8
|
+
if(!nodes.length){const none=h("p","cx-flow-none");setText(none,this.labels.flowPreviewNone);nodes.push(none)}
|
|
9
|
+
return nodes}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-preview",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* The flow-records part: a compact table of editors. */
|
|
2
|
+
.cx-flow-records-scroll{max-width:100%;overflow-x:auto;}
|
|
3
|
+
.cx-flow-records-table{border-collapse:collapse;min-width:min(100%,var(--cx-size-440px));}
|
|
4
|
+
.cx-flow-records-table th{padding:0 var(--cx-size-8px) var(--cx-size-6px) 0;font-size:var(--cx-size-12px);font-weight:600;color:var(--cx-muted);text-align:start;white-space:nowrap;}
|
|
5
|
+
.cx-flow-records-table td{padding:0 var(--cx-size-8px) var(--cx-size-6px) 0;vertical-align:top;}
|
|
6
|
+
.cx-flow-records-table .cx-flow-input{min-width:var(--cx-size-92px);}
|
|
7
|
+
.cx-flow-records-table input[type="checkbox"].cx-flow-input{width:var(--cx-size-18px);min-width:0;height:var(--cx-flow-control);margin:0;accent-color:var(--cx-accent);}
|
|
8
|
+
.cx-flow-records-actions{width:var(--cx-flow-control);}
|
|
9
|
+
.cx-flow-records-empty{font-size:var(--cx-size-13px);color:var(--cx-muted);}
|