@evotecit/canopyx 0.3.5 → 0.3.7
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-calendar.js +4 -3
- package/CanopyX.Html/Assets/canopyx-detail-rows.js +2 -2
- package/CanopyX.Html/Assets/canopyx-filter-editor.js +2 -1
- 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-formatting.js +7 -6
- package/CanopyX.Html/Assets/canopyx-grid-module.mjs +69 -42
- package/CanopyX.Html/Assets/canopyx-grid.css +23 -0
- package/CanopyX.Html/Assets/canopyx-grid.js +69 -42
- package/CanopyX.Html/Assets/canopyx-interactive.js +4 -4
- package/CanopyX.Html/Assets/canopyx-labels-de.js +77 -1
- package/CanopyX.Html/Assets/canopyx-labels-es.js +77 -1
- package/CanopyX.Html/Assets/canopyx-labels-fr.js +77 -1
- package/CanopyX.Html/Assets/canopyx-labels-it.js +77 -1
- package/CanopyX.Html/Assets/canopyx-labels-pl.js +77 -1
- package/CanopyX.Html/Assets/canopyx-live.js +4 -3
- package/CanopyX.Html/Assets/canopyx-matrix.js +3 -3
- package/CanopyX.Html/Assets/canopyx-module.mjs +4 -4
- package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +71 -44
- package/CanopyX.Html/Assets/canopyx-reporting.css +23 -0
- package/CanopyX.Html/Assets/canopyx-reporting.js +71 -44
- package/CanopyX.Html/Assets/canopyx-tree-tools.js +10 -8
- package/CanopyX.Html/Assets/canopyx.js +4 -4
- package/JavaScript/flow.d.ts +180 -9
- package/JavaScript/grid.d.ts +8 -0
- package/JavaScript/locales/de.mjs +77 -1
- package/JavaScript/locales/es.mjs +77 -1
- package/JavaScript/locales/fr.mjs +77 -1
- package/JavaScript/locales/it.mjs +77 -1
- package/JavaScript/locales/pl.mjs +77 -1
- package/JavaScript/protocol.d.ts +1 -1
- package/README.md +6 -2
- package/package.json +3 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
(function(global){"use strict";const installed=new WeakSet();const part={version:"
|
|
2
|
-
if(typeof text!=="string"||!/\S/.test(text)) flowFail("label '" + name + "' must be a string with visible text");
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"842e1f1956f6213f",requires:["forms"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}=context;const{editParseNumber,EDIT_EDITORS,formRuleSpec,formCheck,FORM_LABELS}=FORMS_IMPL;const FLOW_LABELS={flowSteps:"Steps",flowStepOf:"Step {current} of {count}",flowStepAnnounce:"Step {current} of {count}, {title}",flowNext:"Next",flowBack:"Back",flowSubmit:"Submit",flowSubmitting:"Submitting…",flowReviewTitle:"Check your answers",flowChange:"Change",flowChangeField:"Change {field}",flowNotProvided:"Not provided",flowOptional:"(optional)",flowStepOptional:"Optional",flowStepComplete:"Complete",flowStepCurrent:"Current step",flowStepErrors:"Errors: {count}",flowStepLocked:"Complete the earlier steps first",flowStepNotStarted:"Not started",flowErrorSummary:"Fix these before you continue",flowReady:"Ready to submit",flowNotReady:"Errors to fix: {count}",flowWarnings:"Warnings: {count}",flowChangesOnly:"Show changes only",flowNoChanges:"No answers have changed.",flowWas:"Was: {value}",flowChangedCount:"Changed answers: {count}",flowSecretSet:"Set",flowSecretKept:"Unchanged",flowSecretNotSet:"Not set",flowSecretHint:"A value is saved. Leave blank to keep it.",flowSecretShow:"Show",flowSecretHide:"Hide",flowSecretReveal:"Show {field}",flowOn:"On",flowOff:"Off",flowAdd:"Add",flowAddItem:"Add to {field}",flowRemoveItem:"Remove {item}",flowItemLabel:"{field}, item {index}",flowSelectAll:"Select all",flowSelectNone:"Clear",flowSelectedCount:"{count} of {total} selected",flowUnit:"Unit",flowUnitSeconds:"seconds",flowUnitMinutes:"minutes",flowUnitHours:"hours",flowUnitDays:"days",flowSeconds:"[one]{count} second[other]{count} seconds",flowMinutes:"[one]{count} minute[other]{count} minutes",flowHours:"[one]{count} hour[other]{count} hours",flowDays:"[one]{count} day[other]{count} days",flowDurationInvalid:"{field} must be a whole number.",flowItemsMin:"{field} needs at least {min}.",flowItemsMax:"{field} allows at most {max}.",flowItemEmail:"“{item}” is not an email address.",flowItemHost:"“{item}” is not a host name.",flowItemIp:"“{item}” is not an IP address.",flowItemCidr:"“{item}” is not a network range.",flowItemUrl:"“{item}” is not a web address.",flowItemPattern:"“{item}” does not have the expected format.",flowSaved:"Saved",flowAccepted:"In progress",flowProgress:"{done} of {total}",flowConflict:"These settings changed while you were editing them.",flowReload:"Reload current settings",flowNotSubmitted:"The answers were not submitted.",flowNoHost:"No host received the answers.",flowStartAgain:"Start another",flowCopy:"Copy",flowCopied:"Copied",flowSavedIt:"I have saved it",flowShownOnce:"Shown only once. Copy it now."};function flowLabels(...sources){const labels={...FORM_LABELS,...FLOW_LABELS};for(const source of sources){if(source==null)continue;if(!flowIs(source))flowFail("labels must be an object of strings");for(const[name,text]of Object.entries(source)){if(!Object.hasOwn(labels,name))continue;if(typeof text!=="string"||!/\S/.test(text)) flowFail("label '" + name + "' must be a string with visible text");
|
|
3
2
|
labels[name]=text}}
|
|
4
3
|
return Object.freeze(labels)}
|
|
5
4
|
const FLOW_NAME=/^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
6
|
-
const FLOW_KINDS={text:"text",number:"number",date:"text",datetime:"datetime",time:"text",select:"text",toggle:"text",lookup:"text",choice:"text",multiChoice:"list",textarea:"text",list:"list",secret:"text",duration:"duration",custom:"text"};const FLOW_UNITS={seconds:1,minutes:60,hours:3600,days:86400};const flowFail=problem=>{throw new Error("CanopyX: "+problem+".")};const flowIs=value=>value!==null&&typeof value==="object"&&!Array.isArray(value);const flowEmpty=value=>value==null||value===""||Array.isArray(value)&&!value.length;const flowScalar=value=>typeof value==="string"||typeof value==="boolean"||Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;function flowKeys(object,allowed,what){if(!flowIs(object))flowFail(what+" must be an object");for(const key of Object.keys(object))if(!allowed.includes(key))flowFail(what+" has an unknown property '"+key+"'")}
|
|
5
|
+
const FLOW_KINDS={text:"text",number:"number",date:"text",datetime:"datetime",time:"text",select:"text",toggle:"text",lookup:"text",choice:"text",multiChoice:"list",textarea:"text",list:"list",secret:"text",duration:"duration",custom:"text",records:"records",pick:"pick"};const FLOW_UNITS={seconds:1,minutes:60,hours:3600,days:86400};const flowFail=problem=>{throw new Error("CanopyX: "+problem+".")};const flowIs=value=>value!==null&&typeof value==="object"&&!Array.isArray(value);const flowEmpty=value=>value==null||value===""||Array.isArray(value)&&!value.length;const flowScalar=value=>typeof value==="string"||typeof value==="boolean"||Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;const FLOW_HOST={drafts:0,checks:0,preview:0,command:0,consent:0};const flowEach=(hook,...args)=>{for(const feature of Object.values(FLOW_HOST))feature?.[hook]?.(...args)};const flowHost=name=>FLOW_HOST[name]||flowFail("the flow-"+name+" part did not load");function flowKeys(object,allowed,what){if(!flowIs(object))flowFail(what+" must be an object");for(const key of Object.keys(object))if(!allowed.includes(key))flowFail(what+" has an unknown property '"+key+"'")}
|
|
7
6
|
function flowString(value,max,what,required){if(value===undefined&&!required)return undefined;if(typeof value!=="string"||!/\S/.test(value) || value.length > max) flowFail(what + " needs visible text of at most " + max + " characters");
|
|
8
7
|
return value}
|
|
9
8
|
function flowName(value,what){if(typeof value!=="string"||!FLOW_NAME.test(value))flowFail(what+" must be a letter, then up to 63 letters, digits, - or _");return value}
|
|
@@ -11,22 +10,24 @@ const flowFlag=(value,what)=>{if(value!==undefined&&typeof value!=="boolean")flo
|
|
|
11
10
|
return match?Number(match[1]||0)*86400+Number(match[2]||0)*3600+Number(match[3]||0)*60+Number(match[4]||0):null}
|
|
12
11
|
function flowDuration(seconds){if(!seconds)return "PT0S";const days=Math.floor(seconds / 86400), hours = Math.floor(seconds % 86400 / 3600), minutes = Math.floor(seconds % 3600 / 60), rest = seconds % 60;
|
|
13
12
|
return "P" +(days?days+"D":"")+(hours||minutes||rest?"T" +(hours?hours+"H":"")+(minutes?minutes+"M":"")+(rest?rest+"S":""):"")}
|
|
14
|
-
function flowFits(field,value){if(value===null)return field.type!=="secret";const listed=item=>field.options.some(option=>option.value===item);switch(field.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return typeof value==="string"&&/^\d{4}-\d\d-\d\d$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().startsWith(value);
|
|
13
|
+
function flowFits(field,value){if(value===null)return field.type!=="secret";const listed=item=>field.host?flowScalar(item):field.options.some(option=>option.value===item);switch(field.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return typeof value==="string"&&/^\d{4}-\d\d-\d\d$/.test(value) && Number.isFinite(Date.parse(value + "T00:00:00Z")) && new Date(value + "T00:00:00Z").toISOString().startsWith(value);
|
|
15
14
|
case "time":return typeof value==="string"&&/^([01]\d|2[0-3]):[0-5]\d$/.test(value);
|
|
16
15
|
case "datetime":return typeof value==="string"&&/^\d{4}-\d\d-\d\dT([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{1,7})?)?(Z|[+-](0\d|1[0-3]):[0-5]\d|[+-]14:00)$/.test(value) && flowFits({ type: "date" }, value.slice(0, 10)) && Number.isFinite(Date.parse(value.replace(/(\.\d{3})\d+/, "$1")));
|
|
17
|
-
case "select":case "choice":return listed(value);case "lookup":return listed(value);case "multiChoice":return Array.isArray(value)&&value.every(listed)&&new Set(value).size===value.length;case "list":return Array.isArray(value)&&value.length<=1000&&value.every(item=>typeof item==="string");case "duration":return flowSeconds(value)!==null;case "secret":return false;case "custom":return flowScalar(value)||Array.isArray(value)&&value.length<=1000&&value.every(flowScalar);default:return typeof value==="string"}}
|
|
18
|
-
const FLOW_FIELD_KEYS=["id","type","label","help","placeholder","required","readOnly","default","when","width","parameter","path","sensitive","validation","options","display","onText","offText","rows","step","keepWhenBlank","reveal","units","editor"];const FLOW_RULE_KEYS=["min","max","minLength","maxLength","pattern","message","minItems","maxItems","item","itemPattern"];function flowField(raw,step,section,model){flowKeys(raw,FLOW_FIELD_KEYS,"A flow field");const id=flowName(raw.id,"A field id");if(model.fields.has(id))flowFail("the field id '"+id+"' is used twice");if(model.fields.size>=1000)flowFail("a flow holds at most 1000 fields");if(!Object.hasOwn(FLOW_KINDS,raw.type))flowFail("field "+id+" has an unknown type");const type=raw.type,rules=raw.validation===undefined?{}:raw.validation,bad=problem=>flowFail("field "+id+" "+problem);flowKeys(rules,FLOW_RULE_KEYS,"The validation of field "+id);const field={id,type,kind:FLOW_KINDS[type],step,section,label:flowString(raw.label,200,"Field "+id+"'s label",true),help:flowString(raw.help,1000,"Field "+id+"'s help"),placeholder:flowString(raw.placeholder,256,"Field "+id+"'s placeholder"),required:flowFlag(raw.required,"required"),readOnly:flowFlag(raw.readOnly,"readOnly"),sensitive:flowFlag(raw.sensitive,"sensitive"),width:raw.width??"full",parameter:raw.parameter,path:raw.path,display:raw.display,rows:raw.rows??4,reveal:raw.reveal!==false,onText:flowString(raw.onText,60,"onText"),offText:flowString(raw.offText,60,"offText"),keepWhenBlank:raw.keepWhenBlank,raw};if(!["full","half"].includes(field.width))bad("has an unknown width");if(field.parameter!==undefined&&(typeof field.parameter!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(field.parameter))) bad("has an invalid parameter name");
|
|
16
|
+
case "select":case "choice":return listed(value);case "lookup":return listed(value);case "multiChoice":return Array.isArray(value)&&value.every(listed)&&new Set(value).size===value.length;case "list":return Array.isArray(value)&&value.length<=1000&&value.every(item=>typeof item==="string");case "duration":return flowSeconds(value)!==null;case "secret":return false;case "pick":case "records":return flowHost(field.type).fits(field,value);case "custom":return flowScalar(value)||Array.isArray(value)&&value.length<=1000&&value.every(flowScalar);default:return typeof value==="string"}}
|
|
17
|
+
const FLOW_FIELD_KEYS=["id","type","label","help","placeholder","required","readOnly","default","when","width","parameter","path","sensitive","validation","options","display","onText","offText","rows","step","keepWhenBlank","reveal","units","editor","source","dependsOn","columns"];const FLOW_RULE_KEYS=["min","max","minLength","maxLength","pattern","message","minItems","maxItems","item","itemPattern"];function flowField(raw,step,section,model){flowKeys(raw,FLOW_FIELD_KEYS,"A flow field");const id=flowName(raw.id,"A field id");if(model.fields.has(id))flowFail("the field id '"+id+"' is used twice");if(model.fields.size>=1000)flowFail("a flow holds at most 1000 fields");if(!Object.hasOwn(FLOW_KINDS,raw.type)||(raw.type==="pick")!==(step.type==="pick"))flowFail("field "+id+" has an unknown type");const type=raw.type,rules=raw.validation===undefined?{}:raw.validation,bad=problem=>flowFail("field "+id+" "+problem);flowKeys(rules,FLOW_RULE_KEYS,"The validation of field "+id);const field={id,type,kind:FLOW_KINDS[type],step,section,label:flowString(raw.label,200,"Field "+id+"'s label",true),help:flowString(raw.help,1000,"Field "+id+"'s help"),placeholder:flowString(raw.placeholder,256,"Field "+id+"'s placeholder"),required:flowFlag(raw.required,"required"),readOnly:flowFlag(raw.readOnly,"readOnly"),sensitive:flowFlag(raw.sensitive,"sensitive"),width:raw.width??"full",parameter:raw.parameter,path:raw.path,display:raw.display,rows:raw.rows??4,reveal:raw.reveal!==false,onText:flowString(raw.onText,60,"onText"),offText:flowString(raw.offText,60,"offText"),keepWhenBlank:raw.keepWhenBlank,host:false,dependsOn:[],raw};if(!["full","half"].includes(field.width))bad("has an unknown width");if(field.parameter!==undefined&&(typeof field.parameter!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(field.parameter))) bad("has an invalid parameter name");
|
|
19
18
|
if(field.path!==undefined&&(typeof field.path!=="string"||!/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$/.test(field.path) || field.path.length > 256)) bad("has an invalid path");
|
|
20
19
|
if(raw.keepWhenBlank!==undefined&&(type!=="secret"||typeof raw.keepWhenBlank!=="boolean"))bad("can keep a blank value only as a secret");if(raw.reveal!==undefined&&(type!=="secret"||typeof raw.reveal!=="boolean"))bad("has a reveal setting but is not a secret");if((raw.onText!==undefined||raw.offText!==undefined)&&type!=="toggle")bad("names on and off texts but is not a toggle");if(raw.rows!==undefined&&(type!=="textarea"||!Number.isInteger(raw.rows)||raw.rows<2||raw.rows>20))bad("needs 2 to 20 rows, as a text area only");if(raw.display!==undefined&&(type!=="choice"||!["cards","list"].includes(raw.display)))bad("has an unknown display");if(raw.editor!==undefined?type!=="custom"||typeof raw.editor!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(raw.editor) : type === "custom") bad("needs an editor name as a custom field only");
|
|
21
|
-
if(raw.step!==undefined&&type!=="number")bad("has a step but is not a number");if(raw.placeholder!==undefined&&!["text","number","lookup","textarea","list"].includes(type))bad("has a placeholder, which text, number, lookup, text area and list fields take");const choosing=["select","choice","multiChoice","lookup"].includes(type);if(raw.options!==undefined&&!choosing)bad("has options but does not choose");if(choosing&&!(Array.isArray(raw.options)&&raw.options.length))bad("needs options");if(Array.isArray(raw.options)
|
|
20
|
+
if(raw.step!==undefined&&type!=="number")bad("has a step but is not a number");if(raw.placeholder!==undefined&&!["text","number","lookup","textarea","list"].includes(type))bad("has a placeholder, which text, number, lookup, text area and list fields take");const choosing=["select","choice","multiChoice","lookup"].includes(type);if(raw.source!==undefined&&(raw.source!=="document"||!choosing)||raw.dependsOn!==undefined)flowHost("options").field(field,raw,choosing,bad);if(raw.options!==undefined&&!choosing)bad("has options but does not choose");if(choosing&&!field.host&&!(Array.isArray(raw.options)&&raw.options.length))bad("needs options");if(raw.options!==undefined&&!Array.isArray(raw.options))bad("needs a list of options");field.options=flowOptions(raw.options||[],bad);if((raw.columns!==undefined)!==(type==="records"))bad("needs columns as a records field only");if(type==="records")field.columns=flowHost("records").columns(raw.columns,bad);const list=type==="list"||type==="multiChoice"||type==="records"||type==="pick";if(list&&[rules.min,rules.max,rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined))bad("checks items, not lengths or ranges");if(!list&&[rules.minItems,rules.maxItems].some(value=>value!==undefined)||type!=="list"&&(rules.item!==undefined||rules.itemPattern!==undefined))bad("has rules only lists take");if((rules.min!==undefined||rules.max!==undefined)&&!["number","datetime","duration"].includes(type))bad("has a range but is not a number, date and time or duration");if([rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&!["text","textarea","secret"].includes(type))bad("has lengths or a pattern, which text, text area and secret fields take");if(rules.minLength>rules.maxLength)bad("needs the shorter length first");const count=value=>value===undefined||Number.isInteger(value)&&value>=0&&value<=1000;if(!count(rules.minItems)||!count(rules.maxItems)||rules.minItems>rules.maxItems)bad("needs 0 to 1000 items, the fewest first");if(rules.item!==undefined&&!["email","host","ip","cidr","url"].includes(rules.item))bad("has an unknown item rule");let itemPattern=null;if(rules.itemPattern!==undefined)try{itemPattern=boundedPattern(rules.itemPattern,true,true)}catch(error){bad("has an invalid or unsafe item pattern")}
|
|
22
21
|
const range=type==="duration"?{min:rules.min===undefined?undefined:flowSeconds(rules.min),max:rules.max===undefined?undefined:flowSeconds(rules.max)}:rules;if(type==="duration"&&(range.min===null||range.max===null))bad("needs ISO 8601 durations as its range");if(type==="number"&&![rules.min,rules.max].every(bound=>bound===undefined||Number.isFinite(bound)&&Math.abs(bound)<=Number.MAX_SAFE_INTEGER))bad("needs numbers as its range");if(type==="datetime"&&![rules.min,rules.max].every(bound=>bound===undefined||flowFits({type:"datetime"},bound)))bad("needs ISO 8601 moments as its range");const low=type==="datetime"&&typeof range.min==="string"?Date.parse(range.min):range.min,high=type==="datetime"&&typeof range.max==="string"?Date.parse(range.max):range.max;if(low!==undefined&&high!==undefined&&low>high)bad("needs the lower limit first");const shared=formRuleSpec({step:raw.step,placeholder:raw.placeholder},{required:field.required,min:range.min,max:range.max,minLength:rules.minLength,maxLength:rules.maxLength,pattern:rules.pattern,message:rules.message},bad);field.spec=Object.freeze({id,pattern:shared.pattern,rules:shared.rules});field.items=Object.freeze({min:rules.minItems,max:rules.maxItems,rule:rules.item,pattern:itemPattern});const units=raw.units??["minutes","hours","days"];if(raw.units!==undefined&&type!=="duration"||!Array.isArray(units)||!units.length||units.some(unit=>!Object.hasOwn(FLOW_UNITS,unit))||new Set(units).size!==units.length)bad("has invalid duration units");field.units=Object.keys(FLOW_UNITS).filter(unit=>units.includes(unit));if(raw.default!==undefined&&(type==="secret"||list&&!Array.isArray(raw.default)||!flowFits(field,raw.default)))bad("has a default that does not fit it");field.start=raw.default===undefined?(list?[]:null):raw.default;model.fields.set(id,field);return field}
|
|
23
|
-
function flowRead(doc){flowKeys(doc,["kind","version","id","title","description","mode","layout","density","timeZone","steps","initial","storedSecrets","baseline","submit","labels"],"A flow document");if(doc.kind!=="flow"||doc.version!==1)flowFail("a flow document needs kind \"flow\" and version 1");const model={id:flowName(doc.id,"The flow id"),title:flowString(doc.title,200,"The flow title",true),description:flowString(doc.description,1000,"The flow description"),mode:doc.mode??"linear",density:doc.density??"normal",local:doc.timeZone==="local",fields:new Map(),steps:[],sections:new Set()};if(!["linear","free"].includes(model.mode)||!["compact","normal","comfortable"].includes(model.density)||doc.timeZone!==undefined&&!["utc","local"].includes(doc.timeZone))flowFail("the flow's mode, density or time zone is unknown");if(doc.layout!==undefined)flowKeys(doc.layout,["orientation"],"The flow layout");model.orientation=doc.layout?.orientation??"auto";if(!["auto","horizontal","vertical"].includes(model.orientation))flowFail("the flow layout has an unknown orientation");if(!Array.isArray(doc.steps)||!doc.steps.length||doc.steps.length>64)flowFail("a flow needs 1 to 64 steps");for(const[index,raw]of doc.steps.entries()){flowKeys(raw,["id","title","description","type","fields","sections","when","optional","declaration","changesOnly"],"A flow step");const step={id:flowName(raw.id,"A step id"),title:flowString(raw.title,200,"A step title",raw.type!=="review"),description:flowString(raw.description,1000,"A step description"),type:raw.type??"form",optional:flowFlag(raw.optional,"optional"),declaration:flowString(raw.declaration,1000,"The declaration"),changesOnly:raw.changesOnly!==false,blocks:[],raw};if(model.steps.some(other=>other.id===step.id))flowFail("the step id '"+step.id+"' is used twice");if(!["form","info","review"].includes(step.type))flowFail("step "+step.id+" has an unknown type");if(step.type==="review"&&index!==doc.steps.length-1)flowFail("the review step must be the last step");if(step.type==="review"&&(raw.when!==undefined||raw.optional===true))flowFail("the review step takes no condition and is never optional");if(step.type!=="review"&&
|
|
24
|
-
model.
|
|
22
|
+
function flowRead(doc){flowKeys(doc,["kind","version","id","title","description","mode","layout","density","timeZone","steps","initial","storedSecrets","baseline","submit","labels","command","presets","draft","deepLinks","endpoint"],"A flow document");if(doc.kind!=="flow"||doc.version!==1)flowFail("a flow document needs kind \"flow\" and version 1");const model={id:flowName(doc.id,"The flow id"),title:flowString(doc.title,200,"The flow title",true),description:flowString(doc.description,1000,"The flow description"),mode:doc.mode??"linear",density:doc.density??"normal",local:doc.timeZone==="local",fields:new Map(),steps:[],sections:new Set(),checks:new Map()};if(!["linear","free","tasks"].includes(model.mode)||!["compact","normal","comfortable"].includes(model.density)||doc.timeZone!==undefined&&!["utc","local"].includes(doc.timeZone))flowFail("the flow's mode, density or time zone is unknown");if(doc.layout!==undefined)flowKeys(doc.layout,["orientation","container","launch"],"The flow layout");model.orientation=doc.layout?.orientation??"auto";if(!["auto","horizontal","vertical"].includes(model.orientation))flowFail("the flow layout has an unknown orientation");if(!Array.isArray(doc.steps)||!doc.steps.length||doc.steps.length>64)flowFail("a flow needs 1 to 64 steps");for(const[index,raw]of doc.steps.entries()){flowKeys(raw,["id","title","description","type","fields","sections","when","optional","declaration","changesOnly","checks","group","pick","confirm","acknowledge","preview"],"A flow step");const step={id:flowName(raw.id,"A step id"),title:flowString(raw.title,200,"A step title",raw.type!=="review"),description:flowString(raw.description,1000,"A step description"),type:raw.type??"form",optional:flowFlag(raw.optional,"optional"),declaration:flowString(raw.declaration,1000,"The declaration"),changesOnly:raw.changesOnly!==false,group:flowString(raw.group,200,"A step group"),blocks:[],checks:[],raw};if(model.steps.some(other=>other.id===step.id))flowFail("the step id '"+step.id+"' is used twice");if(!["form","info","review","pick"].includes(step.type))flowFail("step "+step.id+" has an unknown type");step.asks=step.type==="form"||step.type==="pick";if(step.type==="review"&&index!==doc.steps.length-1)flowFail("the review step must be the last step");if(step.type==="review"&&(raw.when!==undefined||raw.optional===true||raw.group!==undefined))flowFail("the review step takes no condition or group and is never optional");const reviewKey=["declaration","changesOnly","confirm","acknowledge","preview"].find(key=>raw[key]!==undefined);if(step.type!=="review"&&reviewKey)flowFail("only a review step has "+reviewKey);if(raw.changesOnly!==undefined&&typeof raw.changesOnly!=="boolean")flowFail("changesOnly must be true or false");if(step.type!=="form"&&(raw.fields!==undefined||raw.sections!==undefined))flowFail("only a form step has fields and sections");if((raw.pick!==undefined)!==(step.type==="pick"))flowFail("step "+step.id+" has a pick only as a pick step, and a pick step needs one");if(raw.fields!==undefined&&!Array.isArray(raw.fields)||raw.sections!==undefined&&!Array.isArray(raw.sections))flowFail("step "+step.id+" needs arrays of fields and sections");if(raw.fields?.length)step.blocks.push({section:null,fields:raw.fields.map(field=>flowField(field,step,null,model))});if((raw.sections||[]).length>64)flowFail("step "+step.id+" holds at most 64 sections");for(const rawSection of raw.sections||[]){flowKeys(rawSection,["id","label","description","fields","when","source","dependsOn"],"A flow section");const section={id:flowName(rawSection.id,"A section id"),label:flowString(rawSection.label,200,"A section label",true),description:flowString(rawSection.description,1000,"A section description"),host:false,dependsOn:[],raw:rawSection};if(model.sections.has(section.id))flowFail("the section id '"+section.id+"' is used twice");model.sections.add(section.id);if(rawSection.source!==undefined||rawSection.dependsOn!==undefined)flowHost("options").section(section,rawSection);else if(!Array.isArray(rawSection.fields))flowFail("section "+section.id+" needs an array of fields");step.blocks.push({section,fields:(rawSection.fields||[]).map(field=>flowField(field,step,section,model))})}
|
|
23
|
+
if(step.type==="pick")flowHost("pick").read(raw.pick,step,model);if(step.type==="review"){step.preview=flowFlag(raw.preview,"preview");if(raw.confirm!==undefined||raw.acknowledge!==undefined)flowHost("consent").read(raw,step)}
|
|
24
|
+
if(raw.checks!==undefined)flowHost("checks").read(raw.checks,step,model);model.steps.push(step)}
|
|
25
25
|
const paths=new Set();for(const path of[...model.fields.values()].map(field=>field.path).filter(Boolean)){if(paths.has(path))flowFail("the path '"+path+"' is used twice");paths.add(path)}
|
|
26
|
-
for(const path of paths)for(let at=path.indexOf(".");at>0;at=path.indexOf(".",at+1))if(paths.has(path.slice(0,at)))flowFail("the path '"+path+"' lies inside '"+path.slice(0,at)+ "'");flowConditions(model);flowEditMode(doc,model);if(doc.submit!==undefined)flowKeys(doc.submit,["label"],"The flow's submit");model.submitLabel=flowString(doc.submit?.label,80,"The submit label");model.labels=doc.labels;return model}
|
|
26
|
+
for(const path of paths)for(let at=path.indexOf(".");at>0;at=path.indexOf(".",at+1))if(paths.has(path.slice(0,at)))flowFail("the path '"+path+"' lies inside '"+path.slice(0,at)+ "'");flowConditions(model);flowEach("finish",model);flowEditMode(doc,model);Object.assign(model,{container:"inline",command:null,presets:[],draft:"none",deepLinks:false,endpoint:null});flowEach("document",doc,model);if(doc.submit!==undefined)flowKeys(doc.submit,["label"],"The flow's submit");model.submitLabel=flowString(doc.submit?.label,80,"The submit label");model.labels=doc.labels;return model}
|
|
27
27
|
function flowEditMode(doc,model){if(doc.initial!==undefined){if(!flowIs(doc.initial))flowFail("initial must be an object of field values");for(const[id,value]of Object.entries(doc.initial)){const field=model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))flowFail("initial has no value for '"+id+"' that fits a field")}}
|
|
28
28
|
model.initial=doc.initial?{...doc.initial}:null;if(doc.storedSecrets!==undefined&&(!Array.isArray(doc.storedSecrets)||doc.storedSecrets.some(id=>model.fields.get(id)?.type!=="secret")||new Set(doc.storedSecrets).size!==doc.storedSecrets.length))
|
|
29
29
|
flowFail("storedSecrets lists only secret fields");model.stored=new Set(doc.storedSecrets||[]);if(doc.baseline!==undefined&&(typeof doc.baseline!=="string"||!doc.baseline.length||doc.baseline.length>256))flowFail("the baseline is 1 to 256 characters");model.baseline=doc.baseline??null;if(!model.initial&&(model.stored.size||model.baseline!==null))model.initial={}}
|
|
30
|
+
function flowOptions(raw,bad){if(raw.length>1000)bad("needs at most 1000 options");const options=raw.map(option=>{flowKeys(flowIs(option)?option:{value:option},["value","label","description","badge","disabledReason","group"],"An option");const item=flowIs(option)?option:{value:option};if(!flowScalar(item.value))bad("has an invalid option value");return Object.freeze({value:item.value,label:flowString(item.label,200,"An option label")??String(item.value),description:flowString(item.description,1000,"An option description"),badge:flowString(item.badge,40,"An option badge"),disabledReason:flowString(item.disabledReason,400,"A disabled reason"),group:flowString(item.group,200,"An option group")})});if(new Set(options.map(option=>option.value)).size!==options.length)bad("needs options with distinct values");return options}
|
|
30
31
|
const FLOW_ARITY={eq:1,ne:1,gt:1,ge:1,lt:1,le:1,contains:1,notContains:1,startsWith:1,endsWith:1,matches:1,notMatches:1,in:-1,notIn:-1,between:2,isNull:0,isNotNull:0};function flowCondition(raw,model,counts,depth=1){flowKeys(raw,["operator","filters","groups"],"A condition");if(depth>8||++counts.groups>64)flowFail("a condition holds at most eight levels and 64 groups");const filters=raw.filters??[],groups=raw.groups??[];if(!["and","or","none"].includes(raw.operator)||!Array.isArray(filters)||!Array.isArray(groups)||!filters.length&&!groups.length)
|
|
31
32
|
flowFail("a condition group needs and, or or none and at least one predicate or group");return Object.freeze({operator:raw.operator,groups:groups.map(group=>flowCondition(group,model,counts,depth+1)),filters:filters.map(filter=>{flowKeys(filter,["field","operator","values"],"A condition predicate");if(++counts.filters>64)flowFail("a condition holds at most 64 predicates");if(!model.fields.has(filter.field))flowFail("a condition names the unknown field '"+filter.field+"'");if(model.fields.get(filter.field).type==="secret")flowFail("a condition cannot read the secret field '"+filter.field+"'");const arity=FLOW_ARITY[filter.operator],values=filter.values??[];if(arity===undefined||!Array.isArray(values)||(arity<0?values.length<1||values.length>1000:values.length!==arity)||!values.every(flowScalar))
|
|
32
33
|
flowFail("a condition on '"+filter.field+"' has an unknown operator or the wrong values");const safe=value=>typeof value!=="number"||Math.abs(value)<=Number.MAX_SAFE_INTEGER,op=filter.operator;if(!values.every(value=>safe(value)&&(typeof value!=="string"||value.length<=4096))||
|
|
@@ -51,12 +52,12 @@ hint.id=id+"-hint";hint.hidden=true;message.id=id+"-error";message.hidden=true;c
|
|
|
51
52
|
return instance};fv.mount=value=>{try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}
|
|
52
53
|
fv.instance=create(value);fv.pending=null;const control=fv.control=fv.instance.control||fv.instance.element;if(fv.instance.group){control.setAttribute("aria-labelledby",label.id);label.removeAttribute("for")}
|
|
53
54
|
else{control.id=id;if(label.tagName==="LABEL")label.htmlFor=id}
|
|
54
|
-
if(field.type==="toggle")control.setAttribute("role","switch");if(fv.instance.hint){setText(hint,fv.instance.hint);hint.hidden=false}else hint.hidden=true;fv.describe();const holder=wrapper.querySelector(".cx-flow-control");holder.replaceChildren(fv.instance.element);if(field.type==="toggle")holder.appendChild(label)};fv.describe=()=>{const target=fv.instance.describes||fv.control;const ids=[help&&help.id,!hint.hidden&&hint.id,!message.hidden&&message.id].filter(Boolean);if(ids.length)target.setAttribute("aria-describedby",ids.join(" "));else target.removeAttribute("aria-describedby");if(message.hidden)target.removeAttribute("aria-invalid");else target.setAttribute("aria-invalid","true")};fv.read=()=>{const parse=fv.instance.error?.()||null;if(parse)return{error:parse};let value=fv.instance.getValue();if(value===""||value===undefined)value=null;if(field.type!=="secret"&&!flowFits(field,value))return{error:fill(view.labels.gridEditPattern,{column:field.label})};return{value}};fv.message=text=>{setText(message,text||"");message.hidden=!text;fv.describe();wrapper.classList.toggle("cx-flow-invalid",!!text)};fv.focus=()=>fv.instance.focus?fv.instance.focus():fv.control.focus({preventScroll:false});fv.destroy=()=>{fv.abort?.abort();try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}};const holder=h("div","cx-flow-control" +(field.type==="toggle"?" cx-flow-toggle":""));if(field.type==="toggle")wrapper.append(holder);else wrapper.append(label);if(help)wrapper.appendChild(help);wrapper.append(hint);if(field.type!=="toggle")wrapper.append(holder);wrapper.append(message);return fv}
|
|
55
|
+
if(field.type==="toggle")control.setAttribute("role","switch");if(fv.instance.hint){setText(hint,fv.instance.hint);hint.hidden=false}else hint.hidden=true;fv.describe();const holder=wrapper.querySelector(".cx-flow-control");holder.replaceChildren(fv.instance.element);if(field.type==="toggle")holder.appendChild(label)};fv.describe=()=>{const target=fv.instance.describes||fv.control;const ids=[help&&help.id,!hint.hidden&&hint.id,fv.note&&!fv.note.hidden&&fv.note.id,!message.hidden&&message.id].filter(Boolean);if(ids.length)target.setAttribute("aria-describedby",ids.join(" "));else target.removeAttribute("aria-describedby");if(message.hidden)target.removeAttribute("aria-invalid");else target.setAttribute("aria-invalid","true")};fv.read=()=>{const parse=fv.instance.error?.()||null;if(parse)return{error:parse};let value=fv.instance.getValue();if(value===""||value===undefined)value=null;if(field.type!=="secret"&&!flowFits(field,value))return{error:fill(view.labels.gridEditPattern,{column:field.label})};return{value}};fv.message=text=>{setText(message,text||"");message.hidden=!text;fv.describe();wrapper.classList.toggle("cx-flow-invalid",!!text)};fv.focus=()=>fv.instance.focus?fv.instance.focus():fv.control.focus({preventScroll:false});fv.destroy=()=>{fv.abort?.abort();try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}};const holder=h("div","cx-flow-control" +(field.type==="toggle"?" cx-flow-toggle":""));if(field.type==="toggle")wrapper.append(holder);else wrapper.append(label);if(help)wrapper.appendChild(help);wrapper.append(hint);if(field.type!=="toggle")wrapper.append(holder);wrapper.append(message);return fv}
|
|
55
56
|
function flowEditorContext(view,field,id,value,signal){const kind=field.kind==="number"||field.kind==="datetime"?field.kind:"text";return Object.freeze({id,value,text:value==null?"":flowDisplay(view,field,value),readOnly:field.readOnly,row:Object.freeze({id:view.model.id,cells:Object.freeze(view.getValues())}),column:Object.freeze({id:field.id,title:field.label,kind}),editor:Object.freeze({type:field.type,placeholder:field.placeholder,step:field.raw.step}),options:field.options.length?field.options:null,rules:field.spec.rules,labels:view.labels,locale:view.lang,timeZone:view.model.local?"local":"utc",mode:"flow",initial:undefined,signal,lookupMinLength:1,commit:()=>view.changed(field,"edit"),cancel:()=>{},lookup:search=>{const folded=foldText(search.trim());return field.options.filter(option=>!option.disabledReason&&foldText(option.label).includes(folded)).slice(0,20)},remember:()=>queueMicrotask(()=>view.changed(field,"edit"))})}
|
|
56
|
-
function flowEditor(view,field,context){switch(field.type){case "choice":return flowChoiceInput(context,field);case "multiChoice":return flowMultiInput(context,field,view.labels);case "textarea":return flowTextAreaInput(context,field);case "list":return flowListInput(context,field,view.labels);case "secret":return flowSecretInput(context,field,view.labels,view.keeps(field));case "duration":return flowDurationInput(context,field,view.labels);case "custom":{const editor=view.editors[field.raw.editor];if(!editor||typeof editor.create!=="function")flowFail("the field "+field.id+" names the editor '"+field.raw.editor+"', which nobody registered");return editor.create(context)}
|
|
57
|
+
function flowEditor(view,field,context){switch(field.type){case "choice":return flowChoiceInput(context,field);case "multiChoice":return flowMultiInput(context,field,view.labels);case "textarea":return flowTextAreaInput(context,field);case "list":return flowListInput(context,field,view.labels);case "secret":return flowSecretInput(context,field,view.labels,view.keeps(field));case "duration":return flowDurationInput(context,field,view.labels);case "records":case "pick":return flowHost(field.type).input(context,field,view);case "custom":{const editor=view.editors[field.raw.editor];if(!editor||typeof editor.create!=="function")flowFail("the field "+field.id+" names the editor '"+field.raw.editor+"', which nobody registered");return editor.create(context)}
|
|
57
58
|
default:{const instance=EDIT_EDITORS[field.type].create(context),control=instance.control||instance.element;control.classList.add("cx-flow-input");if(field.readOnly){control.disabled=control.tagName==="SELECT"||control.type==="checkbox";control.readOnly=true}
|
|
58
59
|
return instance}}}
|
|
59
|
-
function flowDisplay(view,field,value){const labels=view.labels,option=item=>field.options.find(entry=>entry.value===item)?.label??String(item);if(flowEmpty(value))return labels.flowNotProvided;switch(field.type){case "number":return numberFormat(view.lang,0,20).format(value);case "toggle":return value?field.onText||labels.flowOn:field.offText||labels.flowOff;case "select":case "choice":case "lookup":return option(value);case "multiChoice":return value.map(option).join(", ");case "list":return value.join("\n");case "date":return flowDate(view,value+"T00:00:00Z",{dateStyle:"medium",timeZone:"UTC"});case "datetime":return flowDate(view,value,{dateStyle:"medium",timeStyle:"short",timeZone:view.model.local?undefined:"UTC"})+(view.model.local?"":" UTC");case "duration":return flowWords(view,flowSeconds(value),field.units[0]);default:return String(value)}}
|
|
60
|
+
function flowDisplay(view,field,value){const labels=view.labels,option=item=>field.options.find(entry=>entry.value===item)?.label??String(item);if(flowEmpty(value))return labels.flowNotProvided;switch(field.type){case "number":return numberFormat(view.lang,0,20).format(value);case "toggle":return value?field.onText||labels.flowOn:field.offText||labels.flowOff;case "select":case "choice":case "lookup":return option(value);case "multiChoice":return value.map(option).join(", ");case "list":return value.join("\n");case "pick":case "records":return flowHost(field.type).display(view,value);case "date":return flowDate(view,value+"T00:00:00Z",{dateStyle:"medium",timeZone:"UTC"});case "datetime":return flowDate(view,value,{dateStyle:"medium",timeStyle:"short",timeZone:view.model.local?undefined:"UTC"})+(view.model.local?"":" UTC");case "duration":return flowWords(view,flowSeconds(value),field.units[0]);default:return String(value)}}
|
|
60
61
|
function flowDate(view,iso,options){try{return new Intl.DateTimeFormat(view.lang||undefined,options).format(new Date(iso))}catch(error){return iso}}
|
|
61
62
|
function flowWords(view,seconds,smallest="seconds"){const unit=["days","hours","minutes","seconds"].find(name=>seconds%FLOW_UNITS[name]===0&&seconds>=FLOW_UNITS[name])||(seconds?"seconds":smallest),count=seconds / FLOW_UNITS[unit];
|
|
62
63
|
const label=view.labels["flow"+unit[0].toUpperCase()+ unit.slice(1)];return fill(plural(label,count,view.lang),{count:numberFormat(view.lang,0,0).format(count)})}
|
|
@@ -80,11 +81,11 @@ function flowDurationInput(context,field,labels){const group=h("div","cx-flow-du
|
|
|
80
81
|
group.append(amount,unit);const read=()=>amount.value.trim()===""?null:editParseNumber(amount.value,context.locale);return{element:group,control:group,group:true,amount,describes:amount,focus:()=>amount.focus(),error:()=>{const value=read(),seconds=value*FLOW_UNITS[unit.value];return value!==null&&!(Number.isSafeInteger(value)&&value>=0&&Number.isSafeInteger(seconds)&&flowSeconds(flowDuration(seconds))!==null)?fill(labels.flowDurationInvalid,{field:field.label}):null},getValue:()=>{const value=read();return value===null?null:flowDuration(value*FLOW_UNITS[unit.value])}}}
|
|
81
82
|
let FLOW_SEQUENCE=0;const flowCopy=value=>value===undefined?undefined:JSON.parse(JSON.stringify(value));const flowSameValue=(left,right)=>JSON.stringify(left??null)===JSON.stringify(right??null);const FLOW_HOOKS=["validateStep","validateFlow","submit"],FLOW_CALLBACKS=["onReady","onStepChange","onValueChange","onSubmit","onSubmitted","onReload"];class FlowView{constructor(host,options,editors){const hooks=options.hooks??{};if(!flowIs(hooks)||FLOW_HOOKS.some(name=>hooks[name]!=null&&typeof hooks[name]!=="function")||hooks.validators!=null&&!flowIs(hooks.validators)||
|
|
82
83
|
FLOW_CALLBACKS.some(name=>options[name]!=null&&typeof options[name]!=="function")||options.editors!=null&&!flowIs(options.editors)||
|
|
83
|
-
options.values!=null&&!flowIs(options.values))flowFail("invalid flow options");this.host=host;this.options=options;this.hooks=hooks
|
|
84
|
+
options.values!=null&&!flowIs(options.values))flowFail("invalid flow options");this.host=host;this.options=options;this.model=flowRead(options.flow);this.hooks=this.model.endpoint?flowHost("endpoint").hooks(this.model,hooks,options.headers||{}):hooks;this.labels=flowLabels(this.model.labels,options.labels);this.editors=Object.assign({},editors,options.editors);this.id="cx-flow-" + ++FLOW_SEQUENCE;this.lang=host.closest("[lang]")?.lang||document.documentElement.lang||"";this.lifetime=new AbortController();this.fields=new Map();this.sections=new Map();this.visibility=new Map();this.secrets=new Map();this.errors=new Map();this.hostErrors=new Map();this.hostStepErrors=new Map();this.stepErrors=new Map();this.completed=new Set();this.banner=null;this.phase="form";this.returnToReview=false;this.generation=0;this.answerRevision=0;this.hostState?.();try{this.begin(options.values);this.build()}
|
|
84
85
|
catch(error){this.destroy();throw error}}
|
|
85
|
-
destroy(){if(this.destroyed)return;this.destroyed=true;this.retireOperation();this.lifetime.abort();this.resize?.disconnect();for(const fv of this.fields.values())fv.destroy();this.secrets.clear();this.root?.remove();this.host.removeAttribute("data-cx-ready")}
|
|
86
|
+
destroy(){if(this.destroyed)return;this.destroyed=true;this.retireOperation();this.lifetime.abort();this.resize?.disconnect();for(const fv of this.fields.values())fv.destroy();this.releaseHostWork?.();this.secrets.clear();this.root?.remove();this.host.removeAttribute("data-cx-ready")}
|
|
86
87
|
begin(values){const model=this.model;this.values=new Map();for(const field of model.fields.values()){if(field.type==="secret")continue;const initial=model.initial?Object.hasOwn(model.initial,field.id)?model.initial[field.id]:null:field.start;this.values.set(field.id,flowCopy(initial))}
|
|
87
|
-
for(const[id,value]of Object.entries(values||{}))this.assign(id,value);this.startValues=new Map([...this.values].map(([id,value])=>[id,flowCopy(value)]));this.secrets.clear();this.errors.clear();this.hostErrors.clear();this.hostStepErrors.clear();this.stepErrors.clear();this.showAll=undefined;this.reviewStale=true;this.completed.clear();this.visibility.clear();this.banner=null;this.returnToReview=false;this.step=this.visibleSteps()[0];if(model.initial)for(const step of this.visibleSteps())if(step.
|
|
88
|
+
for(const[id,value]of Object.entries(values||{}))this.assign(id,value);this.startValues=new Map([...this.values].map(([id,value])=>[id,flowCopy(value)]));this.secrets.clear();this.errors.clear();this.hostErrors.clear();this.hostStepErrors.clear();this.stepErrors.clear();this.showAll=undefined;this.reviewStale=true;this.completed.clear();this.visibility.clear();this.banner=null;this.returnToReview=false;this.step=this.hub||this.visibleSteps()[0];if(model.initial)for(const step of this.visibleSteps())if(step.asks&&this.stepFields(step).every(field=>!this.problemNow(field)))this.completed.add(step.id)}
|
|
88
89
|
assign(id,value){const field=this.model.fields.get(id);if(!field||field.type==="secret"||!flowFits(field,value))flowFail("no field '"+id+"' takes that value");if(!flowSameValue(this.values.get(id),value)||this.fields.get(id)?.pending){this.answersChanged();this.forgetHost(field)}
|
|
89
90
|
this.values.set(id,flowCopy(value))}
|
|
90
91
|
keeps(field){return field.type==="secret"&&this.model.stored.has(field.id)&&field.keepWhenBlank!==false}
|
|
@@ -102,7 +103,7 @@ changed(field,source){const fv=this.fields.get(field.id);if(!fv||this.destroyed|
|
|
|
102
103
|
if(source!=="blur"&&this.errors.has(field.id))this.checkField(field);this.refreshVisibility()}
|
|
103
104
|
forgetHost(field){this.completed.delete(field.step.id);const affected=new Set([field.id]);let changed;do{changed=false;for(const other of this.model.fields.values())if(!affected.has(other.id)&&[...other.refs].some(id=>affected.has(id))){affected.add(other.id);this.completed.delete(other.step.id);changed=true}}while(changed);if(this.hostStepErrors.delete(field.step.id)){const summary=this.sections.get(field.step.id)?._cxSummary;if(summary)summary.hidden=true}
|
|
104
105
|
this.hostErrors.delete(field.id);this.reviewStale=true}
|
|
105
|
-
refreshVisibility(){this.visibility.clear();for(const fv of this.fields.values())fv.element.hidden=!this.visible(fv.field);for(const[stepId,section]of this.sections)for(const block of section._cxBlocks||[])block.element.hidden=!block.fields.some(field=>this.visible(field));if(!this.root)return;if(this.visibleSteps().map(step=>step.id).join()+(this.orientation()==="stack"?"|stack":"")!==this.railKey){this.railTooWide=false;this.render()}
|
|
106
|
+
refreshVisibility(){this.visibility.clear();for(const fv of this.fields.values())fv.element.hidden=!this.visible(fv.field);for(const[stepId,section]of this.sections)for(const block of section._cxBlocks||[])block.element.hidden=!block.fields.some(field=>this.visible(field));this.prepareVisible?.();if(!this.root)return;if(this.visibleSteps().map(step=>step.id).join()+(this.orientation()==="stack"?"|stack":"")!==this.railKey){this.railTooWide=false;this.render()}
|
|
106
107
|
else this.renderRail()}
|
|
107
108
|
setValues(partial){if(!flowIs(partial))flowFail("setValues takes an object of field values");for(const[id,value]of Object.entries(partial)){const previous=this.values.get(id)??null;this.assign(id,value);this.fields.get(id)?.mount(flowCopy(value));this.errors.delete(id);this.hostErrors.delete(id);this.fields.get(id)?.message(null);if(!flowSameValue(previous,value))this.emit("flowValueChanged",{field:id,value:flowCopy(value),previous:flowCopy(previous),source:"api"},"onValueChange")}
|
|
108
109
|
this.refreshVisibility();this.refreshReview()}
|
|
@@ -111,46 +112,47 @@ isDirty(){return this.secrets.size>0||[...this.values].some(([id,value])=>!flowS
|
|
|
111
112
|
getStep(){return this.step.id}
|
|
112
113
|
getState(){return{step:this.step.id,values:Object.fromEntries([...this.values].map(([id,value])=>[id,flowCopy(value)])),completed:[...this.completed]}}
|
|
113
114
|
setState(state){if(!flowIs(state)||state.values!=null&&!flowIs(state.values)||state.completed!=null&&!Array.isArray(state.completed))flowFail("setState takes a flow state");for(const[id,value]of Object.entries(state.values||{})){this.assign(id,value);this.fields.get(id)?.mount(flowCopy(value));this.errors.delete(id);this.hostErrors.delete(id);this.fields.get(id)?.message(null)}
|
|
114
|
-
this.completed=new Set((state.completed||[]).filter(id=>this.model.steps.some(step=>step.id===id)));this.refreshVisibility();const target=this.model.steps.find(step=>step.id===state.step);return this.go(target&&this.stepVisible(target)?target:this.visibleSteps()[0],"jump",true)}
|
|
115
|
-
reset(){this.generation++;this.answersChanged();this.busy=false;this.checks?.clear();this.reviewTicket=null;this.reviewChecking=false;for(const fv of this.fields.values())fv.destroy();this.fields.clear();this.sections.clear();this.begin(this.options.values);this.phase="form";this.render();return true}
|
|
116
|
-
emit(name,detail,callback,cancelable=false){const event=new CustomEvent("canopyx:"+name,{detail,bubbles:true,cancelable});
|
|
115
|
+
this.completed=new Set((state.completed||[]).filter(id=>this.model.steps.some(step=>step.id===id)));this.refreshVisibility();const target=this.hub&&state.step===this.hub.id?this.hub:this.model.steps.find(step=>step.id===state.step);return this.go(target&&this.stepVisible(target)?target:this.hub||this.visibleSteps()[0],"jump",true)}
|
|
116
|
+
reset(){this.generation++;this.answersChanged();this.busy=false;this.checks?.clear();this.reviewTicket=null;this.reviewChecking=false;for(const fv of this.fields.values())fv.destroy();this.fields.clear();this.sections.clear();this.releaseHostWork?.();this.begin(this.options.values);this.phase="form";this.render();return true}
|
|
117
|
+
emit(name,detail,callback,cancelable=false){const event=new CustomEvent("canopyx:"+name,{detail,bubbles:true,cancelable});let go=this.host.dispatchEvent(event);if(callback&&typeof this.options[callback]==="function")try{if(this.options[callback](detail)===false&&cancelable)go=false}catch(error){this.report(error,callback)}
|
|
117
118
|
return go}
|
|
118
119
|
report(error,hook){this.host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error,hook},bubbles:true}))}
|
|
119
120
|
async hook(name,run){try{return{value:await run()}}
|
|
120
121
|
catch(error){this.report(error,name);return{failed:error&&error.message?error.message:String(error)}}}}
|
|
121
122
|
const FLOW_NARROW=640;Object.assign(FlowView.prototype,{build(){const root=this.root=h("div","cx-flow"),head=h("header","cx-flow-head"),title=h("h2","cx-flow-title");root.dataset.cxDensity=this.model.density;setText(title,this.model.title);title.id=this.id+"-title";head.appendChild(title);if(this.model.description){const description=h("p","cx-flow-desc");setText(description,this.model.description);head.appendChild(description)}
|
|
122
|
-
root.setAttribute("aria-labelledby",title.id);root.setAttribute("role","group");this.layout=h("div","cx-flow-layout");this.rail=h("div","cx-flow-rail");this.railList=h("ol","cx-flow-marks");this.rail.appendChild(this.railList);this.main=h("div","cx-flow-main");this.stage=h("div","cx-flow-stage");this.actions=h("div","cx-flow-actions");this.main.append(this.stage,this.actions);this.live=h("div","cx-sr-only");this.live.setAttribute("aria-live","polite");this.layout.append(this.rail,this.main);root.append(head,this.layout,this.live);this.host.appendChild(root);const signal=this.lifetime.signal;root.addEventListener("input",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("change",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("focusout",event=>this.fieldEvent(event,"blur"),{signal});root.addEventListener("keydown",event=>this.key(event),{signal});this.narrow=false;if(global.ResizeObserver){this.resize=new ResizeObserver(()=>this.measure());this.resize.observe(this.
|
|
123
|
-
this.measure(true);this.render()},fieldEvent(event,source){const wrapper=event.target.closest?.("[data-cx-field]");const field=wrapper&&this.model.fields.get(wrapper.dataset.cxField);if(!field||this.phase!=="form")return;if(source==="blur"){if(wrapper.contains(event.relatedTarget))return;this.changed(field,"blur");this.checkField(field);return}
|
|
124
|
-
this.changed(field,source)},measure(first){const width=this.
|
|
125
|
-
else if(!first)this.fitRail()},orientation(){if(this.visibleSteps().length<=1)return "single";if(this.narrow)return "stack";return this.model.orientation==="auto"?(this.visibleSteps().length>6||this.railTooWide?"vertical":"horizontal"):this.model.orientation},fitRail(){if(this.model.orientation!=="auto"||this.railTooWide||this.orientation()!=="horizontal"||!this.rail.clientWidth)return;if(this.rail.scrollWidth>this.rail.clientWidth+1){this.railTooWide=true;this.render()}},render(){const active=document.activeElement;const steps=this.visibleSteps();if(this.phase==="form"&&!steps.includes(this.step)){const from=this.step;this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(from))||steps[steps.length-1];this.reviewStale=true;this.emit("flowStepChanged",{from:from.id,to:this.step.id},"onStepChange")}
|
|
126
|
-
const orientation=this.orientation();this.layout.className="cx-flow-layout cx-flow-"+orientation;this.root.classList.toggle("cx-flow-narrow",this.narrow);if(orientation==="
|
|
127
|
-
else{this.rail.
|
|
128
|
-
this.renderStage();this.renderRail();this.renderActions();if(active&&active!==document.activeElement&&active.isConnected&&this.root.contains(active))active.focus({preventScroll:true});this.fitRail()},stepState(step,steps){if(this.phase!=="form")return "complete";if(step===this.step)return "current";const errors=this.stepErrorCount(step);if(errors)return "error";if(this.completed.has(step.id))return "complete";return this.reachable(step,steps)?"incomplete":"locked"},reachable(step,steps=this.visibleSteps()){if(this.model.mode==="free")return true;for(const before of steps){if(before===step)return true;if(before.
|
|
123
|
+
root.setAttribute("aria-labelledby",title.id);root.setAttribute("role","group");this.layout=h("div","cx-flow-layout");this.rail=h("div","cx-flow-rail");this.railList=h("ol","cx-flow-marks");this.rail.appendChild(this.railList);this.main=h("div","cx-flow-main");this.stage=h("div","cx-flow-stage");this.actions=h("div","cx-flow-actions");this.main.append(this.stage,this.actions);this.live=h("div","cx-sr-only");this.live.setAttribute("aria-live","polite");this.layout.append(this.rail,this.main);root.append(head,this.layout,this.live);this.head=head;if(this.model.container!=="inline")this.buildContainer();else this.host.appendChild(root);this.sizer=this.dialog||this.host;const signal=this.lifetime.signal;root.addEventListener("input",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("change",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("focusout",event=>this.fieldEvent(event,"blur"),{signal});root.addEventListener("keydown",event=>this.key(event),{signal});this.narrow=false;if(global.ResizeObserver){this.resize=new ResizeObserver(()=>this.measure());this.resize.observe(this.sizer)}
|
|
124
|
+
this.buildPresets?.();this.measure(true);this.render();this.startHostFeatures?.()},fieldEvent(event,source){const wrapper=event.target.closest?.("[data-cx-field]");const field=wrapper&&this.model.fields.get(wrapper.dataset.cxField);if(!field||this.phase!=="form")return;if(source==="blur"){if(wrapper.contains(event.relatedTarget))return;this.changed(field,"blur");this.checkField(field);return}
|
|
125
|
+
this.changed(field,source)},measure(first){const width=this.sizer.clientWidth||global.innerWidth,narrow=width<FLOW_NARROW;if(!first&&width===this.width)return;const changed=narrow!==this.narrow||this.railTooWide&&width>this.width;this.width=width;this.narrow=narrow;if(changed){this.railTooWide=false;if(!first)this.render()}
|
|
126
|
+
else if(!first)this.fitRail()},orientation(){if(this.visibleSteps().length<=1)return "single";if(this.narrow)return "stack";return this.model.orientation==="auto"?(this.visibleSteps().length>6||this.railTooWide?"vertical":"horizontal"):this.model.orientation},fitRail(){if(this.model.orientation!=="auto"||this.railTooWide||this.orientation()!=="horizontal"||!this.rail.clientWidth)return;if(this.rail.scrollWidth>this.rail.clientWidth+1){this.railTooWide=true;this.render()}},render(){const active=document.activeElement;const steps=this.visibleSteps();if(this.phase==="form"&&this.step!==this.hub&&!steps.includes(this.step)){const from=this.step;this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(from))||steps[steps.length-1];this.reviewStale=true;this.emit("flowStepChanged",{from:from.id,to:this.step.id},"onStepChange")}
|
|
127
|
+
const orientation=this.orientation();this.layout.className="cx-flow-layout cx-flow-"+orientation;this.root.classList.toggle("cx-flow-narrow",this.narrow);if(orientation==="horizontal"||orientation==="vertical"){this.rail.setAttribute("role","navigation");this.rail.setAttribute("aria-label",this.labels.flowSteps)}
|
|
128
|
+
else{this.rail.removeAttribute("role");this.rail.removeAttribute("aria-label")}
|
|
129
|
+
this.renderStage();this.renderRail();this.renderActions();if(active&&active!==document.activeElement&&active.isConnected&&this.root.contains(active))active.focus({preventScroll:true});this.fitRail()},stepState(step,steps){if(this.phase!=="form")return "complete";if(step===this.step)return "current";const errors=this.stepErrorCount(step);if(errors)return "error";if(this.completed.has(step.id))return "complete";return this.reachable(step,steps)?"incomplete":"locked"},reachable(step,steps=this.visibleSteps()){if(this.model.mode==="free")return true;for(const before of steps){if(before===step)return true;if(before.asks&&!this.completed.has(before.id))return false}
|
|
129
130
|
return false},renderRail(){if(!this.root)return;const steps=this.visibleSteps(),stack=this.orientation()==="stack",labels=this.labels;if(!steps.includes(this.step))this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(this.step))||steps[steps.length-1];const key=steps.map(step=>step.id).join()+(stack?"|stack":"");if(key!==this.railKey){this.railKey=key;this.railItems=new Map();this.railList.replaceChildren(...steps.map(step=>{const item=h("li","cx-flow-mark"),button=h("button","cx-flow-mark-btn");button.type="button";button.addEventListener("click",()=>{if(button.getAttribute("aria-disabled")!=="true"&&step!==this.step)this.goTo(step.id).catch(error=>this.report(error,"goTo"))});item.appendChild(button);this.railItems.set(step.id,item);return item}))}
|
|
130
131
|
steps.forEach((step,index)=>{const state=this.stepState(step,steps),item=this.railItems.get(step.id),button=item.firstChild,number=h("span","cx-flow-num");const text=h("span","cx-flow-mark-text"),title=h("span","cx-flow-mark-title"),note=h("span","cx-flow-mark-state");item.dataset.cxState=state;setText(number,state==="complete"?"✓":state==="error"?"!":String(index+1));number.setAttribute("aria-hidden","true");setText(title,step.title||labels.flowReviewTitle);const errors=this.stepErrorCount(step);setText(note,state==="current"?labels.flowStepCurrent:state==="error"?fill(labels.flowStepErrors,{count:errors}):
|
|
131
132
|
state==="complete"?labels.flowStepComplete:state==="locked"?labels.flowStepLocked:step.optional?labels.flowStepOptional:labels.flowStepNotStarted);note.className="cx-flow-mark-state" +(state==="locked"||state==="incomplete"?" cx-sr-only":"");text.append(title,note);const signature=state+"\u0000"+number.textContent+"\u0000"+note.textContent+"\u0000"+title.textContent;if(button._cxSignature!==signature){button._cxSignature=signature;button.replaceChildren(number,text)}
|
|
132
|
-
if(state==="current")button.setAttribute("aria-current","step");else button.removeAttribute("aria-current");if(state==="locked"||this.phase!=="form")button.setAttribute("aria-disabled","true");else button.removeAttribute("aria-disabled");if(stack&&step===this.step&&this.phase==="form"&&this.main.parentNode!==item)item.appendChild(this.main)});if(!stack&&this.main.parentNode!==this.layout)this.layout.appendChild(this.main);if(stack&&this.phase!=="form")this.railList.after(this.main)},renderActions(){const labels=this.labels,
|
|
133
|
-
const spacer=h("span","cx-flow-spacer");this.actions.append(back,spacer,next)},key(event){if(event.defaultPrevented||event.isComposing||this.phase!=="form")return;const target=event.target;if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)&&this.step.type==="review"){event.preventDefault();this.submit().catch(error=>this.report(error,"submit"));return}
|
|
133
|
+
if(state==="current")button.setAttribute("aria-current","step");else button.removeAttribute("aria-current");if(state==="locked"||this.phase!=="form")button.setAttribute("aria-disabled","true");else button.removeAttribute("aria-disabled");if(stack&&step===this.step&&this.phase==="form"&&this.main.parentNode!==item)item.appendChild(this.main)});if(!stack&&this.main.parentNode!==this.layout)this.layout.appendChild(this.main);if(stack&&this.phase!=="form")this.railList.after(this.main)},lastStep(){const steps=this.visibleSteps();return steps.indexOf(this.step)===steps.length-1},renderActions(){const labels=this.labels,at=this.visibleSteps().indexOf(this.step),last=this.lastStep();this.actions.replaceChildren();this.actions.hidden=this.phase!=="form";if(this.phase!=="form")return;const back=h("button","cx-flow-btn cx-flow-back"),next=h("button","cx-flow-btn cx-primary cx-flow-next");back.type=next.type="button";setText(back,labels.flowBack);back.hidden=at<=0;back.addEventListener("click",()=>this.back().catch(error=>this.report(error,"back")));setText(next,last?this.model.submitLabel||labels.flowSubmit:labels.flowNext);next.addEventListener("click",()=>(last?this.submit():this.next()).catch(error=>this.report(error,last?"submit":"next")));if(this.busy){next.disabled=back.disabled=true;next.setAttribute("aria-busy","true");if(last)setText(next,labels.flowSubmitting)}
|
|
134
|
+
const spacer=h("span","cx-flow-spacer");this.actions.append(back,...[this.draftButton?.(),this.cancelButton?.()].filter(Boolean),spacer,next)},key(event){if(event.defaultPrevented||event.isComposing||this.phase!=="form")return;const target=event.target;if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)&&this.step.type==="review"){event.preventDefault();this.submit().catch(error=>this.report(error,"submit"));return}
|
|
134
135
|
if(event.key!=="Enter"||event.altKey||event.ctrlKey||event.shiftKey||target.tagName!=="INPUT"||/^(button|submit|checkbox|radio|file)$/.test(target.type) ||
|
|
135
|
-
target.closest(".cx-flow-list")||!target.closest(".cx-flow-stage"))return;event.preventDefault();
|
|
136
|
+
target.closest(".cx-flow-list,.cx-flow-records,.cx-flow-pick,.cx-flow-confirm")||!target.closest(".cx-flow-stage"))return;event.preventDefault();(this.lastStep()?this.submit():this.next()).catch(error=>this.report(error,"next"))},announce(text){setText(this.live,"");setTimeout(()=>setText(this.live,text),30)}});Object.assign(FlowView.prototype,{renderStage(){const step=this.step;if(this.phase==="result"||this.phase==="progress"){this.stage.replaceChildren(this.panel);return}
|
|
136
137
|
if(step.type==="review"){if(this.reviewStale||!this.reviewNode){this.reviewNode=this.reviewSection();this.reviewStale=false}
|
|
137
138
|
if(this.stage.firstChild!==this.reviewNode)this.stage.replaceChildren(this.reviewNode);return}
|
|
138
139
|
let section=this.sections.get(step.id);if(!section){section=step.type==="info"?this.stepShell(step):this.formSection(step);this.sections.set(step.id,section)}
|
|
139
140
|
section.querySelector(":scope > .cx-flow-answer-banner")?.remove();if(this.banner&&step===this.visibleSteps().at(-1))section._cxSummary.after(this.answerBanner());if(this.stage.childNodes.length!==1||this.stage.firstChild!==section)this.stage.replaceChildren(section)},stepShell(step){const section=h("section","cx-flow-step"),heading=h("h3","cx-flow-step-title");heading.id=this.id+"-s-"+step.id;heading.tabIndex=-1;setText(heading,step.title||this.labels.flowReviewTitle);section.setAttribute("aria-labelledby",heading.id);section.appendChild(heading);if(step.description){const description=h("p","cx-flow-step-desc");setText(description,step.description);section.appendChild(description)}
|
|
140
141
|
const summary=h("div","cx-flow-summary");summary.setAttribute("role","alert");summary.tabIndex=-1;summary.hidden=true;section.appendChild(summary);section._cxHeading=heading;section._cxSummary=summary;return section},formSection(step){const section=this.stepShell(step);section._cxBlocks=[];for(const block of step.blocks){const element=h("div",block.section?"cx-flow-section":"cx-flow-block"),grid=h("div","cx-flow-grid");if(block.section){const title=h("h4","cx-flow-section-title");title.id=this.id+"-x-"+block.section.id;setText(title,block.section.label);element.setAttribute("role","group");element.setAttribute("aria-labelledby",title.id);element.appendChild(title);if(block.section.description){const description=h("p","cx-flow-section-desc");setText(description,block.section.description);element.appendChild(description)}}
|
|
141
142
|
for(const field of block.fields){const fv=flowFieldView(this,field);this.fields.set(field.id,fv);fv.mount(field.type==="secret"?null:flowCopy(this.values.get(field.id)??null));fv.element.hidden=!this.visible(field);if(this.errors.has(field.id))fv.message(this.errors.get(field.id));grid.appendChild(fv.element)}
|
|
142
|
-
element.appendChild(grid);element.hidden=!block.fields.some(field=>this.visible(field));section._cxBlocks.push({element,fields:block.fields});section.appendChild(element)}
|
|
143
|
-
return section},async go(target,direction,quiet){if(!target||this.destroyed)return false;const from=this.step,generation=this.generation;if(target!==from&&!this.emit("flowStepChanging",{from:from.id,to:target.id,direction},null,true))return false;if(this.destroyed||this.generation!==generation||this.step!==from)return false;for(const fv of this.fields.values())fv.instance?.conceal?.();this.step=target;this.reviewStale=true;for(const field of target.blocks?.flatMap(block=>block.fields)||[])this.fields.get(field.id)?.message(this.errors.get(field.id)??null);this.render();const steps=this.visibleSteps(),heading=this.stage.querySelector(".cx-flow-step-title");if(!quiet){heading?.focus({preventScroll:true});this.root.scrollIntoView?.({block:"nearest"});this.announce(fill(this.labels.flowStepAnnounce,{current:steps.indexOf(target)+ 1,count:steps.length,title:target.title||this.labels.flowReviewTitle}))}
|
|
144
|
-
if(target!==from)this.emit("flowStepChanged",{from:from.id,to:target.id},"onStepChange");
|
|
143
|
+
element.appendChild(grid);element.hidden=!block.section?.host&&!block.fields.some(field=>this.visible(field));section._cxBlocks.push({element,fields:block.fields,section:block.section});section.appendChild(element)}
|
|
144
|
+
flowEach("built",this,step,section);return section},async go(target,direction,quiet){if(!target||this.destroyed)return false;const from=this.step,generation=this.generation;if(target!==from&&!this.emit("flowStepChanging",{from:from.id,to:target.id,direction},null,true))return false;if(this.destroyed||this.generation!==generation||this.step!==from)return false;for(const fv of this.fields.values())fv.instance?.conceal?.();this.step=target;this.reviewStale=true;for(const field of target.blocks?.flatMap(block=>block.fields)||[])this.fields.get(field.id)?.message(this.errors.get(field.id)??null);this.render();const steps=this.visibleSteps(),heading=this.stage.querySelector(".cx-flow-step-title");if(!quiet){(target===this.hub&&this.taskButton(from)||heading)?.focus({preventScroll:true});this.root.scrollIntoView?.({block:"nearest"});this.announce(target===this.hub?this.hubAnnouncement():fill(this.labels.flowStepAnnounce,{current:steps.indexOf(target)+ 1,count:steps.length,title:target.title||this.labels.flowReviewTitle}))}
|
|
145
|
+
if(target!==from){this.emit("flowStepChanged",{from:from.id,to:target.id},"onStepChange");this.stepMoved?.()}
|
|
146
|
+
return true},async next(){if(this.busy||this.phase!=="form"||this.step===this.hub)return false;const step=this.step,steps=this.visibleSteps(),pressed=document.activeElement,generation=this.generation;if(step.asks){const ticket=this.beginOperation();this.renderActions();let result;try{result=await ticket.wait(this.validateStep(step,true,ticket.current));if(!ticket.current())return false}finally{if(ticket.current())this.retireOperation()}
|
|
145
147
|
if(this.destroyed||generation!==this.generation||this.step!==step||result.moved)return false;if(!result.valid){const stayed=document.activeElement===pressed||!this.root.contains(document.activeElement);this.renderRail();this.renderActions();this.showSummary(step,stayed);return false}
|
|
146
|
-
const summary=this.sections.get(step.id)?._cxSummary;if(summary)summary.hidden=true;this.completed.add(step.id)}else this.completed.add(step.id);const review=this.reviewStep();const target=this.returnToReview&&review&&this.stepVisible(review)?review:steps[steps.indexOf(step)+ 1];if(target===review)this.returnToReview=false;return this.go(target,"forward")},async back(){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),at=steps.indexOf(this.step);return at>0?this.go(steps[at-1],"back"):false},async goTo(id){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),target=steps.find(step=>step.id===id);if(!target)return false;const open=this.reachable(target,steps)?target:steps.find(step=>step.
|
|
148
|
+
const summary=this.sections.get(step.id)?._cxSummary;if(summary)summary.hidden=true;this.completed.add(step.id)}else this.completed.add(step.id);const review=this.reviewStep();const target=this.returnToReview&&review&&this.stepVisible(review)?review:this.hub||steps[steps.indexOf(step)+ 1];if(target===review)this.returnToReview=false;return this.go(target,"forward")},async back(){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),at=steps.indexOf(this.step);return at>0?this.go(steps[at-1],"back"):false},async goTo(id){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),target=steps.find(step=>step.id===id);if(!target)return false;const open=this.reachable(target,steps)?target:steps.find(step=>step.asks&&!this.completed.has(step.id))||target;return this.go(open,steps.indexOf(open)<steps.indexOf(this.step)?"back":"forward")},change(field){this.returnToReview=true;return this.go(field.step,"back").then(moved=>{if(moved){const fv=this.fields.get(field.id);if(fv)fv.focus()}
|
|
147
149
|
return moved})}});const FLOW_ITEM_RULES={email:value=>/^[^\s@]+@[^\s@]+\.[^\s@.]+$/.test(value),
|
|
148
150
|
host:value=>value.length<=253&&/^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.?$/.test(value),
|
|
149
151
|
ip:value=>flowIPv4(value)||flowIPv6(value),cidr:value=>{const[address,prefix,extra]=value.split("/");return extra===undefined&&/^\d{1,3}$/.test(prefix || "") && (flowIPv4(address) && Number(prefix) <= 32 || flowIPv6(address) && Number(prefix) <= 128); },
|
|
150
152
|
url:value=>{try{const url=new URL(value);return url.protocol==="https:"||url.protocol==="http:"}catch(error){return false}}};function flowIPv4(value){return /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(value); }
|
|
151
153
|
function flowIPv6(value){if(!/^[0-9A-Fa-f:.]{2,45}$/.test(value) || !value.includes(":")) return false; try { return new URL("http://[" + value + "]/").hostname.length > 2; } catch (error) { return false; } }
|
|
152
154
|
Object.assign(FlowView.prototype,{problemNow(field){const fv=this.fields.get(field.id),labels=this.labels,title=field.label;if(fv?.pending)return fv.pending;if(field.type==="secret"){const value=this.secrets.get(field.id)??null;if(value===null)return field.required&&!this.keeps(field)?fill(labels.gridEditRequired,{column:title}):null;return formCheck(field.spec,value,"text",title,labels,String)}
|
|
153
|
-
const value=this.values.get(field.id)??null;if(field.type==="toggle")return field.required&&value!==true?fill(labels.gridEditRequired,{column:title}):null;if(field.kind==="list"){const items=value||[],rules=field.items;if(!items.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(rules.min!==undefined&&items.length<rules.min)return fill(labels.flowItemsMin,{field:title,min:rules.min});if(rules.max!==undefined&&items.length>rules.max)return fill(labels.flowItemsMax,{field:title,max:rules.max});const name=rules.rule&&"flowItem"+rules.rule[0].toUpperCase()+ rules.rule.slice(1);for(const item of items){if(rules.rule&&!FLOW_ITEM_RULES[rules.rule](item))return field.spec.rules.message||fill(labels[name],{item});if(rules.pattern&&!rules.pattern.test(item))return field.spec.rules.message||fill(labels.flowItemPattern,{item})}
|
|
155
|
+
const value=this.values.get(field.id)??null;if(field.type==="toggle")return field.required&&value!==true?fill(labels.gridEditRequired,{column:title}):null;if(field.kind==="records")return flowHost("records").problem(this,field,value||[]);if(field.kind==="list"||field.kind==="pick"){const items=value||[],rules=field.items;if(!items.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(rules.min!==undefined&&items.length<rules.min)return fill(labels.flowItemsMin,{field:title,min:rules.min});if(rules.max!==undefined&&items.length>rules.max)return fill(labels.flowItemsMax,{field:title,max:rules.max});const name=rules.rule&&"flowItem"+rules.rule[0].toUpperCase()+ rules.rule.slice(1);for(const item of items){if(rules.rule&&!FLOW_ITEM_RULES[rules.rule](item))return field.spec.rules.message||fill(labels[name],{item});if(rules.pattern&&!rules.pattern.test(item))return field.spec.rules.message||fill(labels.flowItemPattern,{item})}
|
|
154
156
|
return null}
|
|
155
157
|
if(field.kind==="duration")return formCheck(field.spec,value===null?null:flowSeconds(value),"number",title,labels,seconds=>flowWords(this,seconds));const shown=bound=>field.type==="datetime"?flowDisplay(this,field,bound):typeof bound==="number"?numberFormat(this.lang,0,20).format(bound):String(bound);return formCheck(field.spec,value,field.kind==="number"||field.kind==="datetime"?field.kind:"text",title,labels,shown)},async problem(field){const own=this.problemNow(field);if(own||field.type==="secret")return own;const validator=this.hooks.validators&&Object.hasOwn(this.hooks.validators,field.id)?this.hooks.validators[field.id]:null;if(typeof validator!=="function")return null;const answer=await this.hook("validators."+field.id,()=>validator(flowCopy(this.values.get(field.id)??null),this.getValues()));return answer.failed||(typeof answer.value==="string"&&/\S/.test(answer.value) ? answer.value : null);
|
|
156
158
|
},async checkField(field){const revision=this.answerRevision,generation=this.generation;const ticket=(this.checks||=new Map()).set(field.id,{}).get(field.id),message=await this.problem(field);if(this.checks.get(field.id)!==ticket||this.destroyed||revision!==this.answerRevision||generation!==this.generation)return message;this.setError(field,message||this.hostErrors.get(field.id)||null);this.renderRail();return message},setError(field,message){if(message)this.errors.set(field.id,message);else this.errors.delete(field.id);const fv=this.fields.get(field.id);fv?.message(message);if(field.type==="list"&&fv?.instance.flag)fv.instance.flag(item=>!!message&&!!item&&this.itemFails(field,item))},itemFails(field,item){const rules=field.items;return!!rules.rule&&!FLOW_ITEM_RULES[rules.rule](item)||!!rules.pattern&&!rules.pattern.test(item)},answerKey(step){return JSON.stringify([this.answerRevision,this.stepFields(step).map(field=>field.type==="secret"?this.secrets.has(field.id):[this.values.get(field.id)??null,this.fields.get(field.id)?.pending||null])])},stepErrorCount(step){return this.stepFields(step).filter(field=>this.errors.has(field.id)).length +(this.stepErrors.has(step.id)||this.hostStepErrors.has(step.id)?1:0)},bucket(answer){const result={errors:[],warnings:[],fields:Object.create(null)},text=value=>typeof value==="string"&&/\S/.test(value);
|
|
@@ -158,19 +160,20 @@ if(answer.failed){result.errors.push(answer.failed);return result}
|
|
|
158
160
|
const value=answer.value;if(text(value))result.errors.push(value);else if(flowIs(value)){for(const[name,key]of[["errors","errors"],["warnings","warnings"]])if(Array.isArray(value[name]))result[key].push(...value[name].filter(text));if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))result.fields[id]=message}
|
|
159
161
|
return result},async validateStep(step,show,current=()=>true){const generation=this.generation;let fields,messages,result,moved=false,stable=false;for(let round=0;round<4;round++,moved=true){const before=this.answerKey(step);fields=this.stepFields(step);messages=await Promise.all(fields.map(field=>this.problem(field)));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true};result={errors:[],warnings:[],fields:Object.create(null)};if(!messages.some(Boolean)&&typeof this.hooks.validateStep==="function")
|
|
160
162
|
result=this.bucket(await this.hook("validateStep",()=>this.hooks.validateStep(step.id,this.getValues())));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true};if(this.answerKey(step)===before){stable=true;break}}
|
|
161
|
-
if(this.destroyed||!current())return{valid:false,errors:[],warnings:[]};if(!stable)return{valid:false,errors:[{step:step.id,message:this.labels.flowNotSubmitted}],warnings:[],moved:true};const failing=[];fields.forEach((field,index)=>{const local=messages[index]||result.fields[field.id]||null,message=local||this.hostErrors.get(field.id)||null;if(show||!message)this.setError(field,message);else this.errors.set(field.id,message);if(local)failing.push({field:field.id,step:step.id,message:local})});if(
|
|
162
|
-
return{valid:
|
|
163
|
+
if(this.destroyed||!current())return{valid:false,errors:[],warnings:[]};if(!stable)return{valid:false,errors:[{step:step.id,message:this.labels.flowNotSubmitted}],warnings:[],moved:true};const failing=[];fields.forEach((field,index)=>{const local=messages[index]||result.fields[field.id]||null,message=local||this.hostErrors.get(field.id)||null;if(show||!message)this.setError(field,message);else this.errors.set(field.id,message);if(local)failing.push({field:field.id,step:step.id,message:local})});if(step.checks.length){result.errors.push(...await this.checkGate(step,show&&!failing.length&&!result.errors.length));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true}}
|
|
164
|
+
if(result.errors.length)this.stepErrors.set(step.id,result.errors.join(" "));else this.stepErrors.delete(step.id);const errors=failing;for(const message of result.errors)errors.push({step:step.id,message});return{valid:stable&&!errors.length,errors,warnings:result.warnings.map(message=>({step:step.id,message})),moved}},async validateAll(current=()=>true){const generation=this.generation;for(let round=0;round<4;round++){const revision=this.answerRevision,result=await this.validateAllOnce(generation,current);if(this.destroyed||generation!==this.generation||!current())return{valid:false,errors:[],warnings:[]};if(revision===this.answerRevision)return result}
|
|
165
|
+
return{valid:false,errors:[{message:this.labels.flowNotSubmitted}],warnings:[]}},async validateAllOnce(generation,current){const revision=this.answerRevision,results=[],steps=this.visibleSteps().filter(step=>step.asks);for(const step of steps){results.push(await this.validateStep(step,false,current));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]}}
|
|
163
166
|
if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]};const errors=results.flatMap(result=>result.errors),warnings=results.flatMap(result=>result.warnings);steps.forEach((step,index)=>{if(results[index].valid)this.completed.add(step.id);else this.completed.delete(step.id)});if(!errors.length&&typeof this.hooks.validateFlow==="function"){const result=this.bucket(await this.hook("validateFlow",()=>this.hooks.validateFlow(this.getValues())));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]};for(const[id,message]of Object.entries(result.fields))if(this.visible(this.model.fields.get(id))){this.setError(this.model.fields.get(id),message);errors.push({field:id,step:this.model.fields.get(id).step.id,message})}
|
|
164
167
|
errors.push(...result.errors.map(message=>({message})));warnings.push(...result.warnings.map(message=>({message})))}
|
|
165
168
|
return{valid:results.every(result=>result.valid)&&!errors.length,errors,warnings}},async validate(stepId){if(stepId===undefined){const result=await this.validateAll();this.renderRail();return result}
|
|
166
|
-
const step=this.model.steps.find(item=>item.id===stepId);if(!step)flowFail("there is no step '"+stepId+"'");const result=step.
|
|
169
|
+
const step=this.model.steps.find(item=>item.id===stepId);if(!step)flowFail("there is no step '"+stepId+"'");const result=step.asks&&this.stepVisible(step)?await this.validateStep(step,true):{valid:true,errors:[],warnings:[]};this.renderRail();return{valid:result.valid,errors:result.errors,warnings:result.warnings}},setFieldError(id,message){const field=this.model.fields.get(id);if(!field||message!=null&&(typeof message!=="string"||!/\S/.test(message))) flowFail("setFieldError takes a field id and a message or null");
|
|
167
170
|
if(message)this.hostErrors.set(id,message);else this.hostErrors.delete(id);this.setError(field,message??null);this.renderRail();this.refreshReview()},showSummary(step,focus=true){const section=this.sections.get(step.id),summary=section?._cxSummary;if(!summary)return;const list=h("ul"),title=h("p","cx-flow-summary-title");setText(title,this.labels.flowErrorSummary);for(const field of this.stepFields(step))if(this.errors.has(field.id)){const item=h("li"),link=h("button","cx-flow-link");link.type="button";setText(link,this.errors.get(field.id));link.addEventListener("click",()=>this.fields.get(field.id)?.focus());item.appendChild(link);list.appendChild(item)}
|
|
168
171
|
const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(stepMessage){const item=h("li");setText(item,stepMessage);list.appendChild(item)}
|
|
169
172
|
summary.replaceChildren(title,list);summary.hidden=!list.children.length;if(!summary.hidden&&focus)summary.focus({preventScroll:false})}});const flowNormal=value=>flowEmpty(value)?null:value;Object.assign(FlowView.prototype,{initialOf(field){return this.model.initial&&Object.hasOwn(this.model.initial,field.id)?this.model.initial[field.id]:null},isChanged(field){if(field.type==="secret")return this.secrets.has(field.id);return!flowSameValue(flowNormal(this.values.get(field.id)),flowNormal(this.initialOf(field)))},secretText(field){return this.secrets.has(field.id)?this.labels.flowSecretSet:this.keeps(field)?this.labels.flowSecretKept:this.labels.flowSecretNotSet},reviewSection(){const step=this.step,labels=this.labels,section=this.stepShell(step),edit=!!this.model.initial;section.classList.add("cx-flow-review");const status=h("div","cx-flow-banner");status.setAttribute("role","status");section.appendChild(status);if(this.banner)section.appendChild(this.answerBanner());let changesOnly=edit&&(this.showAll===undefined?step.changesOnly:!this.showAll);const lists=h("div","cx-flow-review-lists"),tools=h("div","cx-flow-review-tools");if(edit){const toggle=h("label","cx-flow-switch"),box=h("input"),text=h("span"),count=h("span","cx-flow-count");box.type="checkbox";box.checked=changesOnly;box.setAttribute("role","switch");setText(text,labels.flowChangesOnly);box.addEventListener("change",()=>{this.showAll=!box.checked;changesOnly=box.checked;this.fillReview(lists,changesOnly)});toggle.append(box,text);const changed=[...this.model.fields.values()].filter(field=>this.visible(field)&&this.isChanged(field)).length;setText(count,fill(labels.flowChangedCount,{count:changed}));tools.append(toggle,count);section.appendChild(tools)}
|
|
170
|
-
section.appendChild(lists);this.fillReview(lists,changesOnly);if(step.declaration){const declaration=h("p","cx-flow-declaration");setText(declaration,step.declaration);section.appendChild(declaration)}
|
|
171
|
-
const ticket=this.reviewTicket={};setText(status,"…");if(this.banner)status.hidden=true;this.reviewChecking=true;this.validateAll().finally(()=>{if(this.reviewTicket===ticket)this.reviewChecking=false}).then(result=>{if(this.reviewTicket!==ticket||this.destroyed)return;this.reviewResult=result;this.fillBanner(status,result);status.hidden=!!this.banner&&result.valid;this.fillReview(lists,changesOnly);this.renderRail()},error=>this.report(error,"validate"));return section},fillBanner(status,result){const labels=this.labels;status.replaceChildren();status.className="cx-flow-banner " +(result.valid?"cx-flow-ready":"cx-flow-problems");const title=h("p","cx-flow-banner-title");setText(title,result.valid?labels.flowReady:fill(labels.flowNotReady,{count:result.errors.length}));status.appendChild(title);const list=h("ul","cx-flow-banner-list");for(const error of result.errors){const item=h("li"),step=this.model.steps.find(entry=>entry.id===error.step),field=error.field&&this.model.fields.get(error.field);if(step){const link=h("button","cx-flow-link");link.type="button";setText(link,(step.title?step.title+": ":"")+ error.message);link.addEventListener("click",()=>(field?this.change(field):(this.returnToReview=true,this.goTo(step.id))).catch(failure=>this.report(failure,"goTo")));item.appendChild(link)}else setText(item,error.message);list.appendChild(item)}
|
|
173
|
+
section.appendChild(lists);this.fillReview(lists,changesOnly);flowEach("review",this,section);if(step.declaration){const declaration=h("p","cx-flow-declaration");setText(declaration,step.declaration);section.appendChild(declaration)}
|
|
174
|
+
flowEach("confirm",this,section,step);const recheck=this.reviewRecheck=()=>{const ticket=this.reviewTicket={};setText(status,"…");if(this.banner)status.hidden=true;this.reviewChecking=true;return this.validateAll().finally(()=>{if(this.reviewTicket===ticket)this.reviewChecking=false}).then(result=>{if(this.reviewTicket!==ticket||this.destroyed)return;this.reviewResult=result;this.fillBanner(status,result);status.hidden=!!this.banner&&result.valid;this.fillReview(lists,changesOnly);this.renderRail()},error=>this.report(error,"validate"))};recheck();return section},fillBanner(status,result){const labels=this.labels;status.replaceChildren();status.className="cx-flow-banner " +(result.valid?"cx-flow-ready":"cx-flow-problems");const title=h("p","cx-flow-banner-title");setText(title,result.valid?labels.flowReady:fill(labels.flowNotReady,{count:result.errors.length}));status.appendChild(title);const list=h("ul","cx-flow-banner-list");for(const error of result.errors){const item=h("li"),step=this.model.steps.find(entry=>entry.id===error.step),field=error.field&&this.model.fields.get(error.field);if(step){const link=h("button","cx-flow-link");link.type="button";setText(link,(step.title?step.title+": ":"")+ error.message);link.addEventListener("click",()=>(field?this.change(field):(this.returnToReview=true,this.goTo(step.id))).catch(failure=>this.report(failure,"goTo")));item.appendChild(link)}else setText(item,error.message);list.appendChild(item)}
|
|
172
175
|
if(result.warnings.length){const warnings=h("p","cx-flow-banner-warn");setText(warnings,fill(labels.flowWarnings,{count:result.warnings.length}));status.appendChild(warnings);for(const warning of result.warnings){const item=h("li","cx-flow-warning");setText(item,warning.message);list.appendChild(item)}}
|
|
173
|
-
if(list.children.length)status.appendChild(list)},fillReview(lists,changesOnly){const labels=this.labels;lists.replaceChildren();let shown=0;for(const step of this.visibleSteps()){if(step.
|
|
176
|
+
if(list.children.length)status.appendChild(list)},fillReview(lists,changesOnly){const labels=this.labels;lists.replaceChildren();let shown=0;for(const step of this.visibleSteps()){if(!step.asks)continue;const fields=this.stepFields(step).filter(field=>!changesOnly||this.isChanged(field));const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(!fields.length&&!stepMessage)continue;const group=h("section","cx-flow-review-group"),heading=h("h4","cx-flow-review-title"),list=h("dl","cx-flow-answers");heading.id=this.id+"-r-"+step.id;setText(heading,step.title);group.setAttribute("aria-labelledby",heading.id);group.appendChild(heading);if(stepMessage){const error=h("p","cx-flow-error");setText(error,stepMessage);group.appendChild(error)}
|
|
174
177
|
for(const field of fields){const row=h("div","cx-flow-answer"),term=h("dt"),value=h("dd","cx-flow-answer-value"),action=h("dd","cx-flow-answer-action");setText(term,field.label);const current=field.type==="secret"?this.secretText(field):flowDisplay(this,field,this.values.get(field.id)??null),now=h("span","cx-flow-now");setText(now,current);if(flowEmpty(this.values.get(field.id))&&field.type!=="secret")now.classList.add("cx-flow-empty");value.appendChild(now);if(this.model.initial&&field.type!=="secret"&&this.isChanged(field)){const was=h("span","cx-flow-was");setText(was,fill(labels.flowWas,{value:flowDisplay(this,field,this.initialOf(field))}));value.appendChild(was);row.classList.add("cx-flow-changed")}
|
|
175
178
|
if(this.errors.has(field.id)){const error=h("span","cx-flow-error");setText(error,this.errors.get(field.id));value.appendChild(error);row.classList.add("cx-flow-invalid")}
|
|
176
179
|
if(!field.readOnly){const change=h("button","cx-flow-link cx-flow-change");change.type="button";setText(change,labels.flowChange);change.setAttribute("aria-label",fill(labels.flowChangeField,{field:field.label}));change.addEventListener("click",()=>this.change(field).catch(error=>this.report(error,"change")));action.appendChild(change)}
|
|
@@ -179,7 +182,7 @@ group.appendChild(list);lists.appendChild(group)}
|
|
|
179
182
|
if(!shown&&changesOnly){const none=h("p","cx-flow-none");setText(none,labels.flowNoChanges);lists.appendChild(none)}}});const flowPointer=path=>"/"+path.split(".").map(part=>part.replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
|
|
180
183
|
Object.assign(FlowView.prototype,{submission(){const model=this.model,edit=!!model.initial,values={},secrets={},changes=[],patch=[];for(const field of model.fields.values()){if(!this.visible(field))continue;if(field.type==="secret"){if(this.secrets.has(field.id))secrets[field.id]={state:"set"};else if(this.keeps(field))secrets[field.id]={state:"keep"};continue}
|
|
181
184
|
const value=flowCopy(this.values.get(field.id)??null),from=flowNormal(this.initialOf(field)),to=flowNormal(value);values[field.id]=value;if(edit&&this.isChanged(field))changes.push(Object.assign({field:field.id},field.path?{path:field.path}:{},{from:flowCopy(from),to:flowCopy(to)}));if(!field.path)continue;if(edit){if(!this.isChanged(field))continue;patch.push(to===null?{op:"remove",path:flowPointer(field.path)}:{op:from===null?"add":"replace",path:flowPointer(field.path),value:flowCopy(to)})}else if(to!==null)patch.push({op:"add",path:flowPointer(field.path),value:flowCopy(to)})}
|
|
182
|
-
const proposal={kind:"flowSubmission",flow:model.id,version:1,mode:edit?"edit":"create"};if(model.baseline!==null)proposal.baseline=model.baseline;
|
|
185
|
+
const proposal={kind:"flowSubmission",flow:model.id,version:1,mode:edit?"edit":"create"};if(model.baseline!==null)proposal.baseline=model.baseline;Object.assign(proposal,{values,secrets,changes,patch});flowEach("proposal",this,proposal);return proposal},async submit(){if(this.busy||this.phase!=="form")return null;const ticket=this.beginOperation(),current=ticket.current,wait=ticket.wait;this.banner=null;this.hostErrors.clear();this.hostStepErrors.clear();this.renderActions();try{const result=await wait(this.validateAll(current));if(!current())return null;if(!result.valid){const review=this.reviewStep();if(review&&this.stepVisible(review)){if(this.step!==review)await this.go(review,"forward");else{this.reviewStale=true;this.render()}}
|
|
183
186
|
else{const first=result.errors.find(error=>error.step);await this.go(this.model.steps.find(step=>step.id===first?.step)||this.step,"back");if(first?.step)this.showSummary(this.step)}
|
|
184
187
|
return null}
|
|
185
188
|
const proposal=this.submission(),secrets=new Map([...this.secrets].filter(([id])=>this.visible(this.model.fields.get(id)))),read=new Set();let answer,answered=false,open=true;const close=ticket.close=()=>{open=false;secrets.clear()};const detail=Object.assign(flowCopy(proposal),{respondWith:value=>{if(!answered&&open){answered=true;answer=Promise.resolve(value);answer.catch(()=>{})}},secret:id=>{if(!open||read.has(id)||!secrets.has(id))return null;read.add(id);return secrets.get(id)}});Object.freeze(detail);const go=this.emit("flowSubmit",detail,null,true);if(!go||!current()){close();return null}
|
|
@@ -193,7 +196,7 @@ if(!current()){close();return null}
|
|
|
193
196
|
let settled;if(!answered||answer===undefined)settled={status:"error",message:this.labels.flowNoHost};else{const value=await wait(this.hook("respondWith",()=>answer));if(!current()){close();return null}
|
|
194
197
|
settled=value.failed?{status:"error",message:value.failed}:this.answerOf(value.value)}
|
|
195
198
|
close();if(!current())return null;this.retireOperation(false);this.settle(settled);return settled}finally{if(current())this.retireOperation()}},answerOf(value){const text=item=>typeof item==="string"&&/\S/.test(item) ? item : null;
|
|
196
|
-
if(!flowIs(value)||!["saved","accepted","error","conflict"].includes(value.status))return{status:"error",message:this.labels.flowNotSubmitted};const answer={status:value.status,message:text(value.message),fieldErrors:{},stepErrors:{},result:[],links:[]};if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))answer.fieldErrors[id]=message;if(flowIs(value.stepErrors))for(const[id,message]of Object.entries(value.stepErrors))if(this.model.steps.some(step=>step.id===id)&&text(message))answer.stepErrors[id]=message;if(Array.isArray(value.result))for(const row of value.result.slice(0,50))if(flowIs(row)&&text(row.label)&&flowScalar(row.value))answer.result.push({label:row.label,value:String(row.value),once:row.once===true});if(Array.isArray(value.links))for(const link of value.links.slice(0,10))if(flowIs(link)&&text(link.label)&&typeof link.href==="string"&&flowSafeHref(link.href))answer.links.push({label:link.label,href:link.href});return answer},settle(answer){answer=Object.assign({message:null,fieldErrors:{},stepErrors:{},result:[],links:[]},answer);if(answer.status==="saved"||answer.status==="accepted"){this.phase=answer.status==="saved"?"result":"progress";this.panel=answer.status==="saved"?this.resultPanel(answer):this.progressPanel(answer);this.render();this.panel.querySelector("h3")?.focus();this.emit("flowSubmitted",flowCopy(answer),"onSubmitted");return}
|
|
199
|
+
if(!flowIs(value)||!["saved","accepted","error","conflict"].includes(value.status))return{status:"error",message:this.labels.flowNotSubmitted};const answer={status:value.status,message:text(value.message),fieldErrors:{},stepErrors:{},result:[],links:[]};if(flowIs(value.fieldErrors))for(const[id,message]of Object.entries(value.fieldErrors))if(this.model.fields.has(id)&&text(message))answer.fieldErrors[id]=message;if(flowIs(value.stepErrors))for(const[id,message]of Object.entries(value.stepErrors))if(this.model.steps.some(step=>step.id===id)&&text(message))answer.stepErrors[id]=message;if(Array.isArray(value.result))for(const row of value.result.slice(0,50))if(flowIs(row)&&text(row.label)&&flowScalar(row.value))answer.result.push({label:row.label,value:String(row.value),once:row.once===true});if(Array.isArray(value.links))for(const link of value.links.slice(0,10))if(flowIs(link)&&text(link.label)&&typeof link.href==="string"&&flowSafeHref(link.href))answer.links.push({label:link.label,href:link.href});return answer},settle(answer){answer=Object.assign({message:null,fieldErrors:{},stepErrors:{},result:[],links:[]},answer);flowEach("settled",this,answer);if(answer.status==="saved"||answer.status==="accepted"){this.phase=answer.status==="saved"?"result":"progress";this.panel=answer.status==="saved"?this.resultPanel(answer):this.progressPanel(answer);this.render();this.panel.querySelector("h3")?.focus();this.emit("flowSubmitted",flowCopy(answer),"onSubmitted");return}
|
|
197
200
|
this.phase="form";for(const[id,message]of Object.entries(answer.fieldErrors)){this.hostErrors.set(id,message);this.setError(this.model.fields.get(id),message)}
|
|
198
201
|
for(const[id,message]of Object.entries(answer.stepErrors))this.hostStepErrors.set(id,message);this.reviewStale=true;this.banner=answer;const review=this.reviewStep();const target=review&&this.stepVisible(review)?review:this.visibleSteps().find(step=>this.hostStepErrors.has(step.id)||this.stepFields(step).some(field=>this.hostErrors.has(field.id)))||this.step;if(target===this.step){this.render();if(!review)this.showSummary(this.step);this.stage.querySelector(".cx-flow-answer-banner")?.focus()}
|
|
199
202
|
else this.go(target,"forward").then(()=>{if(!review)this.showSummary(target);else this.stage.querySelector(".cx-flow-answer-banner")?.focus()});this.emit("flowSubmitted",flowCopy(answer),"onSubmitted")},answerBanner(){const answer=this.banner,labels=this.labels,banner=h("div","cx-flow-answer-banner cx-flow-problems"),text=h("p","cx-flow-banner-title");banner.setAttribute("role","alert");banner.tabIndex=-1;setText(text,answer.message||(answer.status==="conflict"?labels.flowConflict:labels.flowNotSubmitted));banner.appendChild(text);if(answer.status==="conflict"){const reload=h("button","cx-flow-btn");reload.type="button";setText(reload,labels.flowReload);reload.addEventListener("click",()=>this.emit("flowReload",{baseline:this.model.baseline},"onReload"));banner.appendChild(reload)}
|
|
@@ -210,5 +213,5 @@ item.append(term,value);list.appendChild(item)}
|
|
|
210
213
|
panel.appendChild(list)}
|
|
211
214
|
if(answer.links.length){const links=h("ul","cx-flow-links");for(const link of answer.links){const item=h("li"),anchor=h("a","cx-flow-link");anchor.href=link.href;setText(anchor,link.label);item.appendChild(anchor);links.appendChild(item)}
|
|
212
215
|
panel.appendChild(links)}
|
|
213
|
-
const actions=h("div","cx-flow-actions cx-flow-panel-actions"),again=h("button","cx-flow-btn");again.type="button";setText(again,labels.flowStartAgain);const acknowledged=()=>{again.disabled=[...panel.querySelectorAll(".cx-flow-saved-it")].some(box=>!box.checked)};panel.addEventListener("change",acknowledged);acknowledged();again.addEventListener("click",()=>{this.panel=null;this.reset();this.stage.querySelector(".cx-flow-step-title")?.focus()});actions.appendChild(again);panel.appendChild(actions);return panel},progressPanel(answer){const panel=this.panelShell("progress",this.labels.flowAccepted),text=h("p","cx-flow-panel-text"),bar=h("progress","cx-flow-bar");setText(text,answer.message||"");bar.setAttribute("aria-labelledby",panel.firstChild.id);panel.append(text,bar);return panel}});function flowCreate(host,options,editors){const view=new FlowView(host,options,editors);const impl={ready:Promise.resolve().then(()=>{if(!view.destroyed)host.setAttribute("data-cx-ready","");view.emit("flowReady",{flow:view.model.id,step:view.step.id},"onReady");return impl}),next:()=>view.next(),back:()=>view.back(),goTo:id=>view.goTo(id),getStep:()=>view.getStep(),getValues:()=>view.getValues(),setValues:values=>view.setValues(values),getState:()=>view.getState(),setState:state=>view.setState(state),isDirty:()=>view.isDirty(),reset:()=>view.reset(),validate:id=>view.validate(id),setFieldError:(id,message)=>view.setFieldError(id,message),submit:()=>view.submit(),setProgress:progress=>view.setProgress(progress),complete:answer=>view.complete(answer),destroy:()=>view.destroy()};return impl}
|
|
214
|
-
FLOW_IMPL
|
|
216
|
+
const actions=h("div","cx-flow-actions cx-flow-panel-actions"),again=h("button","cx-flow-btn");again.type="button";setText(again,labels.flowStartAgain);const acknowledged=()=>{again.disabled=[...panel.querySelectorAll(".cx-flow-saved-it")].some(box=>!box.checked)};panel.addEventListener("change",acknowledged);acknowledged();again.addEventListener("click",()=>{this.panel=null;this.reset();this.stage.querySelector(".cx-flow-step-title")?.focus()});actions.appendChild(again);panel.appendChild(actions);return panel},progressPanel(answer){const panel=this.panelShell("progress",this.labels.flowAccepted),text=h("p","cx-flow-panel-text"),bar=h("progress","cx-flow-bar");setText(text,answer.message||"");bar.setAttribute("aria-labelledby",panel.firstChild.id);panel.append(text,bar);return panel}});function flowCreate(host,options,editors){const view=new FlowView(host,options,editors);const impl={ready:Promise.resolve().then(()=>{if(!view.destroyed)host.setAttribute("data-cx-ready","");view.emit("flowReady",{flow:view.model.id,step:view.step.id},"onReady");return impl}),next:()=>view.next(),back:()=>view.back(),goTo:id=>view.goTo(id),getStep:()=>view.getStep(),getValues:()=>view.getValues(),setValues:values=>view.setValues(values),getState:()=>view.getState(),setState:state=>view.setState(state),isDirty:()=>view.isDirty(),reset:()=>view.reset(),validate:id=>view.validate(id),setFieldError:(id,message)=>view.setFieldError(id,message),submit:()=>view.submit(),setProgress:progress=>view.setProgress(progress),complete:answer=>view.complete(answer),destroy:()=>view.destroy()};const part=(name,none)=>(...args)=>view[name]?view[name](...args):none??flowFail("this flow does not use "+name);return Object.assign(impl,{runCheck:part("runCheck"),refreshOptions:part("refreshOptions"),setStepState:part("setStepState"),setPreset:part("setPreset"),saveDraft:()=>part("saveDraft",false)(true),open:()=>part("open",false)(),close:()=>part("close",false)()})}
|
|
217
|
+
Object.assign(FLOW_IMPL,{create:flowCreate,core:{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow",part])})(typeof window!=="undefined"?window:globalThis);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(global){"use strict";const installed=new WeakSet();const part={version:"
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"1a28068597f435de",install(context){if(installed.has(context.DataGrid.prototype))return;const{DataGrid,h,setText,setClass,fill,gridFreeze,gridDateInputMoment,cellValue,cellText,foldText,toneOf,numberFormat,gridButton,gridOption,gridInputLabel,gridIcon,gridDecorateButton,GRID_REPORTING,loadPart,definePart,announceMenu,boundedPattern,wildcardMatch,svgIcon}=context;const FORMAT_OPERATORS={eq:1,ne:1,gt:1,ge:1,lt:1,le:1,contains:1,notContains:1,startsWith:1,endsWith:1,like:1,notLike:1,matches:1,notMatches:1,in:-1,notIn:-1,between:2,betweenExclusive:2,isNull:0,isNotNull:0,olderThan:1,withinLast:1};const FORMAT_TYPES=["text","number","boolean","date"],FORMAT_DAY=86400000;function formatError(message){throw new Error("CanopyX: "+message)}
|
|
2
2
|
function formatKeys(value,keys,what){if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).some(key=>!keys.includes(key)))formatError("invalid "+what+".")}
|
|
3
3
|
const formatCultures=new Map();function formatCulture(culture){let entry=formatCultures.get(culture);if(!entry){let tag;try{tag=Intl.getCanonicalLocales(culture)[0]}catch(_){formatError("unknown culture '"+culture+"'.")}
|
|
4
4
|
const parts=new Intl.NumberFormat(tag).formatToParts(12345.6),part=type=>(parts.find(item=>item.type===type)||{}).value;entry={tag,group:part("group")||",",decimal:part("decimal")||".",accent:new Intl.Collator(tag,{sensitivity:"accent"}),variant:new Intl.Collator(tag,{sensitivity:"variant"})};if(formatCultures.size<64)formatCultures.set(culture,entry)}
|
|
@@ -59,8 +59,9 @@ function formatFinish(target){if(!target)return target;if(target.icon===undefine
|
|
|
59
59
|
Object.assign(DataGrid.prototype,{formatResult(data,details=false){const rules=this.formatCompiled;if(!rules||!rules.length)return null;const cache=details?this.formatDetailCache||=new WeakMap():this.formatCache;const stats=formatStats(this),cached=cache.get(data);if(cached&&cached.version===this.formatVersion)return cached.result;let row=null,cells=null;for(const rule of rules){if(!rule.enabled||rule.test&&!rule.test(data))continue;const style=formatResolve(rule,data,stats);if(style&&(!details||rule.details)){if(rule.target==="row")row=formatMerge(row,rule,style);else for(const id of rule.columns)(cells||=new Map()).set(id,formatMerge(cells.get(id),rule,style))}
|
|
60
60
|
if(rule.stop)break}
|
|
61
61
|
if(cells)for(const target of cells.values())formatFinish(target);const result=row||cells?{row:formatFinish(row),cells:cells||new Map()}:null;cache.set(data,{version:this.formatVersion,result});return result}});const FORMAT_RANK={danger:5,warning:4,info:3,success:2,neutral:1,muted:1};function formatUndo(fx){for(const node of fx.nodes)node.remove();for(const name of fx.props)fx.element.style.removeProperty(name);for(const name of fx.classes)fx.element.classList.remove(name)}
|
|
62
|
-
function
|
|
63
|
-
|
|
62
|
+
function formatClearSlot(slot){if(slot.fx){formatUndo(slot.fx);slot.fx=null}
|
|
63
|
+
for(const child of slot.childSlots||[])formatClearSlot(child)}
|
|
64
|
+
function formatClear(element){for(const slot of element._cells||[])if(slot)formatClearSlot(slot);if(element._fx){formatUndo(element._fx);element._fx=null}}
|
|
64
65
|
function formatEffects(element){return{element,nodes:[],props:[],classes:[]}}
|
|
65
66
|
function formatDecorate(element,target,fx,before,emphasis){const add=name=>{if(!element.classList.contains(name)){element.classList.add(name);fx.classes.push(name)}};const prop=(target,name,value)=>{target.style.setProperty(name,value);if(target===element)fx.props.push(name)};const node=(tag,className,text)=>{const item=h(tag,className);if(text)setText(item,text);fx.nodes.push(item);return item};if(target.tone&&emphasis){add("cx-fmt-" +(target.bar||target.scale?"text":target.emphasis||emphasis));prop(element,"--cx-fmt",formatToneVar(target.tone))}
|
|
66
67
|
if(target.scale){add("cx-fmt-scale");prop(element,"--cx-fmt-scale",target.scale)}
|
|
@@ -73,9 +74,9 @@ if(target.cue)element.appendChild(node("span","cx-fmt-cue",target.cue));if(targe
|
|
|
73
74
|
const formatCues=target=>({glyph:target.glyph,badge:target.badge,cue:target.cue,tone:target.tone,descriptions:target.descriptions});const FORMAT_PAINT=DataGrid.prototype.paintGridHighlights;DataGrid.prototype.paintGridHighlights=function(element,row){FORMAT_PAINT.call(this,element,row);formatClear(element);const result=!row.status&&this.formatResult&&this.formatResult(row.h);if(!result)return;const slots=element._cells,first=slots[0];if(result.row){const own=result.row;if(own.tone||own.className||own.css){const fx=element._fx=formatEffects(element);setClass(element,element._cls +(own.tone?" cx-fmt-row":"")+(own.className?" "+own.className:""));if(own.tone){element.style.setProperty("--cx-fmt",formatToneVar(own.tone));fx.props.push("--cx-fmt")}
|
|
74
75
|
if(own.css)for(const name of Object.keys(own.css)){element.style.setProperty(name,own.css[name]);fx.props.push(name)}}
|
|
75
76
|
if(first){first.fx=formatEffects(first.cell);first.highlighted=true;formatDecorate(first.cell,formatCues(result.row),first.fx,first.textSpan)}}
|
|
76
|
-
for(let i=0;i<
|
|
77
|
-
if(first&&first.hiddenFields){const details=this.formatResult(row.h,true);if(details)this.formatHiddenFields(first,details)}};const formatFlag=target=>target&&target.details&&(target.tone||target.glyph||target.badge||target.cue);DataGrid.prototype.formatHiddenFields=function(slot,result){const button=slot.hiddenFields,hidden=this.
|
|
78
|
-
const fx=slot.fx||(slot.fx=formatEffects(slot.cell)),effects=formatEffects(button),label=button.getAttribute("aria-label");fx.nodes.push({remove(){formatUndo(effects);button.setAttribute("aria-label",label)}});effects.classes.push("cx-fmt-flag");button.classList.add("cx-fmt-flag");button.style.setProperty("--cx-fmt",tone?formatToneVar(tone):"var(--cx-text-2)");effects.props.push("--cx-fmt");const rules=[...new Set(hidden.flatMap(column=>result.cells.get(column.id).descriptions))].join(", ");
|
|
77
|
+
const columns=this.cols.concat(this.phoneLineColumns()),paintSlots=slots.concat(first?.childSlots||[]);for(let i=0;i<columns.length;i++){const target=result.cells.get(columns[i].id),slot=paintSlots[i];if(!target||!slot)continue;if(!slot.fx||slot.fx.element!==slot.cell)slot.fx=formatEffects(slot.cell);slot.highlighted=true;formatDecorate(slot.cell,target,slot.fx,slot.textSpan,"fill")}
|
|
78
|
+
if(first&&first.hiddenFields){const details=this.formatResult(row.h,true);if(details)this.formatHiddenFields(first,details)}};const formatFlag=target=>target&&target.details&&(target.tone||target.glyph||target.badge||target.cue);DataGrid.prototype.formatHiddenFields=function(slot,result){const button=slot.hiddenFields,hidden=this.phoneRemainingColumns().filter(column=>formatFlag(result.cells.get(column.id)));if(!hidden.length)return;let tone=null;for(const column of hidden){const next=result.cells.get(column.id).tone;if(next&&(FORMAT_RANK[next]||3)>(FORMAT_RANK[tone]||0))tone=next}
|
|
79
|
+
const fx=slot.fx||(slot.fx=formatEffects(slot.cell)),effects=formatEffects(button),label=button.getAttribute("aria-label");let decorated;fx.nodes.push({remove(){formatUndo(effects);if(button.getAttribute("aria-label")===decorated)button.setAttribute("aria-label",label)}});effects.classes.push("cx-fmt-flag");button.classList.add("cx-fmt-flag");button.style.setProperty("--cx-fmt",tone?formatToneVar(tone):"var(--cx-text-2)");effects.props.push("--cx-fmt");const rules=[...new Set(hidden.flatMap(column=>result.cells.get(column.id).descriptions))].join(", ");decorated=label+". "+fill(this.formatLabels.formatHiddenFlagged,{count:hidden.length,rules});button.setAttribute("aria-label",decorated)};const FORMAT_MEASURE=DataGrid.prototype.measureGridColumn;DataGrid.prototype.measureGridColumn=function(column){if(column.formatMinW!==undefined){column.minW=column.formatMinW;delete column.formatMinW}
|
|
79
80
|
const result=FORMAT_MEASURE.call(this,column);if(!this.formatCompiled||!this.formatCompiled.length||!this.fitContext||!this.fitFont)return result;const context=this.fitContext,[font,small,,pad]=this.fitFont,first=column===this.columns.find(item=>!item.hidden);const measure=(text,face)=>{context.font=face;return context.measureText(String(text).slice(0,512)).width};let need=0,floor=0,sampled=0;for(const row of this.rowCache.values()){if(sampled++===200)break;const formats=this.formatResult(row.h),own=formats&&formats.cells.get(column.id),cues=first&&formats&&formats.row;if(!own&&!cues)continue;let width=measure(cellText(row.h.cells[column.id],column,this.fmt)||"—",font),flag=false;for(const target of[own,cues])if(target){if(target.glyph){width +=20;flag=true}
|
|
80
81
|
if(target.badge){width +=measure(target.badge,small)+ 20;flag=true}
|
|
81
82
|
if(target.cue){width +=measure(target.cue,small)+ 6;flag=true}
|