@feedmepos/mf-report 5.28.5 → 5.29.0
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/dist/{App-Bc8Mp5vR.js → App-CuBsRwfx.js} +4 -4
- package/dist/{DateRangeSelect-cbSEHMzW.js → DateRangeSelect-CzjlwjuC.js} +1 -1
- package/dist/{Default.vue_vue_type_script_setup_true_lang-BpJFA_ET.js → Default.vue_vue_type_script_setup_true_lang-Di9_F5Yk.js} +1 -1
- package/dist/{DynamicWidget.vue_vue_type_script_setup_true_lang-Dx-cxJP6.js → DynamicWidget.vue_vue_type_script_setup_true_lang-C73R6Fb9.js} +5 -5
- package/dist/{GlobalFilterSelect.vue_vue_type_script_setup_true_lang-Cxrti7_L.js → GlobalFilterSelect.vue_vue_type_script_setup_true_lang-tLRFdLNs.js} +9 -10
- package/dist/{InsightView-Bo0Fb5QG.js → InsightView-BcnmLQ8c.js} +5 -5
- package/dist/{Integration-B4tC2MlL.js → Integration-D4VFT1M8.js} +3 -3
- package/dist/{Integrations-RB8iuvdJ.js → Integrations-v8oF1TL7.js} +2 -2
- package/dist/{Layout-DBCPtUH7.js → Layout-Ds8RSWmi.js} +1 -1
- package/dist/{LocationSelect.vue_vue_type_script_setup_true_lang-w2H9qOgJ.js → LocationSelect.vue_vue_type_script_setup_true_lang-Dk6u8nUH.js} +1638 -1634
- package/dist/{MenuTab.vue_vue_type_script_setup_true_lang-BLVGUSWf.js → MenuTab.vue_vue_type_script_setup_true_lang-BWt7pqFb.js} +5 -5
- package/dist/{NavigationTab.vue_vue_type_script_setup_true_lang-N31ee-oZ.js → NavigationTab.vue_vue_type_script_setup_true_lang-CU00GINo.js} +3853 -3823
- package/dist/OverviewView-1v80g6KX.js +81 -0
- package/dist/{Report-Di6GyP1r.js → Report-DcFB4gM6.js} +3 -3
- package/dist/ReportCanvasView-BQO6Mywc.js +5190 -0
- package/dist/{ReportEditor-BfNmiEVn.js → ReportEditor-Bgcisf1s.js} +7 -7
- package/dist/{ReportView-JWJ-6FCu.js → ReportView-Q_juD8Kr.js} +6 -6
- package/dist/{ReportViewRoute-BMjjJqN2.js → ReportViewRoute-DwStGqyz.js} +2 -2
- package/dist/{Setting-D13NIemB.js → Setting-C9rlM23O.js} +3 -3
- package/dist/{Template-D16bqzrx.js → Template-BZd0NB5w.js} +3 -3
- package/dist/{TileEditor-1Sn9ZSb2.js → TileEditor-B2F9r3RS.js} +6 -6
- package/dist/{TilePageStack.vue_vue_type_script_setup_true_lang-BOXsV7-r.js → TilePageStack.vue_vue_type_script_setup_true_lang-BmbrJAph.js} +2 -2
- package/dist/{TransitionFade-DwJkmMTb.js → TransitionFade-Dkeo9qr0.js} +5 -5
- package/dist/{app-CVBllEjW.js → app-CoQ_GRKo.js} +1819 -1326
- package/dist/app.js +1 -1
- package/dist/assets/{formatChartData.worker-DZp5dSDQ.js → formatChartData.worker-D7sGqQhX.js} +34 -34
- package/dist/assets/{processTableData.worker-Kyuhw4lS.js → processTableData.worker-CltxwQxU.js} +34 -34
- package/dist/{report-Cn4Ik1zm.js → report-Dau_so1j.js} +1 -1
- package/dist/report-canvas-libs/components-1.js +1042 -0
- package/dist/report-canvas-libs/fm-ui-1.js +599 -180
- package/dist/style.css +1 -1
- package/dist/{useRestaurantPermission-C8bLPfk8.js → useRestaurantPermission-CPoHZTxx.js} +1 -1
- package/package.json +2 -1
- package/dist/OverviewView-WKe6Zxe1.js +0 -61
- package/dist/ReportCanvasView-Dg2ItHBO.js +0 -1336
|
@@ -12,56 +12,115 @@
|
|
|
12
12
|
if (FM.ui) return
|
|
13
13
|
|
|
14
14
|
var STYLE_ID = "fm-ui-style"
|
|
15
|
-
//
|
|
15
|
+
// Rebus v2 owns the look: each control renders the anatomy documented in
|
|
16
|
+
// canvas-report-builder/guide/11-rebus-ui. The .fm-* rules below are behavior and test hooks,
|
|
17
|
+
// plus the four things Rebus ships no component for: popover, option row, dialog, report table.
|
|
16
18
|
var CSS = [
|
|
17
|
-
".fm-c{position:relative;display:inline-block;font:inherit;vertical-align:top}",
|
|
19
|
+
".fm-c{position:relative;display:inline-block;max-width:100%;font:inherit;vertical-align:top}",
|
|
18
20
|
".fm-c *{box-sizing:border-box}",
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
".fm-
|
|
23
|
-
".fm-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
21
|
+
// the UA font on <button> beats inheritance, and a Rebus text class only sets the node it is on
|
|
22
|
+
".fm-c button,.fm-dlg button{font-family:inherit}",
|
|
23
|
+
".fm-c>.fm-chip{max-width:100%}",
|
|
24
|
+
".fm-caret{flex:none;color:var(--fm-icon-muted,#6b6c70);font-size:10px;line-height:1}",
|
|
25
|
+
".fm-chip__icon svg,.fm-search-bar__leading svg{display:block;width:var(--fm-icon-size);height:var(--fm-icon-size)}",
|
|
26
|
+
// fixed to the viewport, so a card's overflow clip never cuts it off — popover() places it by its chip;
|
|
27
|
+
// head / list / foot column: only the list scrolls, so search and Apply never leave the panel
|
|
28
|
+
".fm-pop{position:fixed;z-index:20;min-width:248px;",
|
|
29
|
+
"display:flex;flex-direction:column;max-height:min(420px,70vh);overflow:hidden;",
|
|
30
|
+
"padding:var(--fm-spacing-8,8px);",
|
|
31
|
+
"border:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0);",
|
|
32
|
+
"border-radius:var(--fm-radius-md,12px);background:var(--fm-surface-floating,#fff);",
|
|
33
|
+
"box-shadow:var(--fm-shadow-xl,0 8px 16px rgb(28 28 30/.12))}",
|
|
28
34
|
".fm-pop[hidden]{display:none}",
|
|
29
|
-
".fm-
|
|
30
|
-
".fm-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
".fm-
|
|
36
|
-
".fm-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
".fm-
|
|
40
|
-
|
|
41
|
-
".fm-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
".fm-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
".fm-
|
|
51
|
-
".fm-
|
|
35
|
+
".fm-pop-list{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}",
|
|
36
|
+
".fm-pop-foot{flex:none;display:flex;align-items:center;justify-content:flex-end;",
|
|
37
|
+
"gap:var(--fm-spacing-8,8px);margin-top:var(--fm-spacing-8,8px);padding-top:var(--fm-spacing-8,8px);",
|
|
38
|
+
"border-top:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
|
|
39
|
+
// the panel is only as wide as the chip until something refuses to shrink; a wrapped Apply is that
|
|
40
|
+
".fm-pop-foot .fm-button{flex:none}",
|
|
41
|
+
".fm-pop-foot .fm-button__label{white-space:nowrap}",
|
|
42
|
+
".fm-opt{display:flex;align-items:center;gap:var(--fm-spacing-8,8px);",
|
|
43
|
+
"padding:var(--fm-spacing-6,6px) var(--fm-spacing-10,10px);min-height:40px;",
|
|
44
|
+
"border-radius:var(--fm-radius-xs,4px);cursor:pointer;white-space:nowrap}",
|
|
45
|
+
".fm-opt:hover{background:var(--fm-bg-subtle,#f6f6f8)}",
|
|
46
|
+
// a row carrying a checkbox already states its selection; only a bare row paints itself
|
|
47
|
+
".fm-opt[aria-selected=true]:not(.fm-opt-check){background:var(--fm-action-primary-bg-subtle,#fff3e6);",
|
|
48
|
+
"color:var(--fm-text-on-brand,#b25300);font-weight:600}",
|
|
49
|
+
// the frame carries no icon registry, so the Rebus checkbox needs its tick supplied inline
|
|
50
|
+
".fm-opt .fm-checkbox{flex:none}",
|
|
51
|
+
// the named All row is a mode, not an option: it sits above the search behind its own rule,
|
|
52
|
+
// so it never reads as a tick row with the box missing
|
|
53
|
+
".fm-pop-all{flex:none;margin-bottom:var(--fm-spacing-8,8px);",
|
|
54
|
+
"padding-bottom:var(--fm-spacing-8,8px);",
|
|
55
|
+
"border-bottom:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
|
|
56
|
+
".fm-opt-all{justify-content:space-between;gap:var(--fm-spacing-12,12px)}",
|
|
57
|
+
".fm-opt-mark{display:none;flex:none;color:var(--fm-icon-brand,#ff7823)}",
|
|
58
|
+
".fm-opt-mark svg{display:block;width:20px;height:20px}",
|
|
59
|
+
".fm-opt-all[aria-selected=true] .fm-opt-mark{display:inline-flex}",
|
|
60
|
+
".fm-opt .fm-checkbox__glyph svg{display:block;width:var(--fm-icon-size);height:var(--fm-icon-size)}",
|
|
61
|
+
".fm-pop>.fm-search-bar{flex:none;margin-bottom:var(--fm-spacing-8,8px)}",
|
|
62
|
+
".fm-empty{padding:var(--fm-spacing-8,8px) var(--fm-spacing-10,10px);color:var(--fm-text-muted,#6b6c70)}",
|
|
63
|
+
// Rebus has no segmented control. Two bordered chips side by side read as two independent
|
|
64
|
+
// buttons; one sunken track with a raised selected pill reads as one choice, and matches the
|
|
65
|
+
// toggle the classic report puts on a chart. 28px + 4px padding = the 36px chip row height.
|
|
66
|
+
".fm-seg{display:inline-flex;align-items:center;gap:2px;max-width:100%;flex-wrap:wrap;",
|
|
67
|
+
"padding:var(--fm-spacing-4,4px);border-radius:var(--fm-radius-sm,8px);",
|
|
68
|
+
"background:var(--fm-bg-subtle,#f6f6f8)}",
|
|
69
|
+
".fm-seg-item{display:inline-flex;align-items:center;justify-content:center;height:28px;",
|
|
70
|
+
"padding:0 var(--fm-spacing-12,12px);border:0;border-radius:var(--fm-radius-xs,4px);",
|
|
71
|
+
"background:none;color:var(--fm-text-muted,#6b6c70);font:inherit;cursor:pointer;white-space:nowrap}",
|
|
72
|
+
".fm-seg-item:hover:not([aria-pressed=true]):not(:disabled){color:var(--fm-text-primary,#1c1c1e)}",
|
|
73
|
+
".fm-seg-item[aria-pressed=true]{background:var(--fm-surface-default,#fff);",
|
|
74
|
+
"color:var(--fm-text-primary,#1c1c1e);box-shadow:var(--fm-shadow-xs,0 1px 2px rgb(28 28 30/.06))}",
|
|
75
|
+
".fm-seg-item:disabled{color:var(--fm-text-disabled,#b2b3b6);cursor:not-allowed}",
|
|
76
|
+
".fm-seg-item:focus-visible{outline:var(--fm-border-width-regular,1.5px) solid var(--fm-border-brand,#ff7823);outline-offset:-2px}",
|
|
77
|
+
// date: presets column beside the month grid, both over one shared Cancel/Apply foot
|
|
78
|
+
".fm-dr-body{display:flex;flex:1 1 auto;min-height:0;gap:var(--fm-spacing-8,8px)}",
|
|
79
|
+
".fm-dr-presets{flex:none;min-width:132px;overflow:auto;",
|
|
80
|
+
"border-right:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0);",
|
|
81
|
+
"padding-right:var(--fm-spacing-4,4px)}",
|
|
82
|
+
".fm-cal{display:flex;flex-direction:column;gap:var(--fm-spacing-8,8px);padding:0 var(--fm-spacing-4,4px)}",
|
|
83
|
+
".fm-cal-head{display:flex;align-items:center;justify-content:space-between;gap:var(--fm-spacing-8,8px)}",
|
|
84
|
+
".fm-cal-title{color:var(--fm-text-primary,#1c1c1e)}",
|
|
85
|
+
".fm-cal-nav{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;",
|
|
86
|
+
"flex:none;padding:0;border:0;border-radius:var(--fm-radius-xs,4px);background:none;",
|
|
87
|
+
"color:var(--fm-icon-primary,#1c1c1e);cursor:pointer}",
|
|
88
|
+
".fm-cal-nav:hover{background:var(--fm-bg-subtle,#f6f6f8)}",
|
|
89
|
+
".fm-cal-nav svg{display:block;width:16px;height:16px}",
|
|
90
|
+
".fm-cal-grid{display:grid;grid-template-columns:repeat(7,32px)}",
|
|
91
|
+
".fm-cal-dow{display:flex;align-items:center;justify-content:center;height:28px;",
|
|
92
|
+
"color:var(--fm-text-muted,#6b6c70)}",
|
|
93
|
+
// the band is the cell and the endpoint is the button, so a range reads as one continuous run
|
|
94
|
+
".fm-cal-cell{display:flex;align-items:center;justify-content:center;width:32px;height:32px}",
|
|
95
|
+
".fm-cal-cell[data-in-range=true]{background:var(--fm-action-primary-bg-subtle,#fff3e6)}",
|
|
96
|
+
".fm-cal-cell[data-edge=start]{border-radius:var(--fm-radius-full,999px) 0 0 var(--fm-radius-full,999px)}",
|
|
97
|
+
".fm-cal-cell[data-edge=end]{border-radius:0 var(--fm-radius-full,999px) var(--fm-radius-full,999px) 0}",
|
|
98
|
+
".fm-cal-cell[data-edge=both]{border-radius:var(--fm-radius-full,999px)}",
|
|
99
|
+
".fm-cal-day{display:flex;align-items:center;justify-content:center;width:32px;height:32px;",
|
|
100
|
+
"padding:0;border:0;border-radius:var(--fm-radius-full,999px);background:none;font:inherit;",
|
|
101
|
+
"color:var(--fm-text-primary,#1c1c1e);cursor:pointer}",
|
|
102
|
+
".fm-cal-day:hover:not([data-selected=true]){background:var(--fm-bg-subtle,#f6f6f8)}",
|
|
103
|
+
".fm-cal-day[data-outside=true]{color:var(--fm-text-disabled,#b2b3b6)}",
|
|
104
|
+
".fm-cal-day[data-today=true]:not([data-selected=true]){",
|
|
105
|
+
"box-shadow:inset 0 0 0 var(--fm-border-width-regular,1.5px) var(--fm-border-brand,#ff7823)}",
|
|
106
|
+
".fm-cal-day[data-selected=true]{background:var(--fm-action-primary-bg,#ff7823);",
|
|
107
|
+
"color:var(--fm-text-inverse,#fff);font-weight:600}",
|
|
108
|
+
// a clickable row is `fmr-row-btn` in the frame's fmr-table rules, the same look a
|
|
109
|
+
// hand-written table gets; `fm-row-btn` stays as this table's own hook
|
|
52
110
|
".fm-dlg{position:fixed;inset:0;z-index:25;display:flex;align-items:center;justify-content:center;",
|
|
53
|
-
"padding:24px;background:
|
|
111
|
+
"padding:24px;background:var(--fm-overlay-scrim-2,rgb(18 18 18/.48))}",
|
|
54
112
|
".fm-dlg[hidden]{display:none}",
|
|
55
113
|
".fm-dlg-card{position:relative;display:flex;flex-direction:column;min-width:320px;",
|
|
56
|
-
"
|
|
57
|
-
"box-shadow:0 12px
|
|
114
|
+
"width:min(720px,100%);max-height:100%;overflow:hidden;border-radius:var(--fm-radius-md,12px);",
|
|
115
|
+
"background:var(--fm-surface-floating,#fff);box-shadow:var(--fm-shadow-2xl,0 12px 24px rgb(28 28 30/.14));outline:none}",
|
|
58
116
|
".fm-dlg-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;",
|
|
59
|
-
"padding:16px 20px;border-bottom:1px solid var(--fm-
|
|
60
|
-
".fm-dlg-title{margin:0;font-size:16px;font-weight:600;color:var(--fm-
|
|
61
|
-
".fm-dlg-subtitle{margin:2px 0 0;font-size:12px;color:var(--fm-muted,#6b6c70)}",
|
|
62
|
-
".fm-dlg-close{border:0;background:none;padding:
|
|
63
|
-
"
|
|
64
|
-
"
|
|
117
|
+
"padding:16px 20px;border-bottom:var(--fm-border-width-thin,1px) solid var(--fm-border-default,#f0f0f0)}",
|
|
118
|
+
".fm-dlg-title{margin:0;font-size:16px;font-weight:600;color:var(--fm-text-primary,#1c1c1e)}",
|
|
119
|
+
".fm-dlg-subtitle{margin:2px 0 0;font-size:12px;color:var(--fm-text-muted,#6b6c70)}",
|
|
120
|
+
".fm-dlg-close{border:0;background:none;padding:0 6px;font-size:14px;",
|
|
121
|
+
"min-width:28px;min-height:28px;display:inline-flex;align-items:center;justify-content:center;",
|
|
122
|
+
"color:var(--fm-icon-muted,#6b6c70);cursor:pointer;border-radius:var(--fm-radius-xs,4px)}",
|
|
123
|
+
".fm-dlg-close:hover{background:var(--fm-bg-subtle,#f6f6f8);color:var(--fm-icon-primary,#1c1c1e)}",
|
|
65
124
|
".fm-dlg-body{overflow:auto;overscroll-behavior:contain;padding:0 20px 16px}",
|
|
66
125
|
// the shell scrims busy sections; a dialog hosted inside one inherits the same treatment
|
|
67
126
|
"section[data-fm-busy] .fm-dlg-card::after{content:'';position:absolute;inset:0;",
|
|
@@ -83,6 +142,86 @@
|
|
|
83
142
|
return node
|
|
84
143
|
}
|
|
85
144
|
|
|
145
|
+
var SVG_NS = "http://www.w3.org/2000/svg"
|
|
146
|
+
|
|
147
|
+
// Rebus draws every glyph with FmIcon, which the frame has no registry for, and the CSP
|
|
148
|
+
// (`font-src data:`) rules an icon font out — so the handful the controls need are drawn here.
|
|
149
|
+
var ICONS = {
|
|
150
|
+
calendar:
|
|
151
|
+
"M6.5 2.5v3|M13.5 2.5v3|M3.25 8.25h13.5|M4.75 4.5h10.5a1.5 1.5 0 0 1 1.5 1.5v10a1.5 1.5 0 0 1-1.5 1.5H4.75a1.5 1.5 0 0 1-1.5-1.5V6a1.5 1.5 0 0 1 1.5-1.5z",
|
|
152
|
+
store:
|
|
153
|
+
"M3.5 8.5V16a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5V8.5|M2.75 8.5 4.2 4.1a1 1 0 0 1 .95-.6h9.7a1 1 0 0 1 .95.6L17.25 8.5|M2.75 8.5h14.5",
|
|
154
|
+
search: "M9 15.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13z|M13.8 13.8 17.5 17.5",
|
|
155
|
+
chevronLeft: "M12.5 4.5 7 10l5.5 5.5",
|
|
156
|
+
chevronRight: "M7.5 4.5 13 10l-5.5 5.5",
|
|
157
|
+
check: "M4.8 10.4 8.3 13.9 15.2 6.6",
|
|
158
|
+
minus: "M5.2 10h9.6"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A registry name, or raw path data; `|` separates subpaths. */
|
|
162
|
+
function icon(name, width) {
|
|
163
|
+
var svg = document.createElementNS(SVG_NS, "svg")
|
|
164
|
+
svg.setAttribute("viewBox", "0 0 20 20")
|
|
165
|
+
svg.setAttribute("fill", "none")
|
|
166
|
+
svg.setAttribute("aria-hidden", "true")
|
|
167
|
+
;(ICONS[name] || name).split("|").forEach(function (segment) {
|
|
168
|
+
var path = document.createElementNS(SVG_NS, "path")
|
|
169
|
+
path.setAttribute("d", segment)
|
|
170
|
+
path.setAttribute("stroke", "currentColor")
|
|
171
|
+
path.setAttribute("stroke-width", String(width || 1.6))
|
|
172
|
+
path.setAttribute("stroke-linecap", "round")
|
|
173
|
+
path.setAttribute("stroke-linejoin", "round")
|
|
174
|
+
svg.appendChild(path)
|
|
175
|
+
})
|
|
176
|
+
return svg
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The Rebus chip is the canvas trigger; only the caret is ours, since Rebus ships none. */
|
|
180
|
+
function chipTrigger(glyph) {
|
|
181
|
+
var button = el("button", "fm-chip fm-btn")
|
|
182
|
+
button.type = "button"
|
|
183
|
+
if (glyph) {
|
|
184
|
+
var slot = el("span", "fm-chip__icon")
|
|
185
|
+
slot.appendChild(icon(glyph))
|
|
186
|
+
button.appendChild(slot)
|
|
187
|
+
}
|
|
188
|
+
var label = el("span", "fm-chip__label fm-text-body-md-500")
|
|
189
|
+
button.appendChild(label)
|
|
190
|
+
button.appendChild(el("span", "fm-caret", "\u25be"))
|
|
191
|
+
return { button: button, label: label }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Rebus button anatomy; `hook` is the behavior class the tests address it by. */
|
|
195
|
+
function actionButton(text, variant, hook) {
|
|
196
|
+
var button = el("button", "fm-button fm-button--" + variant + " fm-button--sm fm-button--layout-default" + (hook ? " " + hook : ""))
|
|
197
|
+
button.type = "button"
|
|
198
|
+
var labels = el("span", "fm-button__labels")
|
|
199
|
+
labels.appendChild(el("span", "fm-button__label fm-text-body-md-500", text))
|
|
200
|
+
button.appendChild(labels)
|
|
201
|
+
return button
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Rebus checkbox anatomy: the native input is the state, the box and glyph are the picture. */
|
|
205
|
+
function checkbox(checked, indeterminate) {
|
|
206
|
+
var root = el("span", "fm-checkbox fm-checkbox--md")
|
|
207
|
+
var marked = !!checked || !!indeterminate
|
|
208
|
+
if (marked) root.classList.add("fm-checkbox--selected")
|
|
209
|
+
var box = el("span", "fm-checkbox__box")
|
|
210
|
+
var input = el("input", "fm-checkbox__input")
|
|
211
|
+
input.type = "checkbox"
|
|
212
|
+
input.checked = !!checked
|
|
213
|
+
input.indeterminate = !!indeterminate
|
|
214
|
+
input.tabIndex = -1
|
|
215
|
+
box.appendChild(input)
|
|
216
|
+
if (marked) {
|
|
217
|
+
var glyph = el("span", "fm-checkbox__glyph")
|
|
218
|
+
glyph.appendChild(icon(indeterminate ? "minus" : "check", 2.2))
|
|
219
|
+
box.appendChild(glyph)
|
|
220
|
+
}
|
|
221
|
+
root.appendChild(box)
|
|
222
|
+
return root
|
|
223
|
+
}
|
|
224
|
+
|
|
86
225
|
function resolve(target) {
|
|
87
226
|
var node = typeof target === "string" ? document.querySelector(target) : target
|
|
88
227
|
if (!node) throw new Error("FM.ui: no element for " + target)
|
|
@@ -119,12 +258,63 @@
|
|
|
119
258
|
})
|
|
120
259
|
})
|
|
121
260
|
|
|
122
|
-
|
|
261
|
+
var POP_GAP = 4
|
|
262
|
+
var POP_EDGE = 8
|
|
263
|
+
var POP_MIN_ROOM = 120
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Places a fixed panel by its chip: below when it fits, above when only that fits, and no taller
|
|
267
|
+
* than the room on that side, so the Apply foot stays reachable and the list scrolls instead.
|
|
268
|
+
* `natural` is the panel's unconstrained height, measured once per opening.
|
|
269
|
+
*/
|
|
270
|
+
function placePanel(button, panel, natural) {
|
|
271
|
+
var chip = button.getBoundingClientRect()
|
|
272
|
+
var vw = document.documentElement.clientWidth
|
|
273
|
+
var vh = document.documentElement.clientHeight
|
|
274
|
+
var below = vh - chip.bottom - POP_GAP - POP_EDGE
|
|
275
|
+
var above = chip.top - POP_GAP - POP_EDGE
|
|
276
|
+
var up = natural > below && above > below
|
|
277
|
+
var room = Math.max(up ? above : below, POP_MIN_ROOM)
|
|
278
|
+
panel.style.maxHeight = natural > room ? room + "px" : ""
|
|
279
|
+
panel.style.minWidth = Math.max(chip.width, 248) + "px"
|
|
280
|
+
var height = panel.offsetHeight
|
|
281
|
+
var width = panel.offsetWidth
|
|
282
|
+
var top = up ? Math.max(POP_EDGE, chip.top - POP_GAP - height) : chip.bottom + POP_GAP
|
|
283
|
+
var left = Math.min(Math.max(POP_EDGE, chip.left), Math.max(POP_EDGE, vw - POP_EDGE - width))
|
|
284
|
+
panel.style.top = Math.round(top) + "px"
|
|
285
|
+
panel.style.left = Math.round(left) + "px"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function popover(root, button, panel, hooks) {
|
|
123
289
|
var open = false
|
|
290
|
+
var natural = 0
|
|
291
|
+
hooks = hooks || {}
|
|
292
|
+
function place() {
|
|
293
|
+
placePanel(button, panel, natural)
|
|
294
|
+
}
|
|
295
|
+
// the panel follows its chip while the report scrolls, and re-fits when its own content changes
|
|
296
|
+
var observer = typeof ResizeObserver === "function" ? new ResizeObserver(place) : null
|
|
297
|
+
/** `onOpen` seeds the draft, `onClose` discards it — an outside click is a Cancel. */
|
|
124
298
|
function set(next) {
|
|
299
|
+
var was = open
|
|
125
300
|
open = next
|
|
301
|
+
if (!was && open && hooks.onOpen) hooks.onOpen()
|
|
126
302
|
panel.hidden = !open
|
|
127
303
|
button.setAttribute("aria-expanded", String(open))
|
|
304
|
+
button.classList.toggle("fm-chip--selected", open)
|
|
305
|
+
if (open && !was) {
|
|
306
|
+
panel.style.maxHeight = ""
|
|
307
|
+
natural = panel.offsetHeight
|
|
308
|
+
place()
|
|
309
|
+
window.addEventListener("scroll", place, true)
|
|
310
|
+
window.addEventListener("resize", place)
|
|
311
|
+
if (observer) observer.observe(panel)
|
|
312
|
+
} else if (was && !open) {
|
|
313
|
+
window.removeEventListener("scroll", place, true)
|
|
314
|
+
window.removeEventListener("resize", place)
|
|
315
|
+
if (observer) observer.disconnect()
|
|
316
|
+
}
|
|
317
|
+
if (was && !open && hooks.onClose) hooks.onClose()
|
|
128
318
|
}
|
|
129
319
|
button.addEventListener("click", function (event) {
|
|
130
320
|
event.stopPropagation()
|
|
@@ -133,18 +323,29 @@
|
|
|
133
323
|
panel.addEventListener("click", function (event) {
|
|
134
324
|
event.stopPropagation()
|
|
135
325
|
})
|
|
136
|
-
|
|
326
|
+
root.addEventListener("keydown", function (event) {
|
|
327
|
+
if (event.key !== "Escape" || !open) return
|
|
328
|
+
event.stopPropagation()
|
|
329
|
+
set(false)
|
|
330
|
+
button.focus()
|
|
331
|
+
})
|
|
332
|
+
var unregister = registerPopover(function (event) {
|
|
137
333
|
if (open && !root.contains(event.target)) set(false)
|
|
138
334
|
})
|
|
139
335
|
set(false)
|
|
140
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
close: function () { set(false) },
|
|
338
|
+
isOpen: function () { return open },
|
|
339
|
+
destroy: function () { set(false); unregister() }
|
|
340
|
+
}
|
|
141
341
|
}
|
|
142
342
|
|
|
143
343
|
// --- dropdown ------------------------------------------------------------
|
|
144
344
|
|
|
145
345
|
/**
|
|
146
346
|
* Single or multi select. `value` is one value, or an array when `multiple`.
|
|
147
|
-
*
|
|
347
|
+
* Single commits on click; multi collects ticks and commits on Apply, as the classic
|
|
348
|
+
* outlet picker does — one query per decision, not one per tick.
|
|
148
349
|
*/
|
|
149
350
|
function dropdown(target, options) {
|
|
150
351
|
ensureStyle()
|
|
@@ -153,52 +354,97 @@
|
|
|
153
354
|
var items = normalizeOptions(config.options)
|
|
154
355
|
var multiple = !!config.multiple
|
|
155
356
|
var searchable = config.searchable === undefined ? items.length > 8 : !!config.searchable
|
|
156
|
-
var
|
|
357
|
+
var committed = multiple ? asArray(config.value) : config.value
|
|
358
|
+
var draft = multiple ? committed.slice() : committed
|
|
157
359
|
var filter = ""
|
|
360
|
+
/** `{ label, active, onSelect }` — a named action, never inferred from a full selection. */
|
|
361
|
+
var all = config.all || null
|
|
158
362
|
|
|
159
363
|
var root = el("div", "fm-c")
|
|
160
|
-
var
|
|
161
|
-
var
|
|
162
|
-
var
|
|
163
|
-
button.appendChild(label)
|
|
164
|
-
button.appendChild(caret)
|
|
364
|
+
var trigger = chipTrigger(config.icon)
|
|
365
|
+
var button = trigger.button
|
|
366
|
+
var label = trigger.label
|
|
165
367
|
var panel = el("div", "fm-pop")
|
|
166
368
|
panel.hidden = true
|
|
167
|
-
var
|
|
369
|
+
var searchBar = el("div", "fm-search-bar fm-search-bar--sm")
|
|
370
|
+
var leading = el("span", "fm-search-bar__leading")
|
|
371
|
+
leading.appendChild(icon("search"))
|
|
372
|
+
searchBar.appendChild(leading)
|
|
373
|
+
var search = el("input", "fm-search fm-search-bar__control fm-text-body-md-400")
|
|
168
374
|
search.type = "search"
|
|
169
375
|
search.placeholder = "Search"
|
|
170
|
-
|
|
171
|
-
|
|
376
|
+
search.setAttribute("aria-label", "Search options")
|
|
377
|
+
searchBar.appendChild(search)
|
|
378
|
+
var list = el("div", "fm-pop-list")
|
|
379
|
+
var allRow = null
|
|
380
|
+
if (all) {
|
|
381
|
+
allRow = el("div", "fm-opt fm-opt-all")
|
|
382
|
+
allRow.appendChild(el("span", null, all.label || "All"))
|
|
383
|
+
var mark = el("span", "fm-opt-mark")
|
|
384
|
+
mark.appendChild(icon("check", 2.2))
|
|
385
|
+
allRow.appendChild(mark)
|
|
386
|
+
allRow.addEventListener("click", function () {
|
|
387
|
+
control.close()
|
|
388
|
+
if (typeof all.onSelect === "function") all.onSelect()
|
|
389
|
+
})
|
|
390
|
+
var allWrap = el("div", "fm-pop-all")
|
|
391
|
+
allWrap.appendChild(allRow)
|
|
392
|
+
panel.appendChild(allWrap)
|
|
393
|
+
}
|
|
394
|
+
if (searchable) panel.appendChild(searchBar)
|
|
172
395
|
panel.appendChild(list)
|
|
396
|
+
if (multiple) {
|
|
397
|
+
var foot = el("div", "fm-pop-foot")
|
|
398
|
+
var cancel = actionButton("Cancel", "ghost", "fm-dd-cancel")
|
|
399
|
+
var apply = actionButton("Apply", "primary", "fm-dd-apply")
|
|
400
|
+
cancel.addEventListener("click", function () { control.close() })
|
|
401
|
+
apply.addEventListener("click", function () {
|
|
402
|
+
committed = draft.slice()
|
|
403
|
+
control.close()
|
|
404
|
+
emit()
|
|
405
|
+
})
|
|
406
|
+
foot.appendChild(cancel)
|
|
407
|
+
foot.appendChild(apply)
|
|
408
|
+
panel.appendChild(foot)
|
|
409
|
+
}
|
|
173
410
|
root.appendChild(button)
|
|
174
411
|
root.appendChild(panel)
|
|
175
412
|
host.innerHTML = ""
|
|
176
413
|
host.appendChild(root)
|
|
177
414
|
|
|
178
415
|
function isSelected(value) {
|
|
179
|
-
return multiple ?
|
|
416
|
+
return multiple ? draft.indexOf(value) >= 0 : draft === value
|
|
180
417
|
}
|
|
181
418
|
|
|
182
419
|
function summary() {
|
|
420
|
+
if (all && all.active) return all.label || "All"
|
|
183
421
|
if (multiple) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
422
|
+
if (draft.length === 0) return config.placeholder || "None"
|
|
423
|
+
// with a named All action beside it, a full list is a full list, never a claim of All
|
|
424
|
+
if (!all && draft.length === items.length && items.length > 0)
|
|
425
|
+
return config.allLabel || "All"
|
|
426
|
+
if (draft.length === 1) {
|
|
427
|
+
var one = items.filter(function (item) { return item.value === draft[0] })[0]
|
|
428
|
+
return one ? one.label : String(draft[0])
|
|
189
429
|
}
|
|
190
|
-
return
|
|
430
|
+
return draft.length + " selected"
|
|
191
431
|
}
|
|
192
|
-
var match = items.filter(function (item) { return item.value ===
|
|
432
|
+
var match = items.filter(function (item) { return item.value === draft })[0]
|
|
193
433
|
return match ? match.label : config.placeholder || "Select"
|
|
194
434
|
}
|
|
195
435
|
|
|
196
436
|
function emit() {
|
|
197
437
|
if (typeof config.onChange === "function") {
|
|
198
|
-
config.onChange(multiple ?
|
|
438
|
+
config.onChange(multiple ? committed.slice() : committed)
|
|
199
439
|
}
|
|
200
440
|
}
|
|
201
441
|
|
|
442
|
+
function repaint() {
|
|
443
|
+
label.textContent = summary()
|
|
444
|
+
if (allRow) allRow.setAttribute("aria-selected", String(!!(all && all.active)))
|
|
445
|
+
renderList()
|
|
446
|
+
}
|
|
447
|
+
|
|
202
448
|
function renderList() {
|
|
203
449
|
list.innerHTML = ""
|
|
204
450
|
var needle = filter.toLowerCase()
|
|
@@ -208,46 +454,44 @@
|
|
|
208
454
|
if (multiple && items.length > 1 && visible.length > 0) {
|
|
209
455
|
// Select all acts on what the search leaves visible, as the classic outlet picker does.
|
|
210
456
|
var shown = visible.map(function (item) { return item.value })
|
|
211
|
-
var
|
|
212
|
-
var
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
457
|
+
var picked = shown.filter(isSelected).length
|
|
458
|
+
var allShownSelected = picked === shown.length
|
|
459
|
+
var toggleAll = el("div", "fm-opt fm-opt-check")
|
|
460
|
+
toggleAll.setAttribute("aria-selected", String(allShownSelected))
|
|
461
|
+
toggleAll.appendChild(checkbox(allShownSelected, picked > 0 && !allShownSelected))
|
|
462
|
+
toggleAll.appendChild(
|
|
463
|
+
el("span", null, "Select all (" + picked + "/" + shown.length + ")")
|
|
464
|
+
)
|
|
465
|
+
toggleAll.addEventListener("click", function () {
|
|
466
|
+
draft = allShownSelected
|
|
467
|
+
? draft.filter(function (value) { return shown.indexOf(value) < 0 })
|
|
468
|
+
: draft.concat(shown.filter(function (value) { return !isSelected(value) }))
|
|
469
|
+
repaint()
|
|
221
470
|
})
|
|
222
|
-
list.appendChild(
|
|
471
|
+
list.appendChild(toggleAll)
|
|
223
472
|
}
|
|
224
473
|
if (visible.length === 0) {
|
|
225
474
|
list.appendChild(el("div", "fm-empty", "No matches"))
|
|
226
475
|
return
|
|
227
476
|
}
|
|
228
477
|
visible.forEach(function (item) {
|
|
229
|
-
var option = el("div", "fm-opt")
|
|
478
|
+
var option = el("div", multiple ? "fm-opt fm-opt-check" : "fm-opt")
|
|
230
479
|
option.setAttribute("role", "option")
|
|
231
480
|
option.setAttribute("aria-selected", String(isSelected(item.value)))
|
|
232
|
-
if (multiple)
|
|
233
|
-
var box = el("input")
|
|
234
|
-
box.type = "checkbox"
|
|
235
|
-
box.checked = isSelected(item.value)
|
|
236
|
-
box.tabIndex = -1
|
|
237
|
-
option.appendChild(box)
|
|
238
|
-
}
|
|
481
|
+
if (multiple) option.appendChild(checkbox(isSelected(item.value)))
|
|
239
482
|
option.appendChild(el("span", null, item.label))
|
|
240
483
|
option.addEventListener("click", function () {
|
|
241
484
|
if (multiple) {
|
|
242
|
-
|
|
243
|
-
?
|
|
244
|
-
:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
control.close()
|
|
485
|
+
draft = isSelected(item.value)
|
|
486
|
+
? draft.filter(function (value) { return value !== item.value })
|
|
487
|
+
: draft.concat([item.value])
|
|
488
|
+
repaint()
|
|
489
|
+
return
|
|
248
490
|
}
|
|
249
|
-
|
|
250
|
-
|
|
491
|
+
draft = item.value
|
|
492
|
+
committed = draft
|
|
493
|
+
control.close()
|
|
494
|
+
repaint()
|
|
251
495
|
emit()
|
|
252
496
|
})
|
|
253
497
|
list.appendChild(option)
|
|
@@ -259,24 +503,46 @@
|
|
|
259
503
|
renderList()
|
|
260
504
|
})
|
|
261
505
|
|
|
262
|
-
var control = popover(root, button, panel
|
|
263
|
-
|
|
264
|
-
|
|
506
|
+
var control = popover(root, button, panel, {
|
|
507
|
+
onOpen: function () {
|
|
508
|
+
draft = multiple ? committed.slice() : committed
|
|
509
|
+
// a reopened picker showing the last search reads as a broken list
|
|
510
|
+
filter = ""
|
|
511
|
+
search.value = ""
|
|
512
|
+
repaint()
|
|
513
|
+
},
|
|
514
|
+
// an outside click, Cancel or Escape is a discard; Apply commits first, so this is a no-op
|
|
515
|
+
onClose: function () {
|
|
516
|
+
draft = multiple ? committed.slice() : committed
|
|
517
|
+
repaint()
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
repaint()
|
|
265
521
|
|
|
266
522
|
return {
|
|
267
523
|
element: root,
|
|
268
|
-
getValue: function () { return multiple ?
|
|
524
|
+
getValue: function () { return multiple ? committed.slice() : committed },
|
|
269
525
|
setValue: function (next) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
526
|
+
committed = multiple ? asArray(next) : next
|
|
527
|
+
// a param push mid-edit must not wipe the merchant's ticks
|
|
528
|
+
if (!control.isOpen()) draft = multiple ? committed.slice() : committed
|
|
529
|
+
repaint()
|
|
273
530
|
},
|
|
274
531
|
setOptions: function (next) {
|
|
275
532
|
items = normalizeOptions(next)
|
|
276
|
-
|
|
277
|
-
renderList()
|
|
533
|
+
repaint()
|
|
278
534
|
},
|
|
279
|
-
|
|
535
|
+
/** Marks the named All action live; the caller owns what that means. */
|
|
536
|
+
setAll: function (active) {
|
|
537
|
+
if (!all) return
|
|
538
|
+
all.active = !!active
|
|
539
|
+
repaint()
|
|
540
|
+
},
|
|
541
|
+
setDisabled: function (flag) {
|
|
542
|
+
button.disabled = !!flag
|
|
543
|
+
if (flag) control.close()
|
|
544
|
+
},
|
|
545
|
+
destroy: function () { control.destroy(); host.innerHTML = "" }
|
|
280
546
|
}
|
|
281
547
|
}
|
|
282
548
|
|
|
@@ -374,69 +640,124 @@
|
|
|
374
640
|
* Preset list plus a custom range. Emits `{ from, to, preset }` with ISO instants at the
|
|
375
641
|
* report timezone's day boundaries — the section decides which params they land on.
|
|
376
642
|
*/
|
|
643
|
+
var SINGLE_PRESETS = DEFAULT_PRESETS.filter(function (preset) {
|
|
644
|
+
return preset.key === "today" || preset.key === "yesterday"
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
function dowOf(ymd) {
|
|
648
|
+
return new Date(Date.parse(ymd + "T00:00:00.000Z")).getUTCDay()
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Grid labels and titles read in the report's locale; the ymd is a UTC instant by construction. */
|
|
652
|
+
function partsOf(ymd, options) {
|
|
653
|
+
try {
|
|
654
|
+
var spec = { timeZone: "UTC" }
|
|
655
|
+
Object.keys(options).forEach(function (key) { spec[key] = options[key] })
|
|
656
|
+
return new Intl.DateTimeFormat(
|
|
657
|
+
(window.env && window.env.locale) || "en-US",
|
|
658
|
+
spec
|
|
659
|
+
).format(new Date(Date.parse(ymd + "T00:00:00.000Z")))
|
|
660
|
+
} catch (err) {
|
|
661
|
+
return ymd
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function dayNames() {
|
|
666
|
+
var out = []
|
|
667
|
+
// 2026-03-01 is a Sunday: the grid is Sunday-first like the classic calendar, while the
|
|
668
|
+
// week presets above stay Monday-first like the classic report's week boundaries.
|
|
669
|
+
for (var i = 0; i < 7; i++) out.push(partsOf(shiftYmd("2026-03-01", i), { weekday: "short" }))
|
|
670
|
+
return out
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Day-first like the classic filter chip beside it, with the month name still localized:
|
|
674
|
+
// Intl's own en-US order ("Jul 01, 2026") would read differently from the classic report.
|
|
675
|
+
function dayLabel(ymd) {
|
|
676
|
+
return ymd.slice(8) + " " + partsOf(ymd, { month: "short" })
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** "09 Sep 2026", or "09 Sep \u2013 15 Sep 2026" with one year when the range shares it. */
|
|
680
|
+
function describeRange(fromYmd, toYmd) {
|
|
681
|
+
var year = " " + toYmd.slice(0, 4)
|
|
682
|
+
if (fromYmd === toYmd) return dayLabel(fromYmd) + year
|
|
683
|
+
if (fromYmd.slice(0, 4) !== toYmd.slice(0, 4)) {
|
|
684
|
+
return dayLabel(fromYmd) + " " + fromYmd.slice(0, 4) + " \u2013 " + dayLabel(toYmd) + year
|
|
685
|
+
}
|
|
686
|
+
return dayLabel(fromYmd) + " \u2013 " + dayLabel(toYmd) + year
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** `single: true` picks one day: one grid, and both bounds land on that day. */
|
|
377
690
|
function dateRange(target, options) {
|
|
378
691
|
ensureStyle()
|
|
379
692
|
var host = resolve(target)
|
|
380
693
|
var config = options || {}
|
|
381
694
|
var timezone = config.timezone || (window.env && window.env.timezone) || "UTC"
|
|
382
|
-
var
|
|
695
|
+
var single = !!config.single
|
|
696
|
+
var presets = config.presets || (single ? SINGLE_PRESETS : DEFAULT_PRESETS)
|
|
383
697
|
var todayYmd = ymdIn(new Date(), timezone)
|
|
384
698
|
var value = config.value || {}
|
|
385
|
-
var fromYmd = value.from ? ymdIn(new Date(value.from), timezone) : shiftYmd(todayYmd, -29)
|
|
386
|
-
var toYmd = value.to ? ymdIn(new Date(value.to), timezone) : todayYmd
|
|
699
|
+
var fromYmd = value.from ? ymdIn(new Date(value.from), timezone) : single ? todayYmd : shiftYmd(todayYmd, -29)
|
|
700
|
+
var toYmd = single ? fromYmd : value.to ? ymdIn(new Date(value.to), timezone) : todayYmd
|
|
387
701
|
var presetKey = config.preset || null
|
|
702
|
+
// draft: what the open popover is editing, committed only by Apply
|
|
703
|
+
var dFrom = fromYmd
|
|
704
|
+
var dTo = toYmd
|
|
705
|
+
var dPreset = presetKey
|
|
706
|
+
var anchor = null
|
|
707
|
+
var viewYmd = monthStart(dFrom, 0)
|
|
388
708
|
|
|
389
709
|
var root = el("div", "fm-c")
|
|
390
|
-
var
|
|
391
|
-
var
|
|
392
|
-
|
|
393
|
-
button.appendChild(el("span", "fm-caret", "▾"))
|
|
710
|
+
var trigger = chipTrigger(config.icon === null ? null : config.icon || "calendar")
|
|
711
|
+
var button = trigger.button
|
|
712
|
+
var label = trigger.label
|
|
394
713
|
var panel = el("div", "fm-pop")
|
|
395
714
|
panel.hidden = true
|
|
396
715
|
var body = el("div", "fm-dr-body")
|
|
397
716
|
var presetList = el("div", "fm-dr-presets")
|
|
398
|
-
var
|
|
399
|
-
var
|
|
400
|
-
var
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
var
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
var
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
717
|
+
var calendar = el("div", "fm-cal")
|
|
718
|
+
var head = el("div", "fm-cal-head")
|
|
719
|
+
var prev = el("button", "fm-cal-nav fm-cal-prev")
|
|
720
|
+
prev.type = "button"
|
|
721
|
+
prev.setAttribute("aria-label", "Previous month")
|
|
722
|
+
prev.appendChild(icon("chevronLeft"))
|
|
723
|
+
var next = el("button", "fm-cal-nav fm-cal-next")
|
|
724
|
+
next.type = "button"
|
|
725
|
+
next.setAttribute("aria-label", "Next month")
|
|
726
|
+
next.appendChild(icon("chevronRight"))
|
|
727
|
+
var title = el("div", "fm-cal-title fm-text-body-md-500")
|
|
728
|
+
var grid = el("div", "fm-cal-grid")
|
|
729
|
+
head.appendChild(prev)
|
|
730
|
+
head.appendChild(title)
|
|
731
|
+
head.appendChild(next)
|
|
732
|
+
calendar.appendChild(head)
|
|
733
|
+
calendar.appendChild(grid)
|
|
413
734
|
body.appendChild(presetList)
|
|
414
|
-
body.appendChild(
|
|
735
|
+
body.appendChild(calendar)
|
|
415
736
|
panel.appendChild(body)
|
|
737
|
+
var foot = el("div", "fm-pop-foot")
|
|
738
|
+
var cancel = actionButton("Cancel", "ghost", "fm-dr-cancel")
|
|
739
|
+
var apply = actionButton("Apply", "primary", "fm-dr-apply")
|
|
740
|
+
foot.appendChild(cancel)
|
|
741
|
+
foot.appendChild(apply)
|
|
742
|
+
panel.appendChild(foot)
|
|
416
743
|
root.appendChild(button)
|
|
417
744
|
root.appendChild(panel)
|
|
418
745
|
host.innerHTML = ""
|
|
419
746
|
host.appendChild(root)
|
|
420
747
|
|
|
748
|
+
var DOW = dayNames()
|
|
749
|
+
|
|
421
750
|
function summary() {
|
|
422
|
-
|
|
423
|
-
if (active) return active.label
|
|
424
|
-
return fromYmd === toYmd ? fromYmd : fromYmd + " – " + toYmd
|
|
751
|
+
return describeRange(fromYmd, toYmd)
|
|
425
752
|
}
|
|
426
753
|
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
from: boundaryIso(fromYmd, timezone, false),
|
|
435
|
-
to: boundaryIso(toYmd, timezone, true),
|
|
436
|
-
fromDate: fromYmd,
|
|
437
|
-
toDate: toYmd,
|
|
438
|
-
preset: presetKey
|
|
439
|
-
})
|
|
754
|
+
function pick() {
|
|
755
|
+
return {
|
|
756
|
+
from: boundaryIso(fromYmd, timezone, false),
|
|
757
|
+
to: boundaryIso(toYmd, timezone, true),
|
|
758
|
+
fromDate: fromYmd,
|
|
759
|
+
toDate: toYmd,
|
|
760
|
+
preset: presetKey
|
|
440
761
|
}
|
|
441
762
|
}
|
|
442
763
|
|
|
@@ -444,61 +765,119 @@
|
|
|
444
765
|
presetList.innerHTML = ""
|
|
445
766
|
presets.forEach(function (preset) {
|
|
446
767
|
var option = el("div", "fm-opt", preset.label)
|
|
447
|
-
option.setAttribute("aria-selected", String(preset.key ===
|
|
768
|
+
option.setAttribute("aria-selected", String(preset.key === dPreset))
|
|
448
769
|
option.addEventListener("click", function () {
|
|
449
770
|
var range = preset.range(ymdIn(new Date(), timezone))
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
771
|
+
dFrom = range[0]
|
|
772
|
+
dTo = single ? range[0] : range[1]
|
|
773
|
+
dPreset = preset.key
|
|
774
|
+
anchor = null
|
|
775
|
+
viewYmd = monthStart(dFrom, 0)
|
|
776
|
+
renderDraft()
|
|
455
777
|
})
|
|
456
778
|
presetList.appendChild(option)
|
|
457
779
|
})
|
|
458
780
|
}
|
|
459
781
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
782
|
+
function renderCalendar() {
|
|
783
|
+
title.textContent = partsOf(viewYmd, { month: "long", year: "numeric" })
|
|
784
|
+
grid.innerHTML = ""
|
|
785
|
+
DOW.forEach(function (name) {
|
|
786
|
+
grid.appendChild(el("div", "fm-cal-dow fm-text-body-sm-400", name))
|
|
787
|
+
})
|
|
788
|
+
var start = shiftYmd(viewYmd, -dowOf(viewYmd))
|
|
789
|
+
var month = viewYmd.slice(0, 7)
|
|
790
|
+
// six rows always: the popover keeps one height as the merchant pages through months
|
|
791
|
+
for (var index = 0; index < 42; index++) {
|
|
792
|
+
var ymd = shiftYmd(start, index)
|
|
793
|
+
var inRange = ymd >= dFrom && ymd <= dTo
|
|
794
|
+
var edge = ymd === dFrom ? (ymd === dTo ? "both" : "start") : ymd === dTo ? "end" : ""
|
|
795
|
+
var cell = el("div", "fm-cal-cell")
|
|
796
|
+
if (inRange) cell.setAttribute("data-in-range", "true")
|
|
797
|
+
if (edge) cell.setAttribute("data-edge", edge)
|
|
798
|
+
var day = el("button", "fm-cal-day fm-text-body-md-400", String(Number(ymd.slice(8))))
|
|
799
|
+
day.type = "button"
|
|
800
|
+
day.setAttribute("data-ymd", ymd)
|
|
801
|
+
if (ymd.slice(0, 7) !== month) day.setAttribute("data-outside", "true")
|
|
802
|
+
if (ymd === todayYmd) day.setAttribute("data-today", "true")
|
|
803
|
+
if (edge) day.setAttribute("data-selected", "true")
|
|
804
|
+
day.addEventListener("click", pickDay.bind(null, ymd))
|
|
805
|
+
cell.appendChild(day)
|
|
806
|
+
grid.appendChild(cell)
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** First click sets one end, the second the other; a third starts over. */
|
|
811
|
+
function pickDay(ymd) {
|
|
812
|
+
dPreset = null
|
|
813
|
+
if (single || anchor === null) {
|
|
814
|
+
anchor = single ? null : ymd
|
|
815
|
+
dFrom = ymd
|
|
816
|
+
dTo = ymd
|
|
817
|
+
} else {
|
|
818
|
+
dFrom = anchor < ymd ? anchor : ymd
|
|
819
|
+
dTo = anchor < ymd ? ymd : anchor
|
|
820
|
+
anchor = null
|
|
468
821
|
}
|
|
469
|
-
|
|
822
|
+
renderDraft()
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function renderDraft() {
|
|
826
|
+
renderPresets()
|
|
827
|
+
renderCalendar()
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function seedDraft() {
|
|
831
|
+
dFrom = fromYmd
|
|
832
|
+
dTo = toYmd
|
|
833
|
+
dPreset = presetKey
|
|
834
|
+
anchor = null
|
|
835
|
+
viewYmd = monthStart(dFrom, 0)
|
|
836
|
+
renderDraft()
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
prev.addEventListener("click", function () {
|
|
840
|
+
viewYmd = monthStart(viewYmd, 1)
|
|
841
|
+
renderCalendar()
|
|
842
|
+
})
|
|
843
|
+
next.addEventListener("click", function () {
|
|
844
|
+
viewYmd = monthStart(shiftYmd(monthEnd(viewYmd), 1), 0)
|
|
845
|
+
renderCalendar()
|
|
846
|
+
})
|
|
847
|
+
cancel.addEventListener("click", function () {
|
|
470
848
|
control.close()
|
|
471
|
-
|
|
849
|
+
})
|
|
850
|
+
apply.addEventListener("click", function () {
|
|
851
|
+
fromYmd = dFrom
|
|
852
|
+
toYmd = dTo
|
|
853
|
+
presetKey = dPreset
|
|
854
|
+
control.close()
|
|
855
|
+
label.textContent = summary()
|
|
856
|
+
if (typeof config.onChange === "function") config.onChange(pick())
|
|
472
857
|
})
|
|
473
858
|
|
|
474
|
-
var control = popover(root, button, panel)
|
|
859
|
+
var control = popover(root, button, panel, { onOpen: seedDraft, onClose: seedDraft })
|
|
475
860
|
label.textContent = summary()
|
|
476
|
-
|
|
477
|
-
toInput.value = toYmd
|
|
478
|
-
renderPresets()
|
|
861
|
+
renderDraft()
|
|
479
862
|
|
|
480
863
|
return {
|
|
481
864
|
element: root,
|
|
482
|
-
getValue:
|
|
483
|
-
return {
|
|
484
|
-
from: boundaryIso(fromYmd, timezone, false),
|
|
485
|
-
to: boundaryIso(toYmd, timezone, true),
|
|
486
|
-
fromDate: fromYmd,
|
|
487
|
-
toDate: toYmd,
|
|
488
|
-
preset: presetKey
|
|
489
|
-
}
|
|
490
|
-
},
|
|
865
|
+
getValue: pick,
|
|
491
866
|
setValue: function (next) {
|
|
492
867
|
if (!next) return
|
|
493
868
|
if (next.from) fromYmd = ymdIn(new Date(next.from), timezone)
|
|
494
|
-
if (next.to) toYmd = ymdIn(new Date(next.to), timezone)
|
|
869
|
+
if (next.to && !single) toYmd = ymdIn(new Date(next.to), timezone)
|
|
870
|
+
if (single) toYmd = fromYmd
|
|
495
871
|
presetKey = next.preset || null
|
|
496
872
|
label.textContent = summary()
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
renderPresets()
|
|
873
|
+
// a param push mid-edit must not move the calendar under the merchant's hand
|
|
874
|
+
if (!control.isOpen()) seedDraft()
|
|
500
875
|
},
|
|
501
|
-
|
|
876
|
+
setDisabled: function (flag) {
|
|
877
|
+
button.disabled = !!flag
|
|
878
|
+
if (flag) control.close()
|
|
879
|
+
},
|
|
880
|
+
destroy: function () { control.destroy(); host.innerHTML = "" }
|
|
502
881
|
}
|
|
503
882
|
}
|
|
504
883
|
|
|
@@ -516,11 +895,14 @@
|
|
|
516
895
|
host.innerHTML = ""
|
|
517
896
|
host.appendChild(root)
|
|
518
897
|
|
|
898
|
+
var disabled = false
|
|
899
|
+
|
|
519
900
|
function render() {
|
|
520
901
|
root.innerHTML = ""
|
|
521
902
|
items.forEach(function (item) {
|
|
522
|
-
var button = el("button",
|
|
903
|
+
var button = el("button", "fm-seg-item fm-text-body-md-500", item.label)
|
|
523
904
|
button.type = "button"
|
|
905
|
+
button.disabled = disabled
|
|
524
906
|
button.setAttribute("aria-pressed", String(item.value === value))
|
|
525
907
|
button.addEventListener("click", function () {
|
|
526
908
|
if (item.value === value) return
|
|
@@ -541,6 +923,14 @@
|
|
|
541
923
|
value = next
|
|
542
924
|
render()
|
|
543
925
|
},
|
|
926
|
+
setOptions: function (next) {
|
|
927
|
+
items = normalizeOptions(next)
|
|
928
|
+
render()
|
|
929
|
+
},
|
|
930
|
+
setDisabled: function (flag) {
|
|
931
|
+
disabled = !!flag
|
|
932
|
+
render()
|
|
933
|
+
},
|
|
544
934
|
destroy: function () { host.innerHTML = "" }
|
|
545
935
|
}
|
|
546
936
|
}
|
|
@@ -561,6 +951,9 @@
|
|
|
561
951
|
key: column.key,
|
|
562
952
|
label: column.label === undefined ? column.key : column.label,
|
|
563
953
|
className: classes.join(" "),
|
|
954
|
+
part: column.part,
|
|
955
|
+
imageOnly: column.format === "image",
|
|
956
|
+
image: column.image || null,
|
|
564
957
|
format: typeof column.format === "function" ? column.format : null
|
|
565
958
|
}
|
|
566
959
|
})
|
|
@@ -572,6 +965,28 @@
|
|
|
572
965
|
return value === null || value === undefined || value === "" ? "—" : String(value)
|
|
573
966
|
}
|
|
574
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Image values use their own key and occupy an ordinary cell at any column position.
|
|
970
|
+
* The legacy `image` binding composes a thumbnail with this column's text.
|
|
971
|
+
* Missing or failed images retain their box so rows stay aligned.
|
|
972
|
+
*/
|
|
973
|
+
function cellNode(column, row) {
|
|
974
|
+
if (!column.imageOnly && !column.image) return el("td", column.className, cellText(column, row))
|
|
975
|
+
var cell = el("td", (column.className ? column.className + " " : "") +
|
|
976
|
+
(column.imageOnly ? "fmr-image-cell" : "fmr-thumb-cell"))
|
|
977
|
+
var img = document.createElement("img")
|
|
978
|
+
img.className = "fmr-thumb"
|
|
979
|
+
img.alt = ""
|
|
980
|
+
img.loading = "lazy"
|
|
981
|
+
var src = row ? row[column.imageOnly ? column.key : column.image] : null
|
|
982
|
+
if (src === null || src === undefined || src === "") img.style.visibility = "hidden"
|
|
983
|
+
else img.src = String(src)
|
|
984
|
+
img.addEventListener("error", function () { img.style.visibility = "hidden" })
|
|
985
|
+
cell.appendChild(img)
|
|
986
|
+
if (!column.imageOnly) cell.appendChild(el("span", "fmr-thumb-text", cellText(column, row)))
|
|
987
|
+
return cell
|
|
988
|
+
}
|
|
989
|
+
|
|
575
990
|
/**
|
|
576
991
|
* Paged data table. How long a page is, is a reading decision: a report that IS one table pages
|
|
577
992
|
* large (50-100), a table under a summary takes the default 20, a tile beside a chart takes 5-10.
|
|
@@ -626,7 +1041,9 @@
|
|
|
626
1041
|
next.type = "button"
|
|
627
1042
|
|
|
628
1043
|
columns.forEach(function (column) {
|
|
629
|
-
|
|
1044
|
+
var head = el("th", column.className, column.label)
|
|
1045
|
+
if (column.part) head.setAttribute("data-fm-part", column.part)
|
|
1046
|
+
headRow.appendChild(head)
|
|
630
1047
|
})
|
|
631
1048
|
thead.appendChild(headRow)
|
|
632
1049
|
tableEl.appendChild(thead)
|
|
@@ -665,11 +1082,11 @@
|
|
|
665
1082
|
slice.forEach(function (row, i) {
|
|
666
1083
|
var tr = el("tr")
|
|
667
1084
|
columns.forEach(function (column) {
|
|
668
|
-
tr.appendChild(
|
|
1085
|
+
tr.appendChild(cellNode(column, row))
|
|
669
1086
|
})
|
|
670
1087
|
if (onRowClick) {
|
|
671
1088
|
var absIndex = from + i
|
|
672
|
-
tr.className = "fm-row-btn"
|
|
1089
|
+
tr.className = "fm-row-btn fmr-row-btn"
|
|
673
1090
|
tr.tabIndex = 0
|
|
674
1091
|
tr.setAttribute("aria-selected", String(isSelected(absIndex, row)))
|
|
675
1092
|
tr.addEventListener("click", function () { select(absIndex, row) })
|
|
@@ -754,7 +1171,9 @@
|
|
|
754
1171
|
columns = normalizeColumns(nextColumns)
|
|
755
1172
|
headRow.textContent = ""
|
|
756
1173
|
columns.forEach(function (column) {
|
|
757
|
-
|
|
1174
|
+
var head = el("th", column.className, column.label)
|
|
1175
|
+
if (column.part) head.setAttribute("data-fm-part", column.part)
|
|
1176
|
+
headRow.appendChild(head)
|
|
758
1177
|
})
|
|
759
1178
|
render()
|
|
760
1179
|
},
|