@evotecit/canopyx 0.2.2 → 0.3.3
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-editing.css +1 -6
- package/CanopyX.Html/Assets/canopyx-editing.js +4 -39
- package/CanopyX.Html/Assets/canopyx-export.js +2 -2
- package/CanopyX.Html/Assets/canopyx-flow-entry.css +178 -0
- package/CanopyX.Html/Assets/canopyx-flow-entry.js +78 -0
- package/CanopyX.Html/Assets/canopyx-flow-module.mjs +80 -0
- package/CanopyX.Html/Assets/canopyx-flow.css +180 -0
- package/CanopyX.Html/Assets/canopyx-flow.js +214 -0
- package/CanopyX.Html/Assets/canopyx-forms.css +8 -0
- package/CanopyX.Html/Assets/canopyx-forms.js +39 -0
- package/CanopyX.Html/Assets/canopyx-grid-module.mjs +18 -14
- package/CanopyX.Html/Assets/canopyx-grid.css +2 -0
- package/CanopyX.Html/Assets/canopyx-grid.js +18 -14
- package/CanopyX.Html/Assets/canopyx-interactive.js +10 -9
- package/CanopyX.Html/Assets/canopyx-labels-de.js +72 -0
- package/CanopyX.Html/Assets/canopyx-labels-es.js +72 -0
- package/CanopyX.Html/Assets/canopyx-labels-fr.js +72 -0
- package/CanopyX.Html/Assets/canopyx-labels-it.js +72 -0
- package/CanopyX.Html/Assets/canopyx-labels-pl.js +72 -0
- package/CanopyX.Html/Assets/canopyx-module.mjs +10 -9
- package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +18 -14
- package/CanopyX.Html/Assets/canopyx-reporting.css +2 -0
- package/CanopyX.Html/Assets/canopyx-reporting.js +18 -14
- package/CanopyX.Html/Assets/canopyx.js +10 -9
- package/JavaScript/flow.d.ts +232 -0
- package/JavaScript/grid.d.ts +8 -5
- package/JavaScript/hosts.d.ts +3 -1
- package/JavaScript/locales/de.mjs +72 -0
- package/JavaScript/locales/es.mjs +72 -0
- package/JavaScript/locales/fr.mjs +72 -0
- package/JavaScript/locales/it.mjs +72 -0
- package/JavaScript/locales/pl.mjs +72 -0
- package/README.md +3 -2
- package/package.json +20 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* Flow part: steps,fields,the review page and the result,in the grid's tokens,density and type (docs/flow.md#layouts). */
|
|
2
|
+
.cx-flow{--cx-flow-pad:var(--cx-size-20px);--cx-flow-gap:var(--cx-size-16px);--cx-flow-control:var(--cx-size-34px);
|
|
3
|
+
--cx-size-220px:220px;--cx-size-720px:720px;container-type:inline-size;position:relative;color:var(--cx-text);background:var(--cx-surface);border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-size-8px);font:var(--cx-size-14px)/1.45 var(--cx-font);}
|
|
4
|
+
.cx-flow[data-cx-density="compact"]{--cx-flow-pad:var(--cx-size-14px);--cx-flow-gap:var(--cx-size-12px);--cx-flow-control:var(--cx-size-30px);}
|
|
5
|
+
.cx-flow[data-cx-density="comfortable"]{--cx-flow-pad:var(--cx-size-24px);--cx-flow-gap:var(--cx-size-20px);--cx-flow-control:var(--cx-size-38px);}
|
|
6
|
+
.cx-flow :where(h2,h3,h4,p,ul,ol,dl,dd){margin:0;}
|
|
7
|
+
.cx-flow [hidden]{display:none !important;}
|
|
8
|
+
.cx-flow-head{padding:var(--cx-size-14px) var(--cx-flow-pad);border-bottom:var(--cx-size-1px) solid var(--cx-line);}
|
|
9
|
+
.cx-flow-title{font-size:var(--cx-size-17px);font-weight:600;line-height:1.3;}
|
|
10
|
+
.cx-flow-desc{margin-top:var(--cx-size-4px);font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
11
|
+
.cx-flow-marks{list-style:none;padding:0;}
|
|
12
|
+
.cx-flow-mark-btn{display:flex;align-items:center;gap:var(--cx-size-10px);width:100%;min-height:var(--cx-size-44px);padding:var(--cx-size-6px) var(--cx-size-8px);border:0;border-radius:var(--cx-radius);background:transparent;color:var(--cx-muted);font:inherit;text-align:start;cursor:pointer;}
|
|
13
|
+
.cx-flow-mark-btn:hover{background:var(--cx-hover-bg);}
|
|
14
|
+
.cx-flow-mark-btn[aria-disabled="true"]{cursor:default;}
|
|
15
|
+
.cx-flow-mark-btn[aria-disabled="true"]:hover{background:transparent;}
|
|
16
|
+
.cx-flow-mark-btn:focus-visible,.cx-flow :is(.cx-flow-btn,.cx-flow-link,.cx-flow-icon-btn):focus-visible{outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-1px);}
|
|
17
|
+
.cx-flow-num{flex:none;display:inline-flex;align-items:center;justify-content:center;width:var(--cx-size-24px);height:var(--cx-size-24px);border:var(--cx-size-1px) solid var(--cx-line-strong);border-radius:50%;
|
|
18
|
+
background:var(--cx-surface);font-size:var(--cx-size-12d5px);font-weight:600;font-variant-numeric:tabular-nums;color:var(--cx-muted);}
|
|
19
|
+
.cx-flow-mark-text{display:flex;flex-direction:column;min-width:0;}
|
|
20
|
+
.cx-flow-mark-title{font-size:var(--cx-size-13d5px);line-height:1.3;overflow-wrap:anywhere;}
|
|
21
|
+
.cx-flow-mark-state{font-size:var(--cx-size-12px);color:var(--cx-muted);}
|
|
22
|
+
.cx-flow-mark[data-cx-state="current"] .cx-flow-num{background:var(--cx-accent);border-color:var(--cx-accent);color:var(--cx-on-accent);}
|
|
23
|
+
.cx-flow-mark[data-cx-state="current"] .cx-flow-mark-title{color:var(--cx-text);font-weight:600;}
|
|
24
|
+
.cx-flow-mark[data-cx-state="complete"] .cx-flow-mark-title{color:var(--cx-text-2);}
|
|
25
|
+
.cx-flow-mark[data-cx-state="complete"] .cx-flow-num{color:var(--cx-tone-success);border-color:color-mix(in srgb,var(--cx-tone-success) 45%,var(--cx-surface));}
|
|
26
|
+
.cx-flow-mark[data-cx-state="error"] .cx-flow-num{color:var(--cx-tone-danger);border-color:var(--cx-tone-danger);background:var(--cx-tone-danger-bg);}
|
|
27
|
+
.cx-flow-mark[data-cx-state="error"] :is(.cx-flow-mark-title,.cx-flow-mark-state){color:var(--cx-tone-danger);}
|
|
28
|
+
.cx-flow-mark[data-cx-state="locked"]{opacity:.72;}
|
|
29
|
+
.cx-flow-horizontal > .cx-flow-rail{padding:var(--cx-size-8px) calc(var(--cx-flow-pad) - var(--cx-size-8px));border-bottom:var(--cx-size-1px) solid var(--cx-line);background:var(--cx-head-bg);overflow-x:auto;scrollbar-width:thin;}
|
|
30
|
+
.cx-flow-horizontal .cx-flow-marks{display:flex;align-items:center;gap:var(--cx-size-4px);min-width:max-content;}
|
|
31
|
+
.cx-flow-horizontal .cx-flow-mark{display:flex;align-items:center;gap:var(--cx-size-4px);}
|
|
32
|
+
.cx-flow-horizontal .cx-flow-mark + .cx-flow-mark::before{content:"";flex:none;width:var(--cx-size-24px);border-top:var(--cx-size-1px) solid var(--cx-line-strong);}
|
|
33
|
+
.cx-flow-horizontal .cx-flow-mark-btn{width:auto;min-height:var(--cx-size-36px);}
|
|
34
|
+
.cx-flow-horizontal .cx-flow-mark-title{white-space:nowrap;}
|
|
35
|
+
.cx-flow-horizontal .cx-flow-mark-state:not(.cx-sr-only){display:none;}
|
|
36
|
+
.cx-flow-horizontal .cx-flow-mark[data-cx-state="error"] .cx-flow-mark-state{display:block;}
|
|
37
|
+
.cx-flow-vertical{display:grid;grid-template-columns:minmax(var(--cx-size-180px),var(--cx-size-240px)) minmax(0,1fr);}
|
|
38
|
+
.cx-flow-vertical > .cx-flow-rail{padding:var(--cx-size-12px) var(--cx-size-8px);border-inline-end:var(--cx-size-1px) solid var(--cx-line);background:var(--cx-head-bg);border-end-start-radius:var(--cx-size-8px);}
|
|
39
|
+
.cx-flow-vertical .cx-flow-marks{display:flex;flex-direction:column;gap:var(--cx-size-2px);}
|
|
40
|
+
.cx-flow-vertical .cx-flow-mark-btn{align-items:flex-start;}
|
|
41
|
+
.cx-flow-vertical .cx-flow-mark-btn[aria-current="step"]{background:var(--cx-surface);box-shadow:inset 0 0 0 var(--cx-size-1px) var(--cx-line);}
|
|
42
|
+
.cx-flow-single > .cx-flow-rail{display:none;}
|
|
43
|
+
.cx-flow-single .cx-flow-actions{border-end-start-radius:var(--cx-size-8px);}
|
|
44
|
+
.cx-flow-stack .cx-flow-mark{border-bottom:var(--cx-size-1px) solid var(--cx-line);}
|
|
45
|
+
.cx-flow-stack .cx-flow-mark:last-child{border-bottom:0;}
|
|
46
|
+
.cx-flow-stack .cx-flow-mark-btn{padding:var(--cx-size-8px) var(--cx-flow-pad);border-radius:0;}
|
|
47
|
+
.cx-flow-stack .cx-flow-mark[data-cx-state="current"] > .cx-flow-mark-btn{padding-bottom:var(--cx-size-2px);}
|
|
48
|
+
.cx-flow-stack .cx-flow-mark[data-cx-state="current"] .cx-flow-mark-state{display:none;}
|
|
49
|
+
.cx-flow-stack .cx-flow-step-title{position:absolute;width:var(--cx-size-1px);height:var(--cx-size-1px);overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
|
|
50
|
+
.cx-flow-stack .cx-flow-stage{padding-top:var(--cx-size-8px);}
|
|
51
|
+
.cx-flow-main{min-width:0;}
|
|
52
|
+
.cx-flow-stage{padding:var(--cx-size-20px) var(--cx-flow-pad) var(--cx-size-8px);}
|
|
53
|
+
.cx-flow-step{max-width:var(--cx-size-720px);}
|
|
54
|
+
.cx-flow-step-title{font-size:var(--cx-size-16px);font-weight:600;line-height:1.3;outline:none;}
|
|
55
|
+
.cx-flow-step-desc{margin-top:var(--cx-size-4px);color:var(--cx-muted);font-size:var(--cx-size-13d5px);}
|
|
56
|
+
.cx-flow-step > :is(.cx-flow-block,.cx-flow-section,.cx-flow-summary,.cx-flow-banner,.cx-flow-answer-banner,.cx-flow-review-tools,.cx-flow-review-lists,.cx-flow-declaration,.cx-flow-panel-text,.cx-flow-answers,.cx-flow-links,.cx-flow-bar){margin-top:var(--cx-flow-gap);}
|
|
57
|
+
.cx-flow-section{padding-top:var(--cx-size-4px);border-top:var(--cx-size-1px) solid var(--cx-line);}
|
|
58
|
+
.cx-flow-section-title{padding-top:var(--cx-size-10px);font-size:var(--cx-size-11d5px);font-weight:700;letter-spacing:var(--cx-size-d06em);text-transform:uppercase;color:var(--cx-muted);}
|
|
59
|
+
.cx-flow-section-desc{margin-top:var(--cx-size-2px);font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
60
|
+
.cx-flow-section > .cx-flow-grid{margin-top:var(--cx-size-12px);}
|
|
61
|
+
.cx-flow-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--cx-flow-gap) var(--cx-size-16px);}
|
|
62
|
+
.cx-flow-field{grid-column:1 / -1;min-width:0;}
|
|
63
|
+
.cx-flow-field.cx-flow-half{grid-column:auto;}
|
|
64
|
+
@container (max-width:520px){.cx-flow-field.cx-flow-half{grid-column:1 / -1;}}
|
|
65
|
+
.cx-flow-label{display:block;font-weight:600;font-size:var(--cx-size-13d5px);color:var(--cx-text);}
|
|
66
|
+
.cx-flow-optional{font-weight:400;color:var(--cx-muted);}
|
|
67
|
+
.cx-flow-help{margin-top:var(--cx-size-2px);font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
68
|
+
.cx-flow-control{margin-top:var(--cx-size-6px);}
|
|
69
|
+
.cx-flow-error{margin-top:var(--cx-size-4px);font-size:var(--cx-size-13px);color:var(--cx-tone-danger);}
|
|
70
|
+
.cx-flow .cx-flow-input{box-sizing:border-box;width:100%;max-width:var(--cx-size-440px);min-height:var(--cx-flow-control);height:var(--cx-flow-control);margin:0;padding:0 var(--cx-size-10px);
|
|
71
|
+
font:inherit;color:var(--cx-text);background:var(--cx-surface);border:var(--cx-size-1px) solid var(--cx-control-border);border-radius:var(--cx-radius-sm);color-scheme:inherit;}
|
|
72
|
+
.cx-flow-half .cx-flow-input{max-width:100%;}
|
|
73
|
+
.cx-flow .cx-flow-input::placeholder{color:color-mix(in srgb,var(--cx-text) 60%,var(--cx-surface));font-style:italic;opacity:1;}
|
|
74
|
+
.cx-flow .cx-flow-input:focus-visible{outline:none;border-color:var(--cx-accent);box-shadow:0 0 0 var(--cx-size-2px) var(--cx-accent-soft);}
|
|
75
|
+
.cx-flow .cx-flow-input[aria-invalid="true"],.cx-flow :not(.cx-flow-list)[aria-invalid="true"] .cx-flow-input{border-color:var(--cx-tone-danger);}
|
|
76
|
+
.cx-flow .cx-flow-input:read-only:not(select){background:var(--cx-head-bg);color:var(--cx-text-2);}
|
|
77
|
+
.cx-flow .cx-flow-textarea{height:auto;max-width:100%;padding:var(--cx-size-8px) var(--cx-size-10px);resize:vertical;}
|
|
78
|
+
.cx-flow .cx-flow-type-number .cx-flow-input{max-width:var(--cx-size-180px);text-align:end;font-variant-numeric:tabular-nums;}
|
|
79
|
+
.cx-flow .cx-editor-combo{display:block;max-width:var(--cx-size-440px);}
|
|
80
|
+
.cx-flow-toggle{display:flex;align-items:center;gap:var(--cx-size-10px);margin-top:0;}
|
|
81
|
+
.cx-flow-toggle .cx-flow-label{font-weight:500;}
|
|
82
|
+
.cx-flow .cx-flow-toggle input{appearance:none;flex:none;position:relative;width:var(--cx-size-36px);height:var(--cx-size-20px);min-height:0;margin:0;padding:0;border:var(--cx-size-1px) solid var(--cx-control-border);border-radius:var(--cx-size-999px);background:var(--cx-head-bg);cursor:pointer;box-shadow:none;}
|
|
83
|
+
.cx-flow .cx-flow-toggle input::after{content:"";position:absolute;inset-block-start:var(--cx-size-2px);inset-inline-start:var(--cx-size-2px);width:var(--cx-size-14px);height:var(--cx-size-14px);border-radius:50%;background:var(--cx-muted);}
|
|
84
|
+
.cx-flow .cx-flow-toggle input:checked{background:var(--cx-accent);border-color:var(--cx-accent);}
|
|
85
|
+
.cx-flow .cx-flow-toggle input:checked::after{inset-inline-start:calc(100% - var(--cx-size-16px));background:var(--cx-on-accent);}
|
|
86
|
+
.cx-flow .cx-flow-toggle input:focus-visible{outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-2px);}
|
|
87
|
+
.cx-flow-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--cx-size-220px)),1fr));gap:var(--cx-size-8px);}
|
|
88
|
+
.cx-flow-card{position:relative;display:flex;align-items:flex-start;gap:var(--cx-size-10px);padding:var(--cx-size-10px) var(--cx-size-12px);border:var(--cx-size-1px) solid var(--cx-line-strong);border-radius:var(--cx-radius);background:var(--cx-surface);cursor:pointer;}
|
|
89
|
+
.cx-flow-card:hover{background:var(--cx-hover-bg);}
|
|
90
|
+
.cx-flow-card:has(input:checked){border-color:var(--cx-accent);box-shadow:inset 0 0 0 var(--cx-size-1px) var(--cx-accent);background:color-mix(in srgb,var(--cx-accent) 5%,var(--cx-surface));}
|
|
91
|
+
.cx-flow-card:has(input:focus-visible){outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-2px);}
|
|
92
|
+
.cx-flow-card .cx-flow-choice-input{position:absolute;opacity:0;width:var(--cx-size-1px);height:var(--cx-size-1px);margin:0;}
|
|
93
|
+
.cx-flow-card-body{display:flex;flex-direction:column;gap:var(--cx-size-2px);flex:1;min-width:0;}
|
|
94
|
+
.cx-flow-card-title{font-weight:600;}
|
|
95
|
+
.cx-flow-card-desc{font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
96
|
+
.cx-flow-card-reason{font-size:var(--cx-size-13px);color:var(--cx-text-2);font-style:italic;}
|
|
97
|
+
.cx-flow-card-check{flex:none;width:var(--cx-size-18px);height:var(--cx-size-18px);margin-top:var(--cx-size-1px);border:var(--cx-size-1px) solid var(--cx-control-border);border-radius:50%;}
|
|
98
|
+
.cx-flow-card:has(input:checked) .cx-flow-card-check{border-color:var(--cx-accent);background:var(--cx-accent);box-shadow:inset 0 0 0 var(--cx-size-4px) var(--cx-surface);}
|
|
99
|
+
.cx-flow-card.cx-flow-disabled,.cx-flow-radio.cx-flow-disabled{cursor:default;opacity:.62;}
|
|
100
|
+
.cx-flow-card.cx-flow-disabled:hover{background:var(--cx-surface);}
|
|
101
|
+
.cx-flow-badge{display:inline-block;padding:0 var(--cx-size-6px);border-radius:var(--cx-size-999px);background:var(--cx-head-bg);border:var(--cx-size-1px) solid var(--cx-line);font-size:var(--cx-size-11d5px);font-weight:600;color:var(--cx-text-2);vertical-align:1px;}
|
|
102
|
+
.cx-flow-radios,.cx-flow-multi-list{display:flex;flex-direction:column;gap:var(--cx-size-2px);}
|
|
103
|
+
:is(.cx-flow-radio,.cx-flow-check){display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-2px) var(--cx-size-8px);min-height:var(--cx-size-28px);cursor:pointer;}
|
|
104
|
+
:is(.cx-flow-radio,.cx-flow-check) input{flex:none;width:var(--cx-size-16px);height:var(--cx-size-16px);margin:0;accent-color:var(--cx-accent);}
|
|
105
|
+
.cx-flow-radio .cx-flow-card-body{flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:var(--cx-size-2px) var(--cx-size-8px);}
|
|
106
|
+
.cx-flow-radio .cx-flow-card-title{font-weight:400;}
|
|
107
|
+
.cx-flow-check > .cx-flow-card-desc{flex-basis:100%;padding-inline-start:var(--cx-size-24px);}
|
|
108
|
+
@container (min-width:560px){.cx-flow-multi-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--cx-size-16px);}}
|
|
109
|
+
.cx-flow-multi-group{grid-column:1 / -1;margin-top:var(--cx-size-6px);font-size:var(--cx-size-11d5px);font-weight:700;letter-spacing:var(--cx-size-d06em);text-transform:uppercase;color:var(--cx-muted);}
|
|
110
|
+
.cx-flow-multi-tools{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-4px) var(--cx-size-14px);margin-top:var(--cx-size-6px);font-size:var(--cx-size-13px);}
|
|
111
|
+
.cx-flow-count{color:var(--cx-muted);font-size:var(--cx-size-13px);}
|
|
112
|
+
.cx-flow-list-items{display:flex;flex-direction:column;gap:var(--cx-size-6px);list-style:none;padding:0;}
|
|
113
|
+
.cx-flow-list-item{display:flex;gap:var(--cx-size-6px);max-width:var(--cx-size-440px);}
|
|
114
|
+
.cx-flow-list-item .cx-flow-input{flex:1;}
|
|
115
|
+
.cx-flow-icon-btn{flex:none;width:var(--cx-flow-control);height:var(--cx-flow-control);border:var(--cx-size-1px) solid var(--cx-line-strong);border-radius:var(--cx-radius-sm);background:var(--cx-surface);color:var(--cx-muted);font:inherit;font-size:var(--cx-size-17px);line-height:1;cursor:pointer;}
|
|
116
|
+
.cx-flow-icon-btn:hover{background:var(--cx-hover-bg);color:var(--cx-text);}
|
|
117
|
+
.cx-flow-list-add{margin-top:var(--cx-size-8px);}
|
|
118
|
+
.cx-flow-secret,.cx-flow-duration{display:flex;gap:var(--cx-size-6px);max-width:var(--cx-size-440px);}
|
|
119
|
+
.cx-flow-secret .cx-flow-input{flex:1;}
|
|
120
|
+
.cx-flow .cx-flow-duration .cx-flow-amount{width:var(--cx-size-92px);max-width:var(--cx-size-92px);text-align:end;font-variant-numeric:tabular-nums;}
|
|
121
|
+
.cx-flow .cx-flow-duration .cx-flow-unit{width:auto;max-width:var(--cx-size-180px);}
|
|
122
|
+
.cx-flow-hint{color:var(--cx-text-2);}
|
|
123
|
+
.cx-flow-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--cx-size-6px);height:var(--cx-flow-control);min-height:var(--cx-flow-control);padding:0 var(--cx-size-14px);border:var(--cx-size-1px) solid var(--cx-line-strong);
|
|
124
|
+
border-radius:var(--cx-radius);background:var(--cx-surface);color:var(--cx-text);font:500 var(--cx-size-14px) var(--cx-font);white-space:nowrap;cursor:pointer;}
|
|
125
|
+
.cx-flow-btn:hover{background:var(--cx-hover-bg);}
|
|
126
|
+
.cx-flow-btn:disabled{opacity:.5;cursor:default;}
|
|
127
|
+
.cx-flow-btn.cx-primary{background:var(--cx-accent);border-color:var(--cx-accent);color:var(--cx-on-accent);font-weight:600;}
|
|
128
|
+
.cx-flow-btn.cx-primary:hover:not(:disabled){background:color-mix(in srgb,var(--cx-accent) 88%,var(--cx-text));}
|
|
129
|
+
.cx-flow-link{padding:0;border:0;background:none;color:var(--cx-accent-text);font:inherit;text-align:start;text-decoration:underline;text-underline-offset:var(--cx-size-2px);cursor:pointer;}
|
|
130
|
+
.cx-flow-link:hover{text-decoration-thickness:var(--cx-size-2px);}
|
|
131
|
+
.cx-flow-actions{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;gap:var(--cx-size-8px);padding:var(--cx-size-12px) var(--cx-flow-pad);border-top:var(--cx-size-1px) solid var(--cx-line);background:var(--cx-surface);border-end-end-radius:var(--cx-size-8px);}
|
|
132
|
+
.cx-flow-actions[hidden]{display:none;}
|
|
133
|
+
.cx-flow-spacer{flex:1;}
|
|
134
|
+
.cx-flow-stack .cx-flow-actions{position:static;border-radius:0;}
|
|
135
|
+
.cx-flow-narrow :is(.cx-flow-btn,.cx-flow-icon-btn){min-height:var(--cx-size-44px);height:var(--cx-size-44px);}
|
|
136
|
+
.cx-flow.cx-flow-narrow .cx-flow-input:not(textarea,[type="checkbox"]){min-height:var(--cx-size-44px);height:var(--cx-size-44px);}
|
|
137
|
+
.cx-flow-narrow .cx-flow-icon-btn{width:var(--cx-size-44px);}
|
|
138
|
+
.cx-flow-narrow .cx-flow-next{flex:1;}
|
|
139
|
+
.cx-flow-summary,.cx-flow-banner,.cx-flow-answer-banner{padding:var(--cx-size-10px) var(--cx-size-14px);border-radius:var(--cx-radius);border-inline-start:var(--cx-size-3px) solid var(--cx-line-strong);background:var(--cx-head-bg);outline:none;}
|
|
140
|
+
.cx-flow-summary,.cx-flow-problems{border-inline-start-color:var(--cx-tone-danger);background:var(--cx-tone-danger-bg);}
|
|
141
|
+
.cx-flow-ready{border-inline-start-color:var(--cx-tone-success);background:var(--cx-tone-success-bg);}
|
|
142
|
+
.cx-flow-summary-title,.cx-flow-banner-title{font-weight:600;}
|
|
143
|
+
.cx-flow-summary ul,.cx-flow-banner-list{margin-top:var(--cx-size-4px);padding-inline-start:var(--cx-size-18px);}
|
|
144
|
+
.cx-flow-summary .cx-flow-link,.cx-flow-problems .cx-flow-link{color:var(--cx-tone-danger);}
|
|
145
|
+
.cx-flow-banner-warn{margin-top:var(--cx-size-4px);color:var(--cx-tone-warning);font-weight:600;}
|
|
146
|
+
.cx-flow-warning{color:var(--cx-tone-warning);}
|
|
147
|
+
.cx-flow-answer-banner{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-8px) var(--cx-size-12px);}
|
|
148
|
+
.cx-flow-answer-banner .cx-flow-btn{margin-inline-start:auto;}
|
|
149
|
+
.cx-flow-review-tools{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cx-size-6px) var(--cx-size-16px);}
|
|
150
|
+
.cx-flow-switch{display:inline-flex;align-items:center;gap:var(--cx-size-8px);min-height:var(--cx-size-28px);cursor:pointer;}
|
|
151
|
+
.cx-flow-switch input{width:var(--cx-size-16px);height:var(--cx-size-16px);margin:0;accent-color:var(--cx-accent);}
|
|
152
|
+
.cx-flow-review-group + .cx-flow-review-group{margin-top:var(--cx-size-20px);}
|
|
153
|
+
.cx-flow-review-title{padding-bottom:var(--cx-size-6px);border-bottom:var(--cx-size-1px) solid var(--cx-line-strong);font-size:var(--cx-size-14px);font-weight:600;}
|
|
154
|
+
.cx-flow-answers{display:flex;flex-direction:column;}
|
|
155
|
+
.cx-flow-answer{display:grid;grid-template-columns:minmax(var(--cx-size-140px),36%) minmax(0,1fr) auto;gap:var(--cx-size-4px) var(--cx-size-16px);padding:var(--cx-size-8px) 0;border-bottom:var(--cx-size-1px) solid var(--cx-line);}
|
|
156
|
+
.cx-flow-answer dt{color:var(--cx-muted);font-size:var(--cx-size-13d5px);}
|
|
157
|
+
.cx-flow-answer-value{display:flex;flex-direction:column;gap:var(--cx-size-2px);min-width:0;overflow-wrap:anywhere;white-space:pre-line;}
|
|
158
|
+
.cx-flow-empty{color:var(--cx-muted);}
|
|
159
|
+
.cx-flow-was{font-size:var(--cx-size-13px);color:var(--cx-muted);}
|
|
160
|
+
.cx-flow-changed .cx-flow-now{font-weight:600;}
|
|
161
|
+
.cx-flow-answer-action{text-align:end;}
|
|
162
|
+
@container (max-width:520px){.cx-flow-answer{grid-template-columns:minmax(0,1fr) auto;}.cx-flow-answer dt{grid-column:1 / -1;}}
|
|
163
|
+
.cx-flow-declaration{padding:var(--cx-size-10px) 0 0;border-top:var(--cx-size-1px) solid var(--cx-line);color:var(--cx-text-2);font-size:var(--cx-size-13d5px);}
|
|
164
|
+
.cx-flow-none{color:var(--cx-muted);}
|
|
165
|
+
.cx-flow-panel .cx-flow-step-title{display:flex;align-items:center;gap:var(--cx-size-10px);}
|
|
166
|
+
.cx-flow-panel-mark{display:inline-flex;align-items:center;justify-content:center;width:var(--cx-size-28px);height:var(--cx-size-28px);border-radius:50%;background:var(--cx-tone-success-bg);color:var(--cx-tone-success);font-size:var(--cx-size-14px);}
|
|
167
|
+
.cx-flow-progress .cx-flow-panel-mark{background:var(--cx-accent-soft);color:var(--cx-accent-text);}
|
|
168
|
+
.cx-flow-result-value{display:inline-block;max-width:100%;padding:var(--cx-size-4px) var(--cx-size-8px);border-radius:var(--cx-radius-sm);background:var(--cx-head-bg);font-family:var(--cx-font-mono);font-size:var(--cx-size-13px);overflow-wrap:anywhere;white-space:normal;}
|
|
169
|
+
.cx-flow-once .cx-flow-answer-value{align-items:flex-start;gap:var(--cx-size-6px);}
|
|
170
|
+
.cx-flow-links{display:flex;flex-direction:column;gap:var(--cx-size-4px);padding-inline-start:var(--cx-size-18px);}
|
|
171
|
+
.cx-flow-panel-actions{position:static;margin-top:var(--cx-flow-gap);padding-inline:0;border-top:0;}
|
|
172
|
+
progress.cx-flow-bar{display:block;width:100%;max-width:var(--cx-size-440px);height:var(--cx-size-8px);accent-color:var(--cx-accent);}
|
|
173
|
+
@media (forced-colors:active){
|
|
174
|
+
.cx-flow-mark[data-cx-state="current"] .cx-flow-num{forced-color-adjust:none;background:Highlight;color:HighlightText;border-color:Highlight;}
|
|
175
|
+
.cx-flow-card:has(input:checked){outline:var(--cx-size-2px) solid Highlight;}
|
|
176
|
+
.cx-flow .cx-flow-toggle input{border-color:CanvasText;}
|
|
177
|
+
.cx-flow .cx-flow-toggle input:checked{background:Highlight;}
|
|
178
|
+
.cx-flow .cx-flow-toggle input::after{background:CanvasText;}
|
|
179
|
+
}
|
|
180
|
+
@media print{.cx-flow-actions,.cx-flow-rail,.cx-flow-change{display:none;}}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
(function(global){"use strict";const installed=new WeakSet();const part={version:"75b9643b282dddb7",requires:["forms"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,foldText,boundedPattern,numberFormat,gridFreeze}=context;const{editParseNumber,EDIT_EDITORS,formRuleSpec,formCheck,FORM_LABELS}=FORMS_IMPL;const FLOW_LABELS={flowSteps:"Steps",flowStepOf:"Step {current} of {count}",flowStepAnnounce:"Step {current} of {count}, {title}",flowNext:"Next",flowBack:"Back",flowSubmit:"Submit",flowSubmitting:"Submitting…",flowReviewTitle:"Check your answers",flowChange:"Change",flowChangeField:"Change {field}",flowNotProvided:"Not provided",flowOptional:"(optional)",flowStepOptional:"Optional",flowStepComplete:"Complete",flowStepCurrent:"Current step",flowStepErrors:"Errors: {count}",flowStepLocked:"Complete the earlier steps first",flowStepNotStarted:"Not started",flowErrorSummary:"Fix these before you continue",flowReady:"Ready to submit",flowNotReady:"Errors to fix: {count}",flowWarnings:"Warnings: {count}",flowChangesOnly:"Show changes only",flowNoChanges:"No answers have changed.",flowWas:"Was: {value}",flowChangedCount:"Changed answers: {count}",flowSecretSet:"Set",flowSecretKept:"Unchanged",flowSecretNotSet:"Not set",flowSecretHint:"A value is saved. Leave blank to keep it.",flowSecretShow:"Show",flowSecretHide:"Hide",flowSecretReveal:"Show {field}",flowOn:"On",flowOff:"Off",flowAdd:"Add",flowAddItem:"Add to {field}",flowRemoveItem:"Remove {item}",flowItemLabel:"{field}, item {index}",flowSelectAll:"Select all",flowSelectNone:"Clear",flowSelectedCount:"{count} of {total} selected",flowUnit:"Unit",flowUnitSeconds:"seconds",flowUnitMinutes:"minutes",flowUnitHours:"hours",flowUnitDays:"days",flowSeconds:"[one]{count} second[other]{count} seconds",flowMinutes:"[one]{count} minute[other]{count} minutes",flowHours:"[one]{count} hour[other]{count} hours",flowDays:"[one]{count} day[other]{count} days",flowDurationInvalid:"{field} must be a whole number.",flowItemsMin:"{field} needs at least {min}.",flowItemsMax:"{field} allows at most {max}.",flowItemEmail:"“{item}” is not an email address.",flowItemHost:"“{item}” is not a host name.",flowItemIp:"“{item}” is not an IP address.",flowItemCidr:"“{item}” is not a network range.",flowItemUrl:"“{item}” is not a web address.",flowItemPattern:"“{item}” does not have the expected format.",flowSaved:"Saved",flowAccepted:"In progress",flowProgress:"{done} of {total}",flowConflict:"These settings changed while you were editing them.",flowReload:"Reload current settings",flowNotSubmitted:"The answers were not submitted.",flowNoHost:"No host received the answers.",flowStartAgain:"Start another",flowCopy:"Copy",flowCopied:"Copied",flowSavedIt:"I have saved it",flowShownOnce:"Shown only once. Copy it now."};function flowLabels(...sources){const labels={...FORM_LABELS,...FLOW_LABELS};for(const source of sources){if(source==null)continue;if(!flowIs(source))flowFail("labels must be an object of strings");for(const[name,text]of Object.entries(source)){if(!Object.hasOwn(labels,name)){if(/^flow[A-Z]/.test(name)) flowFail("there is no flow label '" + name + "'"); continue; }
|
|
2
|
+
if(typeof text!=="string"||!/\S/.test(text)) flowFail("label '" + name + "' must be a string with visible text");
|
|
3
|
+
labels[name]=text}}
|
|
4
|
+
return Object.freeze(labels)}
|
|
5
|
+
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+"'")}
|
|
7
|
+
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
|
+
return value}
|
|
9
|
+
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}
|
|
10
|
+
const flowFlag=(value,what)=>{if(value!==undefined&&typeof value!=="boolean")flowFail(what+" must be true or false");return value===true};function flowSeconds(text){const match=typeof text==="string"&&/^P(?!$)(?:(\d{1,9})D)?(?:T(?!$)(?:(\d{1,9})H)?(?:(\d{1,9})M)?(?:(\d{1,9})S)?)?$/.exec(text);
|
|
11
|
+
return match?Number(match[1]||0)*86400+Number(match[2]||0)*3600+Number(match[3]||0)*60+Number(match[4]||0):null}
|
|
12
|
+
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
|
+
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);
|
|
15
|
+
case "time":return typeof value==="string"&&/^([01]\d|2[0-3]):[0-5]\d$/.test(value);
|
|
16
|
+
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");
|
|
19
|
+
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
|
+
if(raw.keepWhenBlank!==undefined&&(type!=="secret"||typeof raw.keepWhenBlank!=="boolean"))bad("can keep a blank value only as a secret");if(raw.reveal!==undefined&&(type!=="secret"||typeof raw.reveal!=="boolean"))bad("has a reveal setting but is not a secret");if((raw.onText!==undefined||raw.offText!==undefined)&&type!=="toggle")bad("names on and off texts but is not a toggle");if(raw.rows!==undefined&&(type!=="textarea"||!Number.isInteger(raw.rows)||raw.rows<2||raw.rows>20))bad("needs 2 to 20 rows, as a text area only");if(raw.display!==undefined&&(type!=="choice"||!["cards","list"].includes(raw.display)))bad("has an unknown display");if(raw.editor!==undefined?type!=="custom"||typeof raw.editor!=="string"||!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(raw.editor) : type === "custom") bad("needs an editor name as a custom field only");
|
|
21
|
+
if(raw.step!==undefined&&type!=="number")bad("has a step but is not a number");if(raw.placeholder!==undefined&&!["text","number","lookup","textarea","list"].includes(type))bad("has a placeholder, which text, number, lookup, text area and list fields take");const choosing=["select","choice","multiChoice","lookup"].includes(type);if(raw.options!==undefined&&!choosing)bad("has options but does not choose");if(choosing&&!(Array.isArray(raw.options)&&raw.options.length))bad("needs options");if(Array.isArray(raw.options)&&raw.options.length>1000)bad("needs at most 1000 options");field.options=(raw.options||[]).map(option=>{flowKeys(flowIs(option)?option:{value:option},["value","label","description","badge","disabledReason","group"],"An option of field "+id);const item=flowIs(option)?option:{value:option};if(!flowScalar(item.value))bad("has an invalid option value");return Object.freeze({value:item.value,label:flowString(item.label,200,"An option label")??String(item.value),description:flowString(item.description,1000,"An option description"),badge:flowString(item.badge,40,"An option badge"),disabledReason:flowString(item.disabledReason,400,"A disabled reason"),group:flowString(item.group,200,"An option group")})});if(field.options.length>1000||new Set(field.options.map(option=>option.value)).size!==field.options.length)bad("needs at most 1000 options with distinct values");const list=type==="list"||type==="multiChoice";if(list&&[rules.min,rules.max,rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined))bad("checks items, not lengths or ranges");if(!list&&[rules.minItems,rules.maxItems].some(value=>value!==undefined)||type!=="list"&&(rules.item!==undefined||rules.itemPattern!==undefined))bad("has rules only lists take");if((rules.min!==undefined||rules.max!==undefined)&&!["number","datetime","duration"].includes(type))bad("has a range but is not a number, date and time or duration");if([rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&!["text","textarea","secret"].includes(type))bad("has lengths or a pattern, which text, text area and secret fields take");if(rules.minLength>rules.maxLength)bad("needs the shorter length first");const count=value=>value===undefined||Number.isInteger(value)&&value>=0&&value<=1000;if(!count(rules.minItems)||!count(rules.maxItems)||rules.minItems>rules.maxItems)bad("needs 0 to 1000 items, the fewest first");if(rules.item!==undefined&&!["email","host","ip","cidr","url"].includes(rules.item))bad("has an unknown item rule");let itemPattern=null;if(rules.itemPattern!==undefined)try{itemPattern=boundedPattern(rules.itemPattern,true,true)}catch(error){bad("has an invalid or unsafe item pattern")}
|
|
22
|
+
const range=type==="duration"?{min:rules.min===undefined?undefined:flowSeconds(rules.min),max:rules.max===undefined?undefined:flowSeconds(rules.max)}:rules;if(type==="duration"&&(range.min===null||range.max===null))bad("needs ISO 8601 durations as its range");if(type==="number"&&![rules.min,rules.max].every(bound=>bound===undefined||Number.isFinite(bound)&&Math.abs(bound)<=Number.MAX_SAFE_INTEGER))bad("needs numbers as its range");if(type==="datetime"&&![rules.min,rules.max].every(bound=>bound===undefined||flowFits({type:"datetime"},bound)))bad("needs ISO 8601 moments as its range");const low=type==="datetime"&&typeof range.min==="string"?Date.parse(range.min):range.min,high=type==="datetime"&&typeof range.max==="string"?Date.parse(range.max):range.max;if(low!==undefined&&high!==undefined&&low>high)bad("needs the lower limit first");const shared=formRuleSpec({step:raw.step,placeholder:raw.placeholder},{required:field.required,min:range.min,max:range.max,minLength:rules.minLength,maxLength:rules.maxLength,pattern:rules.pattern,message:rules.message},bad);field.spec=Object.freeze({id,pattern:shared.pattern,rules:shared.rules});field.items=Object.freeze({min:rules.minItems,max:rules.maxItems,rule:rules.item,pattern:itemPattern});const units=raw.units??["minutes","hours","days"];if(raw.units!==undefined&&type!=="duration"||!Array.isArray(units)||!units.length||units.some(unit=>!Object.hasOwn(FLOW_UNITS,unit))||new Set(units).size!==units.length)bad("has invalid duration units");field.units=Object.keys(FLOW_UNITS).filter(unit=>units.includes(unit));if(raw.default!==undefined&&(type==="secret"||list&&!Array.isArray(raw.default)||!flowFits(field,raw.default)))bad("has a default that does not fit it");field.start=raw.default===undefined?(list?[]:null):raw.default;model.fields.set(id,field);return field}
|
|
23
|
+
function flowRead(doc){flowKeys(doc,["kind","version","id","title","description","mode","layout","density","timeZone","steps","initial","storedSecrets","baseline","submit","labels"],"A flow document");if(doc.kind!=="flow"||doc.version!==1)flowFail("a flow document needs kind \"flow\" and version 1");const model={id:flowName(doc.id,"The flow id"),title:flowString(doc.title,200,"The flow title",true),description:flowString(doc.description,1000,"The flow description"),mode:doc.mode??"linear",density:doc.density??"normal",local:doc.timeZone==="local",fields:new Map(),steps:[],sections:new Set()};if(!["linear","free"].includes(model.mode)||!["compact","normal","comfortable"].includes(model.density)||doc.timeZone!==undefined&&!["utc","local"].includes(doc.timeZone))flowFail("the flow's mode, density or time zone is unknown");if(doc.layout!==undefined)flowKeys(doc.layout,["orientation"],"The flow layout");model.orientation=doc.layout?.orientation??"auto";if(!["auto","horizontal","vertical"].includes(model.orientation))flowFail("the flow layout has an unknown orientation");if(!Array.isArray(doc.steps)||!doc.steps.length||doc.steps.length>64)flowFail("a flow needs 1 to 64 steps");for(const[index,raw]of doc.steps.entries()){flowKeys(raw,["id","title","description","type","fields","sections","when","optional","declaration","changesOnly"],"A flow step");const step={id:flowName(raw.id,"A step id"),title:flowString(raw.title,200,"A step title",raw.type!=="review"),description:flowString(raw.description,1000,"A step description"),type:raw.type??"form",optional:flowFlag(raw.optional,"optional"),declaration:flowString(raw.declaration,1000,"The declaration"),changesOnly:raw.changesOnly!==false,blocks:[],raw};if(model.steps.some(other=>other.id===step.id))flowFail("the step id '"+step.id+"' is used twice");if(!["form","info","review"].includes(step.type))flowFail("step "+step.id+" has an unknown type");if(step.type==="review"&&index!==doc.steps.length-1)flowFail("the review step must be the last step");if(step.type==="review"&&(raw.when!==undefined||raw.optional===true))flowFail("the review step takes no condition and is never optional");if(step.type!=="review"&&(raw.declaration!==undefined||raw.changesOnly!==undefined))flowFail("only a review step has a declaration or changesOnly");if(raw.changesOnly!==undefined&&typeof raw.changesOnly!=="boolean")flowFail("changesOnly must be true or false");if(step.type!=="form"&&(raw.fields!==undefined||raw.sections!==undefined))flowFail("only a form step has fields and sections");if(raw.fields!==undefined&&!Array.isArray(raw.fields)||raw.sections!==undefined&&!Array.isArray(raw.sections))flowFail("step "+step.id+" needs arrays of fields and sections");if(raw.fields?.length)step.blocks.push({section:null,fields:raw.fields.map(field=>flowField(field,step,null,model))});if((raw.sections||[]).length>64)flowFail("step "+step.id+" holds at most 64 sections");for(const rawSection of raw.sections||[]){flowKeys(rawSection,["id","label","description","fields","when"],"A flow section");const section={id:flowName(rawSection.id,"A section id"),label:flowString(rawSection.label,200,"A section label",true),description:flowString(rawSection.description,1000,"A section description"),raw:rawSection};if(model.sections.has(section.id))flowFail("the section id '"+section.id+"' is used twice");model.sections.add(section.id);if(!Array.isArray(rawSection.fields))flowFail("section "+section.id+" needs an array of fields");step.blocks.push({section,fields:rawSection.fields.map(field=>flowField(field,step,section,model))})}
|
|
24
|
+
model.steps.push(step)}
|
|
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}
|
|
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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))||
|
|
33
|
+
["startsWith","endsWith","matches","notMatches"].includes(op)&&typeof values[0]!=="string"||
|
|
34
|
+
["gt","ge","lt","le"].includes(op)&&typeof values[0]==="boolean"||
|
|
35
|
+
op==="between"&&!(typeof values[0]===typeof values[1]&&["number","string"].includes(typeof values[0])&&flowOrder(values[0],values[1])<=0))
|
|
36
|
+
flowFail("a condition on '"+filter.field+"' has values its operator does not take");let pattern=null;if(filter.operator==="matches"||filter.operator==="notMatches"){if(typeof values[0]!=="string")flowFail("a pattern condition needs a pattern");try{pattern=boundedPattern(values[0],false)}catch(error){flowFail("a condition has an invalid or unsafe pattern")}}
|
|
37
|
+
return Object.freeze({field:filter.field,operator:filter.operator,values:Object.freeze(values.slice()),pattern})})})}
|
|
38
|
+
function flowRefs(condition,into=new Set()){if(condition){condition.filters.forEach(filter=>into.add(filter.field));condition.groups.forEach(group=>flowRefs(group,into))}
|
|
39
|
+
return into}
|
|
40
|
+
const flowSame=(left,right)=>typeof left===typeof right&&(typeof left==="string"?foldText(left)===foldText(right):left===right);function flowOrder(left,right){if(typeof left==="number"&&typeof right==="number")return left<right?-1:left>right?1:0;if(typeof left!=="string"||typeof right!=="string")return null;const a=foldText(left),b=foldText(right);return a<b?-1:a>b?1:0}
|
|
41
|
+
function flowMatch(filter,value){const operator=filter.operator,operand=filter.values[0];if(flowEmpty(value))return operator==="isNull";if(operator==="isNull"||operator==="isNotNull")return operator==="isNotNull";const listed=item=>filter.values.some(entry=>flowSame(item,entry));if(Array.isArray(value)){if(operator==="contains"||operator==="notContains")return value.some(item=>flowSame(item,operand))===(operator==="contains");if(operator==="in"||operator==="notIn")return value.some(listed)===(operator==="in");return false}
|
|
42
|
+
const text=typeof value==="string"&&typeof operand==="string",order=flowOrder(value,operand);switch(operator){case "eq":return flowSame(value,operand);case "ne":return!flowSame(value,operand);case "in":return listed(value);case "notIn":return!listed(value);case "gt":return order!==null&&order>0;case "ge":return order!==null&&order>=0;case "lt":return order!==null&&order<0;case "le":return order!==null&&order<=0;case "between":{const low=flowOrder(value,filter.values[0]),high=flowOrder(value,filter.values[1]);return low!==null&&high!==null&&low>=0&&high<=0}
|
|
43
|
+
case "contains":return text&&foldText(value).includes(foldText(operand));case "notContains":return text&&!foldText(value).includes(foldText(operand));case "startsWith":return text&&foldText(value).startsWith(foldText(operand));case "endsWith":return text&&foldText(value).endsWith(foldText(operand));case "matches":return typeof value==="string"&&filter.pattern.test(value.slice(0,4096));case "notMatches":return typeof value==="string"&&!filter.pattern.test(value.slice(0,4096));default:return false}}
|
|
44
|
+
function flowTest(condition,read){if(!condition)return true;const results=[...condition.filters.map(filter=>flowMatch(filter,read(filter.field))),...condition.groups.map(group=>flowTest(group,read))];return condition.operator==="and"?results.every(Boolean):condition.operator==="or"?results.some(Boolean):!results.some(Boolean)}
|
|
45
|
+
function flowConditions(model){const compile=raw=>raw===undefined?null:flowCondition(raw,model,{groups:0,filters:0});for(const step of model.steps){step.when=compile(step.raw.when);for(const block of step.blocks)if(block.section)block.section.when=compile(block.section.raw.when)}
|
|
46
|
+
for(const field of model.fields.values()){field.when=compile(field.raw.when);field.refs=flowRefs(field.step.when,flowRefs(field.section?.when,flowRefs(field.when)))}
|
|
47
|
+
const state=new Map(),visit=field=>{if(state.get(field.id)===2)return;if(state.get(field.id)===1)flowFail("the conditions of field "+field.id+" depend on themselves");state.set(field.id,1);field.refs.forEach(id=>visit(model.fields.get(id)));state.set(field.id,2)};model.fields.forEach(visit)}
|
|
48
|
+
const FLOW_SHARED_EDITORS=new Set(["text","number","date","datetime","time","select","toggle","lookup"]);function flowFieldView(view,field){const id=view.id+"-f-"+field.id,wrapper=h("div","cx-flow-field cx-flow-type-"+field.type +(field.width==="half"?" cx-flow-half":""));wrapper.dataset.cxField=field.id;const label=h(FLOW_SHARED_EDITORS.has(field.type)||["textarea","secret","custom"].includes(field.type)?"label":"div","cx-flow-label");label.id=id+"-label";setText(label,field.label);if(!field.required&&field.type!=="toggle"&&!field.readOnly){const optional=h("span","cx-flow-optional");setText(optional," "+view.labels.flowOptional);label.appendChild(optional)}
|
|
49
|
+
const help=field.help?h("p","cx-flow-help"):null,hint=h("p","cx-flow-help cx-flow-hint"),message=h("p","cx-flow-error");if(help){help.id=id+"-help";setText(help,field.help)}
|
|
50
|
+
hint.id=id+"-hint";hint.hidden=true;message.id=id+"-error";message.hidden=true;const fv={field,element:wrapper,label,message,instance:null,abort:null};const create=value=>{fv.abort?.abort();fv.abort=new AbortController();const context=flowEditorContext(view,field,id,value,fv.abort.signal);const instance=flowEditor(view,field,context);if(!instance||!(instance.element instanceof Element)||typeof instance.getValue!=="function")flowFail("the editor of field "+field.id+" must return an element and getValue");if(value==null&&["toggle","list","multiChoice"].includes(field.type)){let touched=false;const read=instance.getValue;for(const event of["input","change"])instance.element.addEventListener(event,()=>{touched=true},{signal:fv.abort.signal});instance.getValue=()=>touched?read():null}
|
|
51
|
+
return instance};fv.mount=value=>{try{fv.instance?.destroy?.()}catch(error){view.report(error,"editor")}
|
|
52
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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 flowDate(view,iso,options){try{return new Intl.DateTimeFormat(view.lang||undefined,options).format(new Date(iso))}catch(error){return iso}}
|
|
61
|
+
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
|
+
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)})}
|
|
63
|
+
const flowId=(context,suffix)=>context.id+"-"+suffix;function flowChoiceInput(context,field){const cards=field.display?field.display==="cards":field.options.length<=5,group=h("div","cx-flow-choices" +(cards?" cx-flow-cards":" cx-flow-radios"));group.setAttribute("role","radiogroup");const inputs=field.options.map((option,index)=>{const item=h("label",cards?"cx-flow-card":"cx-flow-radio"),input=h("input","cx-flow-choice-input"),body=h("span","cx-flow-card-body"),title=h("span","cx-flow-card-title");input.type="radio";input.name=context.id;input.value=String(index);input.checked=option.value===context.value;input.disabled=!!option.disabledReason||context.readOnly;setText(title,option.label);if(option.badge){const badge=h("span","cx-flow-badge");setText(badge,option.badge);title.append(" ",badge)}
|
|
64
|
+
body.appendChild(title);const notes=[];for(const[text,name]of[[option.description,"cx-flow-card-desc"],[option.disabledReason,"cx-flow-card-reason"]])if(text){const note=h("span",name);note.id=flowId(context,"o"+index+name.slice(-4));setText(note,text);body.appendChild(note);notes.push(note.id)}
|
|
65
|
+
if(notes.length)input.setAttribute("aria-describedby",notes.join(" "));item.append(input,body);if(cards){const mark=h("span","cx-flow-card-check");mark.setAttribute("aria-hidden","true");item.appendChild(mark)}
|
|
66
|
+
if(input.disabled)item.classList.add("cx-flow-disabled");group.appendChild(item);return input});return{element:group,control:group,group:true,focus:()=>(inputs.find(input=>input.checked)||inputs.find(input=>!input.disabled))?.focus(),getValue:()=>{const at=inputs.findIndex(input=>input.checked);return at<0?null:field.options[at].value}}}
|
|
67
|
+
function flowMultiInput(context,field,labels){const group=h("div","cx-flow-multi"),list=h("div","cx-flow-multi-list"),count=h("span","cx-flow-count"),chosen=new Set(context.value||[]);group.setAttribute("role","group");count.setAttribute("aria-live","polite");const inputs=[];let heading=null;field.options.forEach((option,index)=>{if(option.group&&option.group!==heading){heading=option.group;const title=h("div","cx-flow-multi-group");setText(title,heading);list.appendChild(title)}
|
|
68
|
+
const item=h("label","cx-flow-check"),input=h("input"),text=h("span");input.type="checkbox";input.value=String(index);input.checked=chosen.has(option.value);input.disabled=!!option.disabledReason||context.readOnly;setText(text,option.label);item.append(input,text);if(option.description||option.disabledReason){const note=h("span","cx-flow-card-desc");note.id=flowId(context,"o"+index);setText(note,option.disabledReason||option.description);item.appendChild(note);input.setAttribute("aria-describedby",note.id)}
|
|
69
|
+
list.appendChild(item);inputs.push(input)});const paint=()=>setText(count,fill(labels.flowSelectedCount,{count:inputs.filter(input=>input.checked).length,total:inputs.length}));const tools=h("div","cx-flow-multi-tools");if(inputs.length>4&&!context.readOnly)for(const[text,on]of[[labels.flowSelectAll,true],[labels.flowSelectNone,false]]){const button=h("button","cx-flow-link");button.type="button";setText(button,text);button.addEventListener("click",()=>{inputs.forEach(input=>{if(!input.disabled)input.checked=on});paint();group.dispatchEvent(new Event("change",{bubbles:true}))});tools.appendChild(button)}
|
|
70
|
+
tools.appendChild(count);group.addEventListener("change",paint);group.append(list,tools);paint();return{element:group,control:group,group:true,focus:()=>inputs.find(input=>!input.disabled)?.focus(),getValue:()=>field.options.filter((option,index)=>inputs[index].checked).map(option=>option.value)}}
|
|
71
|
+
function flowTextAreaInput(context,field){const area=h("textarea","cx-flow-input cx-flow-textarea");area.rows=field.rows;area.value=context.value??"";area.readOnly=context.readOnly;if(field.placeholder)area.placeholder=field.placeholder;return{element:area,getValue:()=>area.value===""?null:area.value}}
|
|
72
|
+
function flowListInput(context,field,labels){const group=h("div","cx-flow-list"),rows=h("ul","cx-flow-list-items"),add=h("button","cx-flow-btn cx-flow-list-add");group.setAttribute("role","group");add.type="button";setText(add,"+ "+labels.flowAdd);add.setAttribute("aria-label",fill(labels.flowAddItem,{field:field.label}));const renumber=()=>[...rows.children].forEach((row,index)=>{const input=row.firstChild,remove=row.lastChild;input.setAttribute("aria-label",fill(labels.flowItemLabel,{field:field.label,index:index+1}));remove.setAttribute("aria-label",fill(labels.flowRemoveItem,{item:input.value.trim()||fill(labels.flowItemLabel,{field:field.label,index:index+1})}))});const row=(value,focus)=>{const item=h("li","cx-flow-list-item"),input=h("input","cx-flow-input"),remove=h("button","cx-flow-icon-btn");input.type="text";input.value=value;input.autocomplete="off";input.readOnly=context.readOnly;if(field.placeholder)input.placeholder=field.placeholder;remove.type="button";setText(remove,"×");remove.hidden=context.readOnly;remove.addEventListener("click",()=>{const next=item.nextSibling?.firstChild||item.previousSibling?.firstChild;item.remove();if(!rows.children.length)row("",false);renumber();(next||add).focus();group.dispatchEvent(new Event("change",{bubbles:true}))});input.addEventListener("keydown",event=>{if(event.key==="Enter"&&!event.isComposing){event.preventDefault();if(!context.readOnly)row("",true)}});input.addEventListener("input",renumber);input.addEventListener("paste",event=>{if(context.readOnly){event.preventDefault();return}
|
|
73
|
+
const text=event.clipboardData?.getData("text")||"",lines=text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
74
|
+
if(lines.length<2)return;event.preventDefault();input.value=lines[0];let after=item;for(const line of lines.slice(1,1000)){const added=row(line,false);rows.insertBefore(added,after.nextSibling);after=added}
|
|
75
|
+
renumber();group.dispatchEvent(new Event("change",{bubbles:true}))});item.append(input,remove);rows.appendChild(item);if(focus){renumber();input.focus()}
|
|
76
|
+
return item};for(const value of context.value?.length?context.value:[""])row(value,false);add.hidden=context.readOnly;add.addEventListener("click",()=>row("",true));group.append(rows,add);renumber();return{element:group,control:group,group:true,focus:()=>(rows.querySelector("input[aria-invalid]")||rows.querySelector("input"))?.focus(),flag:bad=>rows.querySelectorAll("input").forEach(input=>{if(bad(input.value.trim()))input.setAttribute("aria-invalid","true");else input.removeAttribute("aria-invalid")}),getValue:()=>[...rows.querySelectorAll("input")].map(input=>input.value.trim()).filter(Boolean)}}
|
|
77
|
+
function flowSecretInput(context,field,labels,stored){const box=h("div","cx-flow-secret"),input=h("input","cx-flow-input");input.type="password";input.autocomplete="new-password";input.spellcheck=false;input.readOnly=context.readOnly;box.appendChild(input);if(field.reveal){const reveal=h("button","cx-flow-btn cx-flow-reveal");reveal.type="button";reveal.setAttribute("aria-pressed","false");reveal.setAttribute("aria-label",fill(labels.flowSecretReveal,{field:field.label}));setText(reveal,labels.flowSecretShow);reveal.addEventListener("click",()=>{const shown=input.type==="password";input.type=shown?"text":"password";reveal.setAttribute("aria-pressed",String(shown));setText(reveal,shown?labels.flowSecretHide:labels.flowSecretShow)});box.appendChild(reveal)}
|
|
78
|
+
return{element:box,control:input,secret:true,hint:stored?labels.flowSecretHint:null,conceal:()=>{input.type="password";box.querySelector(".cx-flow-reveal")?.setAttribute("aria-pressed","false")},getValue:()=>input.value,clear:()=>{input.value=""}}}
|
|
79
|
+
function flowDurationInput(context,field,labels){const group=h("div","cx-flow-duration"),amount=h("input","cx-flow-input cx-flow-amount"),unit=h("select","cx-flow-input cx-flow-unit");group.setAttribute("role","group");amount.type="text";amount.inputMode="numeric";amount.autocomplete="off";amount.readOnly=context.readOnly;unit.disabled=context.readOnly;unit.setAttribute("aria-label",labels.flowUnit);amount.setAttribute("aria-labelledby",context.id+"-label");for(const name of field.units)unit.add(new Option(labels["flowUnit"+name[0].toUpperCase()+ name.slice(1)],name));const seconds=context.value==null?null:flowSeconds(context.value);const start=seconds==null?field.units[Math.min(1,field.units.length-1)]:seconds===0?field.units[0]:[...field.units].reverse().find(name=>seconds%FLOW_UNITS[name]===0)||field.units[0];unit.value=start;if(seconds!=null)amount.value=numberFormat(context.locale,0,0).format(seconds / FLOW_UNITS[start]);
|
|
80
|
+
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
|
+
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
|
+
FLOW_CALLBACKS.some(name=>options[name]!=null&&typeof options[name]!=="function")||options.editors!=null&&!flowIs(options.editors)||
|
|
83
|
+
options.values!=null&&!flowIs(options.values))flowFail("invalid flow options");this.host=host;this.options=options;this.hooks=hooks;this.model=flowRead(options.flow);this.labels=flowLabels(this.model.labels,options.labels);this.editors=Object.assign({},editors,options.editors);this.id="cx-flow-" + ++FLOW_SEQUENCE;this.lang=host.closest("[lang]")?.lang||document.documentElement.lang||"";this.lifetime=new AbortController();this.fields=new Map();this.sections=new Map();this.visibility=new Map();this.secrets=new Map();this.errors=new Map();this.hostErrors=new Map();this.hostStepErrors=new Map();this.stepErrors=new Map();this.completed=new Set();this.banner=null;this.phase="form";this.returnToReview=false;this.generation=0;this.answerRevision=0;try{this.begin(options.values);this.build()}
|
|
84
|
+
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
|
+
begin(values){const model=this.model;this.values=new Map();for(const field of model.fields.values()){if(field.type==="secret")continue;const initial=model.initial?Object.hasOwn(model.initial,field.id)?model.initial[field.id]:null:field.start;this.values.set(field.id,flowCopy(initial))}
|
|
87
|
+
for(const[id,value]of Object.entries(values||{}))this.assign(id,value);this.startValues=new Map([...this.values].map(([id,value])=>[id,flowCopy(value)]));this.secrets.clear();this.errors.clear();this.hostErrors.clear();this.hostStepErrors.clear();this.stepErrors.clear();this.showAll=undefined;this.reviewStale=true;this.completed.clear();this.visibility.clear();this.banner=null;this.returnToReview=false;this.step=this.visibleSteps()[0];if(model.initial)for(const step of this.visibleSteps())if(step.type==="form"&&this.stepFields(step).every(field=>!this.problemNow(field)))this.completed.add(step.id)}
|
|
88
|
+
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
|
+
this.values.set(id,flowCopy(value))}
|
|
90
|
+
keeps(field){return field.type==="secret"&&this.model.stored.has(field.id)&&field.keepWhenBlank!==false}
|
|
91
|
+
read(id){const field=this.model.fields.get(id);return field&&this.visible(field)?this.values.get(id)??null:null}
|
|
92
|
+
stepVisible(step){return flowTest(step.when,id=>this.read(id))}
|
|
93
|
+
visible(field){if(this.visibility.has(field.id))return this.visibility.get(field.id);const shown=this.stepVisible(field.step)&&(!field.section||flowTest(field.section.when,id=>this.read(id)))&&flowTest(field.when,id=>this.read(id));this.visibility.set(field.id,shown);return shown}
|
|
94
|
+
visibleSteps(){return this.model.steps.filter(step=>this.stepVisible(step))}
|
|
95
|
+
stepFields(step){return step.blocks.flatMap(block=>block.fields).filter(field=>this.visible(field))}
|
|
96
|
+
reviewStep(){const last=this.model.steps[this.model.steps.length-1];return last.type==="review"?last:null}
|
|
97
|
+
getValues(){const values={};for(const field of this.model.fields.values())if(field.type!=="secret"&&this.visible(field))values[field.id]=flowCopy(this.values.get(field.id)??null);return values}
|
|
98
|
+
answersChanged(){this.answerRevision++;this.retireOperation()}
|
|
99
|
+
beginOperation(){let cancel;const cancelled=new Promise(resolve=>{cancel=resolve});const generation=this.generation,revision=this.answerRevision,ticket={cancel};ticket.current=()=>!this.destroyed&&generation===this.generation&&revision===this.answerRevision&&this.operation===ticket;ticket.wait=work=>Promise.race([work,cancelled]);this.operation=ticket;this.busy=true;return ticket}
|
|
100
|
+
retireOperation(render=true){const ticket=this.operation;if(!ticket)return;this.operation=null;ticket.close?.();this.busy=false;ticket.cancel();if(render&&!this.destroyed&&this.phase==="form"&&this.actions)this.renderActions()}
|
|
101
|
+
changed(field,source){const fv=this.fields.get(field.id);if(!fv||this.destroyed||field.readOnly)return;const result=fv.read();if(fv.pending!==(result.error||null))this.answersChanged();fv.pending=result.error||null;if(result.error)this.completed.delete(field.step.id);if(field.type==="secret"){const before=this.secrets.get(field.id)??null;if(result.value)this.secrets.set(field.id,result.value);else this.secrets.delete(field.id);if(before!==(result.value||null)){this.answersChanged();this.forgetHost(field);this.emit("flowValueChanged",{field:field.id,source:"edit"},"onValueChange")}}else if(!result.error){const previous=this.values.get(field.id)??null;if(!flowSameValue(previous,result.value)){this.answersChanged();this.values.set(field.id,result.value);this.forgetHost(field);this.emit("flowValueChanged",{field:field.id,value:flowCopy(result.value),previous:flowCopy(previous),source:"edit"},"onValueChange")}}
|
|
102
|
+
if(source!=="blur"&&this.errors.has(field.id))this.checkField(field);this.refreshVisibility()}
|
|
103
|
+
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
|
+
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
|
+
else this.renderRail()}
|
|
107
|
+
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
|
+
this.refreshVisibility();this.refreshReview()}
|
|
109
|
+
refreshReview(){if(this.step.type!=="review"||this.phase!=="form"||this.reviewChecking||this.reviewQueued)return;this.reviewQueued=true;queueMicrotask(()=>{this.reviewQueued=false;if(this.destroyed||this.step.type!=="review")return;this.reviewStale=true;this.render();this.stage.querySelector(".cx-flow-step-title")?.focus({preventScroll:true})})}
|
|
110
|
+
isDirty(){return this.secrets.size>0||[...this.values].some(([id,value])=>!flowSameValue(value,this.startValues.get(id)))}
|
|
111
|
+
getStep(){return this.step.id}
|
|
112
|
+
getState(){return{step:this.step.id,values:Object.fromEntries([...this.values].map(([id,value])=>[id,flowCopy(value)])),completed:[...this.completed]}}
|
|
113
|
+
setState(state){if(!flowIs(state)||state.values!=null&&!flowIs(state.values)||state.completed!=null&&!Array.isArray(state.completed))flowFail("setState takes a flow state");for(const[id,value]of Object.entries(state.values||{})){this.assign(id,value);this.fields.get(id)?.mount(flowCopy(value));this.errors.delete(id);this.hostErrors.delete(id);this.fields.get(id)?.message(null)}
|
|
114
|
+
this.completed=new Set((state.completed||[]).filter(id=>this.model.steps.some(step=>step.id===id)));this.refreshVisibility();const target=this.model.steps.find(step=>step.id===state.step);return this.go(target&&this.stepVisible(target)?target:this.visibleSteps()[0],"jump",true)}
|
|
115
|
+
reset(){this.generation++;this.answersChanged();this.busy=false;this.checks?.clear();this.reviewTicket=null;this.reviewChecking=false;for(const fv of this.fields.values())fv.destroy();this.fields.clear();this.sections.clear();this.begin(this.options.values);this.phase="form";this.render();return true}
|
|
116
|
+
emit(name,detail,callback,cancelable=false){const event=new CustomEvent("canopyx:"+name,{detail,bubbles:true,cancelable});const go=this.host.dispatchEvent(event);if(callback&&typeof this.options[callback]==="function")try{this.options[callback](detail)}catch(error){this.report(error,callback)}
|
|
117
|
+
return go}
|
|
118
|
+
report(error,hook){this.host.dispatchEvent(new CustomEvent("canopyx:error",{detail:{error,hook},bubbles:true}))}
|
|
119
|
+
async hook(name,run){try{return{value:await run()}}
|
|
120
|
+
catch(error){this.report(error,name);return{failed:error&&error.message?error.message:String(error)}}}}
|
|
121
|
+
const FLOW_NARROW=640;Object.assign(FlowView.prototype,{build(){const root=this.root=h("div","cx-flow"),head=h("header","cx-flow-head"),title=h("h2","cx-flow-title");root.dataset.cxDensity=this.model.density;setText(title,this.model.title);title.id=this.id+"-title";head.appendChild(title);if(this.model.description){const description=h("p","cx-flow-desc");setText(description,this.model.description);head.appendChild(description)}
|
|
122
|
+
root.setAttribute("aria-labelledby",title.id);root.setAttribute("role","group");this.layout=h("div","cx-flow-layout");this.rail=h("div","cx-flow-rail");this.railList=h("ol","cx-flow-marks");this.rail.appendChild(this.railList);this.main=h("div","cx-flow-main");this.stage=h("div","cx-flow-stage");this.actions=h("div","cx-flow-actions");this.main.append(this.stage,this.actions);this.live=h("div","cx-sr-only");this.live.setAttribute("aria-live","polite");this.layout.append(this.rail,this.main);root.append(head,this.layout,this.live);this.host.appendChild(root);const signal=this.lifetime.signal;root.addEventListener("input",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("change",event=>this.fieldEvent(event,"edit"),{signal});root.addEventListener("focusout",event=>this.fieldEvent(event,"blur"),{signal});root.addEventListener("keydown",event=>this.key(event),{signal});this.narrow=false;if(global.ResizeObserver){this.resize=new ResizeObserver(()=>this.measure());this.resize.observe(this.host)}
|
|
123
|
+
this.measure(true);this.render()},fieldEvent(event,source){const wrapper=event.target.closest?.("[data-cx-field]");const field=wrapper&&this.model.fields.get(wrapper.dataset.cxField);if(!field||this.phase!=="form")return;if(source==="blur"){if(wrapper.contains(event.relatedTarget))return;this.changed(field,"blur");this.checkField(field);return}
|
|
124
|
+
this.changed(field,source)},measure(first){const width=this.host.clientWidth||global.innerWidth,narrow=width<FLOW_NARROW;if(!first&&width===this.width)return;const changed=narrow!==this.narrow||this.railTooWide&&width>this.width;this.width=width;this.narrow=narrow;if(changed){this.railTooWide=false;if(!first)this.render()}
|
|
125
|
+
else if(!first)this.fitRail()},orientation(){if(this.visibleSteps().length<=1)return "single";if(this.narrow)return "stack";return this.model.orientation==="auto"?(this.visibleSteps().length>6||this.railTooWide?"vertical":"horizontal"):this.model.orientation},fitRail(){if(this.model.orientation!=="auto"||this.railTooWide||this.orientation()!=="horizontal"||!this.rail.clientWidth)return;if(this.rail.scrollWidth>this.rail.clientWidth+1){this.railTooWide=true;this.render()}},render(){const active=document.activeElement;const steps=this.visibleSteps();if(this.phase==="form"&&!steps.includes(this.step)){const from=this.step;this.step=steps.find(step=>this.model.steps.indexOf(step)>this.model.steps.indexOf(from))||steps[steps.length-1];this.reviewStale=true;this.emit("flowStepChanged",{from:from.id,to:this.step.id},"onStepChange")}
|
|
126
|
+
const orientation=this.orientation();this.layout.className="cx-flow-layout cx-flow-"+orientation;this.root.classList.toggle("cx-flow-narrow",this.narrow);if(orientation==="stack"||orientation==="single"){this.rail.removeAttribute("role");this.rail.removeAttribute("aria-label")}
|
|
127
|
+
else{this.rail.setAttribute("role","navigation");this.rail.setAttribute("aria-label",this.labels.flowSteps)}
|
|
128
|
+
this.renderStage();this.renderRail();this.renderActions();if(active&&active!==document.activeElement&&active.isConnected&&this.root.contains(active))active.focus({preventScroll:true});this.fitRail()},stepState(step,steps){if(this.phase!=="form")return "complete";if(step===this.step)return "current";const errors=this.stepErrorCount(step);if(errors)return "error";if(this.completed.has(step.id))return "complete";return this.reachable(step,steps)?"incomplete":"locked"},reachable(step,steps=this.visibleSteps()){if(this.model.mode==="free")return true;for(const before of steps){if(before===step)return true;if(before.type==="form"&&!this.completed.has(before.id))return false}
|
|
129
|
+
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
|
+
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
|
+
state==="complete"?labels.flowStepComplete:state==="locked"?labels.flowStepLocked:step.optional?labels.flowStepOptional:labels.flowStepNotStarted);note.className="cx-flow-mark-state" +(state==="locked"||state==="incomplete"?" cx-sr-only":"");text.append(title,note);const signature=state+"\u0000"+number.textContent+"\u0000"+note.textContent+"\u0000"+title.textContent;if(button._cxSignature!==signature){button._cxSignature=signature;button.replaceChildren(number,text)}
|
|
132
|
+
if(state==="current")button.setAttribute("aria-current","step");else button.removeAttribute("aria-current");if(state==="locked"||this.phase!=="form")button.setAttribute("aria-disabled","true");else button.removeAttribute("aria-disabled");if(stack&&step===this.step&&this.phase==="form"&&this.main.parentNode!==item)item.appendChild(this.main)});if(!stack&&this.main.parentNode!==this.layout)this.layout.appendChild(this.main);if(stack&&this.phase!=="form")this.railList.after(this.main)},renderActions(){const labels=this.labels,steps=this.visibleSteps(),at=steps.indexOf(this.step),last=at===steps.length-1;this.actions.replaceChildren();this.actions.hidden=this.phase!=="form";if(this.phase!=="form")return;const back=h("button","cx-flow-btn cx-flow-back"),next=h("button","cx-flow-btn cx-primary cx-flow-next");back.type=next.type="button";setText(back,labels.flowBack);back.hidden=at<=0;back.addEventListener("click",()=>this.back().catch(error=>this.report(error,"back")));setText(next,last?this.model.submitLabel||labels.flowSubmit:labels.flowNext);next.addEventListener("click",()=>(last?this.submit():this.next()).catch(error=>this.report(error,last?"submit":"next")));if(this.busy){next.disabled=back.disabled=true;next.setAttribute("aria-busy","true");if(last)setText(next,labels.flowSubmitting)}
|
|
133
|
+
const spacer=h("span","cx-flow-spacer");this.actions.append(back,spacer,next)},key(event){if(event.defaultPrevented||event.isComposing||this.phase!=="form")return;const target=event.target;if(event.key==="Enter"&&(event.ctrlKey||event.metaKey)&&this.step.type==="review"){event.preventDefault();this.submit().catch(error=>this.report(error,"submit"));return}
|
|
134
|
+
if(event.key!=="Enter"||event.altKey||event.ctrlKey||event.shiftKey||target.tagName!=="INPUT"||/^(button|submit|checkbox|radio|file)$/.test(target.type) ||
|
|
135
|
+
target.closest(".cx-flow-list")||!target.closest(".cx-flow-stage"))return;event.preventDefault();const steps=this.visibleSteps();(steps.indexOf(this.step)===steps.length-1?this.submit():this.next()).catch(error=>this.report(error,"next"))},announce(text){setText(this.live,"");setTimeout(()=>setText(this.live,text),30)}});Object.assign(FlowView.prototype,{renderStage(){const step=this.step;if(this.phase==="result"||this.phase==="progress"){this.stage.replaceChildren(this.panel);return}
|
|
136
|
+
if(step.type==="review"){if(this.reviewStale||!this.reviewNode){this.reviewNode=this.reviewSection();this.reviewStale=false}
|
|
137
|
+
if(this.stage.firstChild!==this.reviewNode)this.stage.replaceChildren(this.reviewNode);return}
|
|
138
|
+
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
|
+
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
|
+
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
|
+
for(const field of block.fields){const fv=flowFieldView(this,field);this.fields.set(field.id,fv);fv.mount(field.type==="secret"?null:flowCopy(this.values.get(field.id)??null));fv.element.hidden=!this.visible(field);if(this.errors.has(field.id))fv.message(this.errors.get(field.id));grid.appendChild(fv.element)}
|
|
142
|
+
element.appendChild(grid);element.hidden=!block.fields.some(field=>this.visible(field));section._cxBlocks.push({element,fields:block.fields});section.appendChild(element)}
|
|
143
|
+
return section},async go(target,direction,quiet){if(!target||this.destroyed)return false;const from=this.step,generation=this.generation;if(target!==from&&!this.emit("flowStepChanging",{from:from.id,to:target.id,direction},null,true))return false;if(this.destroyed||this.generation!==generation||this.step!==from)return false;for(const fv of this.fields.values())fv.instance?.conceal?.();this.step=target;this.reviewStale=true;for(const field of target.blocks?.flatMap(block=>block.fields)||[])this.fields.get(field.id)?.message(this.errors.get(field.id)??null);this.render();const steps=this.visibleSteps(),heading=this.stage.querySelector(".cx-flow-step-title");if(!quiet){heading?.focus({preventScroll:true});this.root.scrollIntoView?.({block:"nearest"});this.announce(fill(this.labels.flowStepAnnounce,{current:steps.indexOf(target)+ 1,count:steps.length,title:target.title||this.labels.flowReviewTitle}))}
|
|
144
|
+
if(target!==from)this.emit("flowStepChanged",{from:from.id,to:target.id},"onStepChange");return true},async next(){if(this.busy||this.phase!=="form")return false;const step=this.step,steps=this.visibleSteps(),pressed=document.activeElement,generation=this.generation;if(step.type==="form"){const ticket=this.beginOperation();this.renderActions();let result;try{result=await ticket.wait(this.validateStep(step,true,ticket.current));if(!ticket.current())return false}finally{if(ticket.current())this.retireOperation()}
|
|
145
|
+
if(this.destroyed||generation!==this.generation||this.step!==step||result.moved)return false;if(!result.valid){const stayed=document.activeElement===pressed||!this.root.contains(document.activeElement);this.renderRail();this.renderActions();this.showSummary(step,stayed);return false}
|
|
146
|
+
const summary=this.sections.get(step.id)?._cxSummary;if(summary)summary.hidden=true;this.completed.add(step.id)}else this.completed.add(step.id);const review=this.reviewStep();const target=this.returnToReview&&review&&this.stepVisible(review)?review:steps[steps.indexOf(step)+ 1];if(target===review)this.returnToReview=false;return this.go(target,"forward")},async back(){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),at=steps.indexOf(this.step);return at>0?this.go(steps[at-1],"back"):false},async goTo(id){if(this.busy||this.phase!=="form")return false;const steps=this.visibleSteps(),target=steps.find(step=>step.id===id);if(!target)return false;const open=this.reachable(target,steps)?target:steps.find(step=>step.type==="form"&&!this.completed.has(step.id))||target;return this.go(open,steps.indexOf(open)<steps.indexOf(this.step)?"back":"forward")},change(field){this.returnToReview=true;return this.go(field.step,"back").then(moved=>{if(moved){const fv=this.fields.get(field.id);if(fv)fv.focus()}
|
|
147
|
+
return moved})}});const FLOW_ITEM_RULES={email:value=>/^[^\s@]+@[^\s@]+\.[^\s@.]+$/.test(value),
|
|
148
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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})}
|
|
154
|
+
return null}
|
|
155
|
+
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
|
+
},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);
|
|
157
|
+
if(answer.failed){result.errors.push(answer.failed);return result}
|
|
158
|
+
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
|
+
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
|
+
result=this.bucket(await this.hook("validateStep",()=>this.hooks.validateStep(step.id,this.getValues())));if(this.destroyed||this.generation!==generation||!current())return{valid:false,errors:[],warnings:[],moved:true};if(this.answerKey(step)===before){stable=true;break}}
|
|
161
|
+
if(this.destroyed||!current())return{valid:false,errors:[],warnings:[]};if(!stable)return{valid:false,errors:[{step:step.id,message:this.labels.flowNotSubmitted}],warnings:[],moved:true};const failing=[];fields.forEach((field,index)=>{const local=messages[index]||result.fields[field.id]||null,message=local||this.hostErrors.get(field.id)||null;if(show||!message)this.setError(field,message);else this.errors.set(field.id,message);if(local)failing.push({field:field.id,step:step.id,message:local})});if(result.errors.length)this.stepErrors.set(step.id,result.errors.join(" "));else this.stepErrors.delete(step.id);const errors=failing;for(const message of result.errors)errors.push({step:step.id,message});return{valid:stable&&!errors.length,errors,warnings:result.warnings.map(message=>({step:step.id,message})),moved}},async validateAll(current=()=>true){const generation=this.generation;for(let round=0;round<4;round++){const revision=this.answerRevision,result=await this.validateAllOnce(generation,current);if(this.destroyed||generation!==this.generation||!current())return{valid:false,errors:[],warnings:[]};if(revision===this.answerRevision)return result}
|
|
162
|
+
return{valid:false,errors:[{message:this.labels.flowNotSubmitted}],warnings:[]}},async validateAllOnce(generation,current){const revision=this.answerRevision,results=[],steps=this.visibleSteps().filter(step=>step.type==="form");for(const step of steps){results.push(await this.validateStep(step,false,current));if(this.destroyed||generation!==this.generation||revision!==this.answerRevision||!current())return{valid:false,errors:[],warnings:[]}}
|
|
163
|
+
if(this.destroyed||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
|
+
errors.push(...result.errors.map(message=>({message})));warnings.push(...result.warnings.map(message=>({message})))}
|
|
165
|
+
return{valid:results.every(result=>result.valid)&&!errors.length,errors,warnings}},async validate(stepId){if(stepId===undefined){const result=await this.validateAll();this.renderRail();return result}
|
|
166
|
+
const step=this.model.steps.find(item=>item.id===stepId);if(!step)flowFail("there is no step '"+stepId+"'");const result=step.type==="form"&&this.stepVisible(step)?await this.validateStep(step,true):{valid:true,errors:[],warnings:[]};this.renderRail();return{valid:result.valid,errors:result.errors,warnings:result.warnings}},setFieldError(id,message){const field=this.model.fields.get(id);if(!field||message!=null&&(typeof message!=="string"||!/\S/.test(message))) flowFail("setFieldError takes a field id and a message or null");
|
|
167
|
+
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
|
+
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
|
+
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)}
|
|
172
|
+
if(result.warnings.length){const warnings=h("p","cx-flow-banner-warn");setText(warnings,fill(labels.flowWarnings,{count:result.warnings.length}));status.appendChild(warnings);for(const warning of result.warnings){const item=h("li","cx-flow-warning");setText(item,warning.message);list.appendChild(item)}}
|
|
173
|
+
if(list.children.length)status.appendChild(list)},fillReview(lists,changesOnly){const labels=this.labels;lists.replaceChildren();let shown=0;for(const step of this.visibleSteps()){if(step.type!=="form")continue;const fields=this.stepFields(step).filter(field=>!changesOnly||this.isChanged(field));const stepMessage=this.stepErrors.get(step.id)||this.hostStepErrors.get(step.id);if(!fields.length&&!stepMessage)continue;const group=h("section","cx-flow-review-group"),heading=h("h4","cx-flow-review-title"),list=h("dl","cx-flow-answers");heading.id=this.id+"-r-"+step.id;setText(heading,step.title);group.setAttribute("aria-labelledby",heading.id);group.appendChild(heading);if(stepMessage){const error=h("p","cx-flow-error");setText(error,stepMessage);group.appendChild(error)}
|
|
174
|
+
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
|
+
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
|
+
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)}
|
|
177
|
+
row.append(term,value,action);list.appendChild(row);shown++}
|
|
178
|
+
group.appendChild(list);lists.appendChild(group)}
|
|
179
|
+
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
|
+
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
|
+
const value=flowCopy(this.values.get(field.id)??null),from=flowNormal(this.initialOf(field)),to=flowNormal(value);values[field.id]=value;if(edit&&this.isChanged(field))changes.push(Object.assign({field:field.id},field.path?{path:field.path}:{},{from:flowCopy(from),to:flowCopy(to)}));if(!field.path)continue;if(edit){if(!this.isChanged(field))continue;patch.push(to===null?{op:"remove",path:flowPointer(field.path)}:{op:from===null?"add":"replace",path:flowPointer(field.path),value:flowCopy(to)})}else if(to!==null)patch.push({op:"add",path:flowPointer(field.path),value:flowCopy(to)})}
|
|
182
|
+
const proposal={kind:"flowSubmission",flow:model.id,version:1,mode:edit?"edit":"create"};if(model.baseline!==null)proposal.baseline=model.baseline;return Object.assign(proposal,{values,secrets,changes,patch})},async submit(){if(this.busy||this.phase!=="form")return null;const ticket=this.beginOperation(),current=ticket.current,wait=ticket.wait;this.banner=null;this.hostErrors.clear();this.hostStepErrors.clear();this.renderActions();try{const result=await wait(this.validateAll(current));if(!current())return null;if(!result.valid){const review=this.reviewStep();if(review&&this.stepVisible(review)){if(this.step!==review)await this.go(review,"forward");else{this.reviewStale=true;this.render()}}
|
|
183
|
+
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
|
+
return null}
|
|
185
|
+
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}
|
|
186
|
+
if(!answered&&typeof this.options.onSubmit==="function"){const value=await wait(this.hook("onSubmit",()=>this.options.onSubmit(detail)));if(!current()){close();return null}
|
|
187
|
+
if(value.failed){answered=true;answer={status:"error",message:value.failed}}
|
|
188
|
+
else if(value.value!==undefined){answered=true;answer=value.value}}
|
|
189
|
+
if(!current()){close();return null}
|
|
190
|
+
if(!answered&&typeof this.hooks.submit==="function"){const value=await wait(this.hook("submit",()=>this.hooks.submit(detail)));if(!current()){close();return null}
|
|
191
|
+
answered=true;answer=value.failed?{status:"error",message:value.failed}:value.value}
|
|
192
|
+
if(!current()){close();return null}
|
|
193
|
+
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
|
+
settled=value.failed?{status:"error",message:value.failed}:this.answerOf(value.value)}
|
|
195
|
+
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}
|
|
197
|
+
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
|
+
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
|
+
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)}
|
|
200
|
+
return banner},setProgress(progress){if(this.phase!=="progress"||!flowIs(progress))return false;const bar=this.panel.querySelector(".cx-flow-bar"),note=this.panel.querySelector(".cx-flow-panel-text");if(Number.isFinite(progress.total)&&progress.total>0&&Number.isFinite(progress.done)){const done=Math.max(0,Math.min(progress.total,progress.done));bar.max=progress.total;bar.value=done;bar.setAttribute("aria-valuetext",fill(this.labels.flowProgress,{done:numberFormat(this.lang,0,0).format(done),total:numberFormat(this.lang,0,0).format(progress.total)}))}
|
|
201
|
+
if(typeof progress.message==="string")setText(note,progress.message);return true},complete(value){if(this.phase!=="progress")return false;this.settle(this.answerOf(value));return true}});function flowSafeHref(href){if(href.length>2048||/[\u0000-\u0020\u007f\\]/.test(href)) return false;
|
|
202
|
+
if(href.startsWith("//"))return false;let url;try{url=new URL(href,document.baseURI)}catch(error){return false}
|
|
203
|
+
if(url.protocol==="mailto:")return /^mailto:/i.test(href);
|
|
204
|
+
if(!/^[a-z][a-z\d+.-]*:/i.test(href)) { const page = new URL(document.baseURI); return url.protocol === page.protocol && url.origin === page.origin; }
|
|
205
|
+
return url.protocol==="http:"||url.protocol==="https:"}
|
|
206
|
+
Object.assign(FlowView.prototype,{panelShell(kind,title){const panel=h("section","cx-flow-step cx-flow-panel cx-flow-"+kind),heading=h("h3","cx-flow-step-title"),mark=h("span","cx-flow-panel-mark");heading.id=this.id+"-panel";heading.tabIndex=-1;mark.setAttribute("aria-hidden","true");setText(mark,kind==="result"?"✓":"…");heading.append(mark,title);panel.setAttribute("aria-labelledby",heading.id);panel.appendChild(heading);return panel},resultPanel(answer){const labels=this.labels,panel=this.panelShell("result",labels.flowSaved);if(answer.message){const text=h("p","cx-flow-panel-text");setText(text,answer.message);panel.appendChild(text)}
|
|
207
|
+
if(answer.result.length){const list=h("dl","cx-flow-answers cx-flow-results");for(const row of answer.result){const item=h("div","cx-flow-answer"),term=h("dt"),value=h("dd","cx-flow-answer-value"),output=h("output","cx-flow-result-value");setText(term,row.label);setText(output,row.value);term.id=this.id+"-result-"+list.children.length;value.appendChild(output);if(row.once){item.classList.add("cx-flow-once");const note=h("span","cx-flow-was"),copy=h("button","cx-flow-btn"),saved=h("label","cx-flow-check"),box=h("input"),text=h("span");setText(note,labels.flowShownOnce);copy.type="button";setText(copy,labels.flowCopy);copy.setAttribute("aria-describedby",term.id);copy.addEventListener("click",()=>{const done=()=>{setText(copy,labels.flowCopied);this.announce(labels.flowCopied)};const select=()=>global.getSelection?.().selectAllChildren(output);if(!global.navigator?.clipboard){select();return}
|
|
208
|
+
global.navigator.clipboard.writeText(row.value).then(done,error=>{select();this.report(error,"copy")})});box.type="checkbox";box.className="cx-flow-saved-it";setText(text,labels.flowSavedIt);saved.append(box,text);value.append(note,copy,saved)}
|
|
209
|
+
item.append(term,value);list.appendChild(item)}
|
|
210
|
+
panel.appendChild(list)}
|
|
211
|
+
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
|
+
panel.appendChild(links)}
|
|
213
|
+
const actions=h("div","cx-flow-actions cx-flow-panel-actions"),again=h("button","cx-flow-btn");again.type="button";setText(again,labels.flowStartAgain);const acknowledged=()=>{again.disabled=[...panel.querySelectorAll(".cx-flow-saved-it")].some(box=>!box.checked)};panel.addEventListener("change",acknowledged);acknowledged();again.addEventListener("click",()=>{this.panel=null;this.reset();this.stage.querySelector(".cx-flow-step-title")?.focus()});actions.appendChild(again);panel.appendChild(actions);return panel},progressPanel(answer){const panel=this.panelShell("progress",this.labels.flowAccepted),text=h("p","cx-flow-panel-text"),bar=h("progress","cx-flow-bar");setText(text,answer.message||"");bar.setAttribute("aria-labelledby",panel.firstChild.id);panel.append(text,bar);return panel}});function flowCreate(host,options,editors){const view=new FlowView(host,options,editors);const impl={ready:Promise.resolve().then(()=>{if(!view.destroyed)host.setAttribute("data-cx-ready","");view.emit("flowReady",{flow:view.model.id,step:view.step.id},"onReady");return impl}),next:()=>view.next(),back:()=>view.back(),goTo:id=>view.goTo(id),getStep:()=>view.getStep(),getValues:()=>view.getValues(),setValues:values=>view.setValues(values),getState:()=>view.getState(),setState:state=>view.setState(state),isDirty:()=>view.isDirty(),reset:()=>view.reset(),validate:id=>view.validate(id),setFieldError:(id,message)=>view.setFieldError(id,message),submit:()=>view.submit(),setProgress:progress=>view.setProgress(progress),complete:answer=>view.complete(answer),destroy:()=>view.destroy()};return impl}
|
|
214
|
+
FLOW_IMPL.create=flowCreate;installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow",part])})(typeof window!=="undefined"?window:globalThis);
|