@mono-lit/helper 0.0.9 → 0.1.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.
Files changed (164) hide show
  1. package/dist/accordion-core-DaNyPWRr.js +277 -0
  2. package/dist/alert-core-DWtHbX_-.js +134 -0
  3. package/dist/breadcrumb-list-core-Dh68T8Cf.js +747 -0
  4. package/dist/{mono-button-CLkHg3WW.js → button-core-DerXg84Q.js} +3 -276
  5. package/dist/button-dropdown-core-DKYEOyjy.js +654 -0
  6. package/dist/button-utils-D5-66M02.js +63 -0
  7. package/dist/card-utils-l80qAmrN.js +312 -0
  8. package/dist/chart-core-DV7hD3ZY.js +801 -0
  9. package/dist/checkbox-utils-DKdrhAzF.js +337 -0
  10. package/dist/chip-utils-DpRrd7Zj.js +403 -0
  11. package/dist/{data-source-controller-C6C4ej_M.js → data-source-controller-DS_JLrmS.js} +1 -1
  12. package/dist/{data-source-options-fxT5QIX4.js → data-source-options-DJUNU26C.js} +6 -6
  13. package/dist/{data-source-read-BL266IW0.js → data-source-read-uKJpRAzx.js} +2 -2
  14. package/dist/date-core-CyuSyvvG.js +734 -0
  15. package/dist/drawer-core-DbFWvNln.js +759 -0
  16. package/dist/dropdown-core-BCH1pK14.js +511 -0
  17. package/dist/dropdown-table-core-SfHUJVFq.js +1342 -0
  18. package/dist/export.js +1 -1
  19. package/dist/{field-path-BabiMrrA.js → field-path-DK2VBWHl.js} +1 -1
  20. package/dist/field-tooltip.css_raw-DVblekCl.js +5 -0
  21. package/dist/{field-validation.css_raw-Cx6SGrfP.js → field-validation.css_raw-BU7C6173.js} +1 -170
  22. package/dist/file-upload-core-Cz_dBQMx.js +624 -0
  23. package/dist/filter-builder-core-DyWjbBP7.js +381 -0
  24. package/dist/{filter-builder-B6Y2mmbM.js → filter-builder-vbqIuPhX.js} +1 -1
  25. package/dist/{filter-eval-ChDSaAUH.js → filter-eval-CqeBnMYh.js} +1 -1
  26. package/dist/{filter-odata-Bi27XUpQ.js → filter-odata-GsczYKNC.js} +1 -1
  27. package/dist/{hydration-flush-BaE9gN3C.js → hydration-flush-Volt9X7Q.js} +2 -1071
  28. package/dist/index.js +11 -11
  29. package/dist/{index.node-DTqDBca7.js → index.node-Csy9zrr1.js} +4 -4
  30. package/dist/index.node.js +11 -11
  31. package/dist/input-utils-BglY-k0H.js +1145 -0
  32. package/dist/menu-core-DkAYuCZ2.js +831 -0
  33. package/dist/modal-core-CSBSPN_m.js +844 -0
  34. package/dist/mono-button-DVvZ03P-.js +280 -0
  35. package/dist/{mono-button.shadow-DqZMpcqY.js → mono-button.shadow-BANxMGln.js} +6 -594
  36. package/dist/{mono-data-chart-NKPdAgVq.js → mono-data-chart-ChF4J9RT.js} +6 -6
  37. package/dist/{mono-data-dropdown-B2iRpJuI.js → mono-data-dropdown-Bgj2PDfm.js} +1 -1
  38. package/dist/{mono-data-grid-llJCX9MC.js → mono-data-grid-DJkp96N-.js} +144 -46
  39. package/dist/{mono-event-BPku9HpS.js → mono-event-CAZf0F_k.js} +14 -9
  40. package/dist/{mono-form-controller-BrA10erm.js → mono-form-controller-DxZLvyg6.js} +2 -1
  41. package/dist/mono-table-checkbox-core-BKOMx8R5.js +5288 -0
  42. package/dist/nav-core-B5qU2dzn.js +225 -0
  43. package/dist/{normalize-error-D93O4q8g.js → normalize-error-BEcd1tmS.js} +2 -2
  44. package/dist/nuxt.js +1 -1
  45. package/dist/otp-core-DXg3vFLh.js +758 -0
  46. package/dist/radio-utils-Dk9Tl1jz.js +319 -0
  47. package/dist/rich-text-editor-core-Ddd_QaLn.js +1280 -0
  48. package/dist/select-utils-BgArYHk8.js +2140 -0
  49. package/dist/sidebar-core-CD6txDo3.js +583 -0
  50. package/dist/slider-core-vcWr321-.js +1014 -0
  51. package/dist/src/components/dropdown-table/dropdown-table-core.d.ts +2 -2
  52. package/dist/src/components/dropdown-table/dropdown-table-types.d.ts +1 -1
  53. package/dist/src/components/dropdown-table/index.d.ts +1 -1
  54. package/dist/src/components/dropdown-table/index.shadow.d.ts +1 -1
  55. package/dist/src/components/dropdown-table/mono-data-dropdown.d.ts +1 -1
  56. package/dist/src/components/form/form-types.d.ts +9 -3
  57. package/dist/src/components/input/input-core.d.ts +1 -1
  58. package/dist/src/components/input/input-types.d.ts +1 -1
  59. package/dist/src/components/otp/otp-core.d.ts +1 -1
  60. package/dist/src/components/select/select-core.d.ts +1 -1
  61. package/dist/src/components/select/select-types.d.ts +1 -1
  62. package/dist/src/components/slider/slider-core.d.ts +1 -1
  63. package/dist/src/components/slider/slider-types.d.ts +1 -1
  64. package/dist/src/components/table/filter-suggestion.d.ts +171 -0
  65. package/dist/src/components/table/index.d.ts +5 -4
  66. package/dist/src/components/table/index.shadow.d.ts +5 -4
  67. package/dist/src/components/table/mono-data-grid.d.ts +22 -3
  68. package/dist/src/components/table/mono-table-empty-core.d.ts +37 -0
  69. package/dist/src/components/table/mono-table-search-core.d.ts +22 -3
  70. package/dist/src/components/table/table-types.d.ts +74 -4
  71. package/dist/src/components/tag-input/tag-input-core.d.ts +1 -1
  72. package/dist/src/components/tag-input/tag-input-types.d.ts +1 -1
  73. package/dist/src/components/textarea/textarea-core.d.ts +1 -1
  74. package/dist/src/composables/mono-event.d.ts +7 -1
  75. package/dist/src/entries/index.d.ts +8 -6
  76. package/dist/src/entries/index.node.d.ts +8 -6
  77. package/dist/src/search/index.d.ts +1 -1
  78. package/dist/src/utils/group-count.d.ts +36 -0
  79. package/dist/switch-utils-CTZ4id2V.js +338 -0
  80. package/dist/tabs-core-r5-B8k_N.js +317 -0
  81. package/dist/tag-input-utils-D2JA90DQ.js +3060 -0
  82. package/dist/textarea-utils-VU1KeXm8.js +821 -0
  83. package/dist/ui/accordion.js +2 -274
  84. package/dist/ui/alert.js +2 -131
  85. package/dist/ui/breadcrumb.js +4 -745
  86. package/dist/ui/button-dropdown.js +4 -652
  87. package/dist/ui/button.js +2 -63
  88. package/dist/ui/card.js +2 -310
  89. package/dist/ui/chart.js +4 -800
  90. package/dist/ui/checkbox.js +4 -335
  91. package/dist/ui/chip.js +4 -402
  92. package/dist/ui/date.js +3 -732
  93. package/dist/ui/drawer.js +3 -757
  94. package/dist/ui/dropdown-table.js +5 -1341
  95. package/dist/ui/dropdown.js +4 -509
  96. package/dist/ui/file-upload.js +3 -622
  97. package/dist/ui/filter.js +5 -380
  98. package/dist/ui/form.js +1 -1
  99. package/dist/ui/index.css +118 -1
  100. package/dist/ui/input.js +3 -1141
  101. package/dist/ui/menu.js +4 -828
  102. package/dist/ui/modal.js +4 -843
  103. package/dist/ui/nav.js +4 -223
  104. package/dist/ui/otp.js +4 -756
  105. package/dist/ui/radio.js +4 -317
  106. package/dist/ui/rich-text-editor.js +4 -1278
  107. package/dist/ui/select.js +5 -2138
  108. package/dist/ui/shadow/accordion.js +4 -273
  109. package/dist/ui/shadow/alert.js +4 -130
  110. package/dist/ui/shadow/breadcrumb.js +5 -744
  111. package/dist/ui/shadow/button-dropdown.js +6 -652
  112. package/dist/ui/shadow/button.js +3 -63
  113. package/dist/ui/shadow/card.js +4 -309
  114. package/dist/ui/shadow/chart.js +6 -1576
  115. package/dist/ui/shadow/checkbox.js +4 -334
  116. package/dist/ui/shadow/chip.js +4 -396
  117. package/dist/ui/shadow/date.js +7 -731
  118. package/dist/ui/shadow/drawer.js +4 -756
  119. package/dist/ui/shadow/dropdown-table.js +8 -1762
  120. package/dist/ui/shadow/dropdown.js +4 -508
  121. package/dist/ui/shadow/file-upload.js +4 -620
  122. package/dist/ui/shadow/filter.js +7 -379
  123. package/dist/ui/shadow/input.js +6 -1140
  124. package/dist/ui/shadow/menu.js +5 -828
  125. package/dist/ui/shadow/modal.js +6 -1063
  126. package/dist/ui/shadow/nav.js +5 -223
  127. package/dist/ui/shadow/otp.js +6 -754
  128. package/dist/ui/shadow/radio.js +4 -316
  129. package/dist/ui/shadow/rich-text-editor.js +4 -1276
  130. package/dist/ui/shadow/select.js +6 -2136
  131. package/dist/ui/shadow/sidebar.js +5 -581
  132. package/dist/ui/shadow/slider.js +5 -1010
  133. package/dist/ui/shadow/switch.js +4 -335
  134. package/dist/ui/shadow/table.js +10 -4501
  135. package/dist/ui/shadow/tabs.js +6 -315
  136. package/dist/ui/shadow/tag-input.js +7 -3057
  137. package/dist/ui/shadow/textarea.js +6 -817
  138. package/dist/ui/sidebar.js +3 -581
  139. package/dist/ui/slider.js +4 -1012
  140. package/dist/ui/switch.js +4 -336
  141. package/dist/ui/table.js +9 -4502
  142. package/dist/ui/tabs.js +4 -315
  143. package/dist/ui/tag-input.js +5 -3058
  144. package/dist/ui/textarea.js +4 -819
  145. package/dist/{vite-CivRCJaf.js → vite-CfW7z2wn.js} +21 -14
  146. package/dist/vite.js +1 -1
  147. package/dist/vue.d.ts +3 -2
  148. package/package.json +10 -11
  149. package/dist/chip-strip-bNfLn52Z.js +0 -215
  150. package/dist/data-source-controller-Dx6IQ2CN.js +0 -243
  151. package/dist/data-source-options-BM4r4PRw.js +0 -558
  152. package/dist/data-source-read-HyBCRg1t.js +0 -242
  153. package/dist/excel-BXzr3uI1.js +0 -784
  154. package/dist/export-ixRdCS2y.js +0 -2388
  155. package/dist/field-tooltip-addon-CWCp19mJ.js +0 -702
  156. package/dist/field-tooltip.css_raw-U5UeTa2d.js +0 -269
  157. package/dist/filter-builder-C0T3pMx2.js +0 -208
  158. package/dist/form-control-core-BbiACMtL.js +0 -226
  159. package/dist/icon-ClxnYhBG.js +0 -19
  160. package/dist/import-Dc4eXC02.js +0 -612
  161. package/dist/light-slots-_no0r2Z-.js +0 -246
  162. package/dist/markdown-uGlE_OfJ.js +0 -20191
  163. package/dist/mono-data-grid-e9ZVBlNN.js +0 -4240
  164. package/dist/normalize-error-BmLbgANw.js +0 -1048
@@ -1,4240 +0,0 @@
1
- import { t as createNotifier } from "./notifier-BIrqUzRh.js";
2
- import { a as parseFieldPath, c as toODataClause, l as toODataSelector, n as isPath, o as projectFields, r as mergePatch, s as setFieldValue, t as getFieldValue } from "./field-path-BabiMrrA.js";
3
- import { C as resolveSearchEntries, D as compileFilterPredicate, E as andPredicates, O as joinFilters, S as plainSearchColumns, T as andFilters, f as mergeSearchFields, g as customRemoteClause, h as customPredicate, n as describeError, o as isCanceled, u as DEFAULT_SEARCH_FIELDS, v as hasCustomSearch, w as searchEntryFor, y as hasWildcardSearch } from "./normalize-error-BmLbgANw.js";
4
- import { a as buildGroups, c as isGroupNode, i as searchFilterOf, o as collectGroupPaths, r as readAllRows, s as flattenLeaves } from "./data-source-read-HyBCRg1t.js";
5
- import { a as resolveDataSourceOptions, c as composeSummaryApply, d as createApplyGate, f as isRolledUp, h as monoArraySource, i as normalizeSortList, l as summaryAliases, m as storeRows, n as extraLoadOptions, o as toList, p as loadApply, s as buildSummaryApply, t as dataSourceOptionsKey, u as summaryFilterOf } from "./data-source-options-BM4r4PRw.js";
6
- import { n as unwrapReactive } from "./reactive-Dtd-esHO.js";
7
- import { t as arrayToODataString } from "./filter-odata-Bi27XUpQ.js";
8
- import { t as monoFilterBuilder } from "./filter-builder-B6Y2mmbM.js";
9
-
10
- //#region src/components/table/date-filter-tree.ts
11
- const DATE_LEVELS = [
12
- "year",
13
- "month",
14
- "day",
15
- "hour",
16
- "minute",
17
- "second"
18
- ];
19
- /** A `{ from, to }` pair of Dates (or ISO strings, which `setColumnFilter` also accepts). */
20
- function isDateRange(v) {
21
- if (!v || typeof v !== "object") return false;
22
- const o = v;
23
- const ok = (x) => x instanceof Date || typeof x === "string";
24
- return ok(o.from) && ok(o.to);
25
- }
26
- const BLANK_KEY = "blank";
27
- /**
28
- * A column value as a Date, or `null` for a blank / unparsable one. Accepts a
29
- * `Date`, a number (epoch ms), an ISO string, and a bare `YYYY-MM-DD` — the
30
- * last is parsed as a LOCAL date on purpose (`new Date('2026-03-05')` would be
31
- * UTC midnight and shift a day west of Greenwich).
32
- */
33
- function parseDateValue(v) {
34
- if (v == null || v === "") return null;
35
- if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
36
- if (typeof v === "number") {
37
- const d = new Date(v);
38
- return Number.isNaN(d.getTime()) ? null : d;
39
- }
40
- if (typeof v !== "string") return null;
41
- const s = v.trim();
42
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
43
- if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
44
- const d = new Date(s);
45
- return Number.isNaN(d.getTime()) ? null : d;
46
- }
47
- /** `[year, month(1-12), day, hour, minute, second]` of a Date, local or UTC. */
48
- function datePartsOf(d, utc = false) {
49
- return utc ? [
50
- d.getUTCFullYear(),
51
- d.getUTCMonth() + 1,
52
- d.getUTCDate(),
53
- d.getUTCHours(),
54
- d.getUTCMinutes(),
55
- d.getUTCSeconds()
56
- ] : [
57
- d.getFullYear(),
58
- d.getMonth() + 1,
59
- d.getDate(),
60
- d.getHours(),
61
- d.getMinutes(),
62
- d.getSeconds()
63
- ];
64
- }
65
- /** The Date at the START of the period the parts describe (missing parts = their minimum). */
66
- function dateFromParts(parts, utc) {
67
- const [y, mo = 1, d = 1, h = 0, mi = 0, s = 0] = parts;
68
- return utc ? new Date(Date.UTC(y, mo - 1, d, h, mi, s)) : new Date(y, mo - 1, d, h, mi, s);
69
- }
70
- /** The period `[from, to)` of a node at `level` with these parts. */
71
- function periodOf(parts, level, utc = false) {
72
- const i = DATE_LEVELS.indexOf(level);
73
- const own = parts.slice(0, i + 1);
74
- const next = [...own];
75
- next[i] += 1;
76
- return {
77
- from: dateFromParts(own, utc),
78
- to: dateFromParts(next, utc)
79
- };
80
- }
81
- function labelFor(parts, level, utc, locale) {
82
- const d = dateFromParts(parts, utc);
83
- const tz = utc ? { timeZone: "UTC" } : {};
84
- const two = (n) => String(n).padStart(2, "0");
85
- switch (level) {
86
- case "year": return String(parts[0]);
87
- case "month": return new Intl.DateTimeFormat(locale, {
88
- month: "long",
89
- ...tz
90
- }).format(d);
91
- case "day": return `${two(parts[2])} (${new Intl.DateTimeFormat(locale, {
92
- weekday: "short",
93
- ...tz
94
- }).format(d)})`;
95
- case "hour": return `${two(parts[3])}:00`;
96
- case "minute": return `${two(parts[3])}:${two(parts[4])}`;
97
- case "second": return `${two(parts[3])}:${two(parts[4])}:${two(parts[5])}`;
98
- }
99
- }
100
- /**
101
- * Fold distinct timestamps into the tree. Only periods that occur are built,
102
- * counts are summed up the branches, siblings are in chronological order, and
103
- * a blank value becomes one `(Blanks)` node at the top (never expandable).
104
- */
105
- function buildDateTree(values, options = {}) {
106
- const depth = DATE_LEVELS.indexOf(options.depth ?? "month");
107
- const utc = options.utc ?? false;
108
- const locale = options.locale;
109
- const roots = /* @__PURE__ */ new Map();
110
- let blanks = 0;
111
- for (const v of values) {
112
- const d = parseDateValue(v.value);
113
- const count = Number(v.count) || 0;
114
- if (!d) {
115
- blanks += count;
116
- continue;
117
- }
118
- const parts = datePartsOf(d, utc);
119
- let siblings = roots;
120
- for (let i = 0; i <= depth; i++) {
121
- const level = DATE_LEVELS[i];
122
- const own = parts.slice(0, i + 1);
123
- let n = siblings.get(parts[i]);
124
- if (!n) {
125
- const { from, to } = periodOf(own, level, utc);
126
- n = {
127
- key: own.join("-"),
128
- level,
129
- parts: own,
130
- label: labelFor(own, level, utc, locale),
131
- count: 0,
132
- from,
133
- to,
134
- children: []
135
- };
136
- siblings.set(parts[i], n);
137
- }
138
- n.count += count;
139
- siblings = childMap(n);
140
- }
141
- }
142
- const finish = (map) => [...map.entries()].sort(([a], [b]) => a - b).map(([, n]) => {
143
- n.children = finish(childMap(n));
144
- childMaps.delete(n);
145
- return n;
146
- });
147
- const out = finish(roots);
148
- if (blanks > 0) out.unshift({
149
- key: BLANK_KEY,
150
- level: "year",
151
- parts: [],
152
- label: "(Blanks)",
153
- count: blanks,
154
- from: null,
155
- to: null,
156
- children: []
157
- });
158
- return out;
159
- }
160
- const childMaps = /* @__PURE__ */ new WeakMap();
161
- function childMap(n) {
162
- let m = childMaps.get(n);
163
- if (!m) {
164
- m = /* @__PURE__ */ new Map();
165
- childMaps.set(n, m);
166
- }
167
- return m;
168
- }
169
- /**
170
- * A node's tick state from the set of ticked keys. A key in the set means the
171
- * WHOLE node is ticked (so every descendant is on too); otherwise the children
172
- * decide — all on → on, none → off, some (or a partly-ticked child) → mixed.
173
- *
174
- * Pass `roots` to have a ticked ANCESTOR count (the node is then on) — the
175
- * renderer walks top-down and passes `inheritedOn` instead, which is the same
176
- * answer without a path lookup per row.
177
- */
178
- function checkState(node, checked, roots, inheritedOn = false) {
179
- if (inheritedOn || checked.has(node.key)) return "on";
180
- if (roots && pathTo(node.key, roots).slice(0, -1).some((a) => checked.has(a.key))) return "on";
181
- if (!node.children.length) return "off";
182
- let on = 0;
183
- let mixed = false;
184
- for (const c of node.children) {
185
- const s = checkState(c, checked);
186
- if (s === "on") on++;
187
- else if (s === "mixed") mixed = true;
188
- }
189
- if (on === node.children.length) return "on";
190
- return on > 0 || mixed ? "mixed" : "off";
191
- }
192
- /**
193
- * Tick or untick a node: the node's own key goes in (and every descendant key
194
- * comes out — the node covers them) or the node and every descendant come out.
195
- * Ticking a child of a fully-ticked parent first expands the parent's tick to its
196
- * siblings, so unticking one day of a ticked month leaves the other days ticked.
197
- */
198
- function setChecked(node, on, checked, roots) {
199
- const next = new Set(checked);
200
- const path = pathTo(node.key, roots);
201
- for (let i = 0; i < path.length - 1; i++) {
202
- const anc = path[i];
203
- if (next.has(anc.key)) {
204
- next.delete(anc.key);
205
- for (const c of anc.children) next.add(c.key);
206
- }
207
- }
208
- const strip = (n) => {
209
- next.delete(n.key);
210
- n.children.forEach(strip);
211
- };
212
- strip(node);
213
- if (on) next.add(node.key);
214
- for (let i = path.length - 2; i >= 0; i--) {
215
- const anc = path[i];
216
- if (anc.children.length && anc.children.every((c) => next.has(c.key))) {
217
- anc.children.forEach((c) => next.delete(c.key));
218
- next.add(anc.key);
219
- }
220
- }
221
- return next;
222
- }
223
- function pathTo(key, roots) {
224
- const walk = (nodes, trail) => {
225
- for (const n of nodes) {
226
- if (n.key === key) return [...trail, n];
227
- const found = walk(n.children, [...trail, n]);
228
- if (found) return found;
229
- }
230
- return null;
231
- };
232
- return walk(roots, []) ?? [];
233
- }
234
- /** Tick everything (every root) or nothing. */
235
- function setAllChecked(roots, on) {
236
- return on ? new Set(roots.map((r) => r.key)) : /* @__PURE__ */ new Set();
237
- }
238
- /**
239
- * The ticked periods as filter values — one `{ from, to }` per fully-ticked node,
240
- * walking down only where a node is partly ticked. `(Blanks)` ticked adds a
241
- * `null` value (the column filter's "is blank"), matching what the plain header
242
- * filter sends for a blank.
243
- */
244
- function minimize(roots, checked) {
245
- const out = [];
246
- const walk = (n) => {
247
- if (checked.has(n.key)) {
248
- out.push(n.from && n.to ? {
249
- from: n.from,
250
- to: n.to
251
- } : null);
252
- return;
253
- }
254
- n.children.forEach(walk);
255
- };
256
- roots.forEach(walk);
257
- return out;
258
- }
259
- /**
260
- * The tick set that reproduces an applied filter (`table.columnFilter(field)`):
261
- * a node is ticked when one of the ranges covers its whole period; a blank value
262
- * ticks `(Blanks)`. A range that only partly covers a node ticks the children it
263
- * covers instead.
264
- */
265
- function seedFromRanges(roots, values) {
266
- const ranges = [];
267
- let blank = false;
268
- for (const v of values) if (v == null || v === "") blank = true;
269
- else if (isDateRange(v)) {
270
- const from = v.from instanceof Date ? v.from : new Date(v.from);
271
- const to = v.to instanceof Date ? v.to : new Date(v.to);
272
- if (!Number.isNaN(from.getTime()) && !Number.isNaN(to.getTime())) ranges.push({
273
- from,
274
- to
275
- });
276
- }
277
- const out = /* @__PURE__ */ new Set();
278
- const covers = (n) => !!n.from && !!n.to && ranges.some((r) => r.from.getTime() <= n.from.getTime() && r.to.getTime() >= n.to.getTime());
279
- const walk = (n) => {
280
- if (n.key === "blank") {
281
- if (blank) out.add(n.key);
282
- return;
283
- }
284
- if (covers(n)) {
285
- out.add(n.key);
286
- return;
287
- }
288
- n.children.forEach(walk);
289
- };
290
- roots.forEach(walk);
291
- return out;
292
- }
293
- /** Whether a Date falls in `[from, to)`. */
294
- function inRange(d, r) {
295
- const t = d.getTime();
296
- return t >= r.from.getTime() && t < r.to.getTime();
297
- }
298
-
299
- //#endregion
300
- //#region src/components/table/store-group-source.ts
301
- /**
302
- * Build a {@link MonoServerGroupSource} that drives **server-side** group paging
303
- * straight from a devextreme `DataSource`'s store — no extra fetcher, no custom
304
- * callbacks. `loadGroups` issues one cheap grouped query (group keys + counts +
305
- * optional summaries, no rows); `loadRows` fetches a single group's page with
306
- * `$filter` + `$skip`/`$top`. Returns `null` if the source has no usable store
307
- * (e.g. a plain in-memory array), so the caller can fall back to client grouping.
308
- */
309
- function storeGroupSource(source, config) {
310
- const store = source.store?.();
311
- if (!store || typeof store.load !== "function") return null;
312
- const { groupField, select, searchExpr, searchOperation, groupSummary } = config;
313
- const summaryDesc = groupSummary ? Object.entries(groupSummary).map(([selector, summaryType]) => ({
314
- selector,
315
- summaryType
316
- })) : void 0;
317
- /**
318
- * Build a devextreme filter expression for the active search (or undefined).
319
- * Delegates to the shared builder, which takes the search state off a source;
320
- * here the search arrives per-call via `ctx`, so a minimal stand-in carries it.
321
- */
322
- function searchFilter(search) {
323
- if (!search || !searchExpr) return void 0;
324
- return searchFilterOf({
325
- load: () => [],
326
- items: () => [],
327
- searchValue: () => search,
328
- searchExpr: () => searchExpr,
329
- searchOperation: () => searchOperation ?? "contains"
330
- });
331
- }
332
- /** devextreme `store.load` returns either an array or `{ data, … }`. */
333
- const rows = (res) => Array.isArray(res) ? res : res?.data ?? [];
334
- return {
335
- async loadGroups(ctx) {
336
- const filter = joinFilters([ctx.filter, searchFilter(ctx.search)], "and");
337
- const res = await store.load({
338
- group: [{
339
- selector: groupField,
340
- isExpanded: false
341
- }],
342
- ...summaryDesc ? { groupSummary: summaryDesc } : {},
343
- requireGroupCount: true,
344
- ...filter ? { filter } : {},
345
- sort: ctx.sort ?? [{ selector: groupField }],
346
- ...ctx.loadOptions ?? {}
347
- });
348
- return rows(res).map((g) => ({
349
- key: g.key,
350
- count: Number(g.count ?? (Array.isArray(g.items) ? g.items.length : 0)) || 0,
351
- aggregates: summaryDesc && Array.isArray(g.summary) ? Object.fromEntries(summaryDesc.map((s, i) => [s.selector, Number(g.summary[i]) || 0])) : void 0
352
- }));
353
- },
354
- async loadRows(key, ctx) {
355
- const groupFilter = [
356
- groupField,
357
- "=",
358
- key
359
- ];
360
- const rowSelect = select ?? ctx.select;
361
- const res = await store.load({
362
- filter: joinFilters([
363
- groupFilter,
364
- ctx.filter,
365
- searchFilter(ctx.search)
366
- ], "and"),
367
- ...rowSelect ? { select: rowSelect } : {},
368
- ...ctx.sort ? { sort: ctx.sort } : {},
369
- skip: ctx.skip,
370
- take: ctx.take,
371
- requireTotalCount: false,
372
- ...ctx.loadOptions ?? {}
373
- });
374
- return rows(res);
375
- }
376
- };
377
- }
378
-
379
- //#endregion
380
- //#region src/components/table/mono-data-grid.ts
381
- function monoDataGrid(ds = null, opts = {}) {
382
- let bound = null;
383
- let searchFieldsOverride;
384
- let searchFieldsCache = null;
385
- /**
386
- * The configured search fields, or `undefined` when none of the four spellings
387
- * was given. `undefined` is load-bearing: it means "not configured", which lets
388
- * a data source keep using its own `searchExpr` instead of being handed `[]`.
389
- *
390
- * An override set through {@link setSearchExpr} (i.e. by `<mono-table-search>`)
391
- * wins over the options — the element is the more specific declaration.
392
- */
393
- function searchFields() {
394
- if (searchFieldsOverride !== void 0) return searchFieldsOverride;
395
- const keys = [
396
- opts.searchExpr,
397
- opts["search-expr"],
398
- opts.searchValue,
399
- opts["search-value"]
400
- ];
401
- if (searchFieldsCache && searchFieldsCache.keys.length === keys.length && searchFieldsCache.keys.every((k, i) => k === keys[i])) return searchFieldsCache.entries;
402
- const entries = mergeSearchFields(opts);
403
- searchFieldsCache = {
404
- keys,
405
- entries
406
- };
407
- return entries;
408
- }
409
- /** Whether the CONSUMER declared fields (any spelling, or via `setSearchValue`). */
410
- function searchFieldsConfigured() {
411
- return searchFields() !== void 0;
412
- }
413
- /**
414
- * Whether the bound SOURCE carries its own column list.
415
- *
416
- * A devextreme DataSource can be built with `searchExpr` of its own, and a grid
417
- * that declares nothing relies on that source folding the search itself. The
418
- * `'*'` default must not take that over — it would replace a correct server-side
419
- * search with one derived from the loaded page.
420
- */
421
- function sourceDeclaresSearchFields() {
422
- const e = bound?.searchExpr?.();
423
- return Array.isArray(e) ? e.length > 0 : !!e;
424
- }
425
- /**
426
- * The fields actually searched: what the consumer configured, else what the
427
- * source declares (left to the source — see above), else the **default `'*'`**,
428
- * meaning every top-level field.
429
- *
430
- * `'*'` is resolved against real rows by `searchEntries()` and never leaves the
431
- * client as a literal — it expands to concrete column names first.
432
- */
433
- function effectiveSearchFields() {
434
- const configured = searchFields();
435
- if (configured !== void 0) return configured;
436
- if (sourceDeclaresSearchFields()) return void 0;
437
- return [...DEFAULT_SEARCH_FIELDS];
438
- }
439
- let groupFields = toFields(opts.group);
440
- const collapsed = /* @__PURE__ */ new Set();
441
- let groupPageSize = Math.max(1, opts.pageSize ?? 10);
442
- let groupPageIndex = 0;
443
- let pageSizeAll = false;
444
- const chunkSize = Math.max(1, opts.chunkSize ?? 100);
445
- let suppressSync = false;
446
- let allRows = [];
447
- let browserRows = null;
448
- let browserPageIndex = 0;
449
- let browserPageSize = 0;
450
- let scrollMode = "off";
451
- let accumulated = [];
452
- let scrollNextPage = 0;
453
- let scrollHasMore = false;
454
- let scrollLoading = false;
455
- let scrollToken = 0;
456
- let preferredPageSize = null;
457
- let sourcePageSize = null;
458
- let vStart = 0;
459
- let vEnd = 0;
460
- let vPadTop = 0;
461
- let vPadBottom = 0;
462
- const groupRowPaging = /* @__PURE__ */ new Map();
463
- const defaultGroupRowPageSize = opts.groupRowPageSize && opts.groupRowPageSize > 0 ? Math.floor(opts.groupRowPageSize) : 0;
464
- /** Effective row paging for a group: explicit registration, else the default. */
465
- function groupRowPagingFor(path) {
466
- const explicit = groupRowPaging.get(path);
467
- if (explicit) return explicit;
468
- return defaultGroupRowPageSize ? {
469
- pageIndex: 0,
470
- pageSize: defaultGroupRowPageSize
471
- } : null;
472
- }
473
- const pathIndex = /* @__PURE__ */ new Map();
474
- let serverGroup = opts.serverGroup ?? null;
475
- let serverGroups = [];
476
- const groupRows = /* @__PURE__ */ new Map();
477
- const groupRowsKey = /* @__PURE__ */ new Map();
478
- const groupRowLoading = /* @__PURE__ */ new Set();
479
- let searchValue = null;
480
- let groupSeq = 0;
481
- const isGrouped = () => groupFields.length > 0;
482
- /** Server mode requires a single group field + a `serverGroup` provider. */
483
- const serverMode = () => !!serverGroup && groupFields.length === 1;
484
- function clearRowCaches() {
485
- groupRows.clear();
486
- groupRowsKey.clear();
487
- groupRowLoading.clear();
488
- }
489
- const formOps = [];
490
- let formKey = null;
491
- let formShowForm = false;
492
- let formTempSeq = 0;
493
- let formHandle = null;
494
- const MONO_ROW_KEY = Symbol("monoFormRowKey");
495
- const EDITABLE_MARK = "Editable";
496
- const ctrl = {
497
- items: [],
498
- mapped: [],
499
- displayRows: [],
500
- loading: false,
501
- hasLoaded: false,
502
- error: null,
503
- totalCount: 0,
504
- pageIndex: 0,
505
- pageSize: 0,
506
- pageSizeAll: false,
507
- pageCount: 0,
508
- searchTerms: [],
509
- sorts: [],
510
- sortField: null,
511
- sortOrder: null,
512
- grouped: groupFields.length > 0,
513
- scrollMode: "off",
514
- hasMore: false,
515
- loadedCount: 0,
516
- virtualStart: 0,
517
- virtualEnd: 0,
518
- virtualPadTop: 0,
519
- virtualPadBottom: 0,
520
- dataSource: null,
521
- editingKey: null,
522
- editableTrigger: opts.editableTrigger ?? "click",
523
- editorNavKeys: opts.editorNavKeys ?? "tab-arrows",
524
- onCellChange: null,
525
- pendingData: formOps,
526
- load,
527
- reload,
528
- clear,
529
- setPage,
530
- setScrollPaging,
531
- loadNext,
532
- setVirtualWindow,
533
- setPreferredPageSize,
534
- setPageSize,
535
- setSearch,
536
- setSearchTerms,
537
- setSearchValue: setSearchExpr,
538
- setSearchExpr,
539
- searchValue: effectiveSearchFields,
540
- searchExpr: effectiveSearchFields,
541
- setFilter,
542
- setDataSourceOptions,
543
- setOdataOptions,
544
- refresh,
545
- resolvedDataSourceOptions,
546
- setColumnFilter,
547
- columnFilterCombining,
548
- errorBehaviour,
549
- setErrorBehaviour,
550
- columnFilter,
551
- filteredColumns,
552
- distinctValues,
553
- setSort,
554
- sortCombining,
555
- sortOf,
556
- sortIndex,
557
- clearSort,
558
- columnMap,
559
- setData,
560
- setGroup,
561
- toggleGroup,
562
- isGroupCollapsed,
563
- expandAllGroups,
564
- collapseAllGroups,
565
- setGroupPageSize,
566
- setGroupPage,
567
- clearGroupPaging,
568
- groupPageInfo,
569
- groupNode,
570
- getData,
571
- summary,
572
- summaryText,
573
- setSummaryResolver,
574
- registerSummary,
575
- unregisterSummary,
576
- summaries,
577
- buildGroups: buildGroupTree,
578
- export: exportReport,
579
- import: importSheet,
580
- isCellImported,
581
- isRowImported,
582
- bind,
583
- subscribe,
584
- dispose,
585
- registerColumn,
586
- unregisterColumn,
587
- detail,
588
- check,
589
- registerDetail,
590
- unregisterDetail,
591
- columns,
592
- props,
593
- registeredColumns,
594
- editableColumns,
595
- moveEditor,
596
- bindRowTrigger,
597
- beginEditRow,
598
- cancelEdit,
599
- isEditingRow,
600
- isEditingCell,
601
- commitCell,
602
- editorKeydown,
603
- stageCell,
604
- cellValue,
605
- isCellDirty,
606
- isRowDirty,
607
- pendingCount,
608
- hasChanges,
609
- changes,
610
- discardChanges,
611
- saveChanges,
612
- form,
613
- rowKey
614
- };
615
- if (opts.filterBuilder) ctrl.filterBuilder = monoFilterBuilder({
616
- ...opts.filterBuilder,
617
- dataGrid: ctrl
618
- });
619
- const columnEls = /* @__PURE__ */ new Set();
620
- const pending = /* @__PURE__ */ new Map();
621
- function normalizeSummary(cfg) {
622
- if (!cfg) return {
623
- specs: [],
624
- recalcSearching: false,
625
- recalcChangedData: true,
626
- resolve: null
627
- };
628
- if (Array.isArray(cfg)) return {
629
- specs: [...cfg],
630
- recalcSearching: false,
631
- recalcChangedData: true,
632
- resolve: null
633
- };
634
- const specs = [];
635
- for (const [field, spec] of Object.entries(cfg.fields ?? {})) {
636
- const list = Array.isArray(spec) ? spec : [spec];
637
- for (const s of list) specs.push({
638
- field,
639
- ...s
640
- });
641
- }
642
- return {
643
- specs,
644
- recalcSearching: cfg.recalculate?.searching ?? false,
645
- recalcChangedData: cfg.recalculate?.changedData ?? true,
646
- resolve: cfg.resolve ?? null
647
- };
648
- }
649
- const _summaryCfg = normalizeSummary(opts.summary);
650
- const summarySpecs = _summaryCfg.specs;
651
- for (const col of opts.props?.th ?? []) {
652
- if (!col?.field || !col.summary) continue;
653
- const spec = {
654
- field: col.field,
655
- ...col.summary
656
- };
657
- if (!summarySpecs.some((s) => s.field === spec.field && (s.type ?? "sum") === (spec.type ?? "sum") && s.name === spec.name)) summarySpecs.push(spec);
658
- }
659
- const recalcSearching = _summaryCfg.recalcSearching;
660
- const recalcChangedData = _summaryCfg.recalcChangedData;
661
- let summaryResolve = _summaryCfg.resolve;
662
- const summaryCache = /* @__PURE__ */ new Map();
663
- /**
664
- * The `$apply`-through-the-store switch, shared by the summary footer and the
665
- * header filter: off by option, or off for good once the backend rejected one.
666
- */
667
- const applyGate = createApplyGate(opts.serverApply !== false);
668
- let errorMode = opts.onError === "keep-list" ? "keep-list" : "clear-list";
669
- function errorBehaviour() {
670
- return errorMode;
671
- }
672
- function setErrorBehaviour(next) {
673
- errorMode = next === "keep-list" ? "keep-list" : "clear-list";
674
- }
675
- const summaryKey = (spec) => `${spec.field ?? "*"}|${spec.type ?? "sum"}|${spec.name ?? ""}`;
676
- /** Find the spec that a `(field, type)` read refers to. Type is matched only when given. */
677
- function findSummarySpec(field, type) {
678
- return summarySpecs.find((s) => (s.field ?? "") === field && (type === void 0 || (s.type ?? "sum") === type));
679
- }
680
- /**
681
- * How many elements currently want each spec.
682
- *
683
- * Registration is REFCOUNTED because several `<mono-table-summary>` cells can
684
- * declare the same field (a footer total and a toolbar total, say). Only the
685
- * first pushes the spec; previously the first to unmount removed the shared spec
686
- * out from under the others, and since nothing re-runs their registration they
687
- * rendered an empty value forever.
688
- */
689
- const summaryRefs = /* @__PURE__ */ new Map();
690
- function registerSummary(spec) {
691
- const key = summaryKey(spec);
692
- summaryRefs.set(key, (summaryRefs.get(key) ?? 0) + 1);
693
- if (summarySpecs.some((s) => summaryKey(s) === key)) return;
694
- summarySpecs.push(spec);
695
- recomputeSummaries(true);
696
- }
697
- function unregisterSummary(spec) {
698
- const key = summaryKey(spec);
699
- const refs = (summaryRefs.get(key) ?? 1) - 1;
700
- if (refs > 0) {
701
- summaryRefs.set(key, refs);
702
- return;
703
- }
704
- summaryRefs.delete(key);
705
- const i = summarySpecs.findIndex((s) => summaryKey(s) === key);
706
- if (i >= 0) summarySpecs.splice(i, 1);
707
- }
708
- function summaries() {
709
- return summarySpecs;
710
- }
711
- /** Build the public result object for one spec from the cache. */
712
- function summaryResult(spec) {
713
- const value = summaryCache.get(summaryKey(spec)) ?? null;
714
- return {
715
- field: spec.field,
716
- type: spec.type ?? "sum",
717
- name: spec.name,
718
- value,
719
- text: formatSummary(value, spec)
720
- };
721
- }
722
- const summaryHandle = {
723
- get(field, type) {
724
- const spec = findSummarySpec(field, type);
725
- return spec ? summaryResult(spec) : null;
726
- },
727
- getAll() {
728
- return summarySpecs.map(summaryResult);
729
- }
730
- };
731
- function summary(field, type) {
732
- if (field === void 0) return summaryHandle;
733
- let spec = findSummarySpec(field, type);
734
- if (!spec) {
735
- spec = {
736
- field,
737
- type: type ?? "sum"
738
- };
739
- registerSummary(spec);
740
- }
741
- return summaryCache.get(summaryKey(spec)) ?? null;
742
- }
743
- let summaryVersion = 0;
744
- let summaryComputed = -1;
745
- /**
746
- * Bumped on EVERY row-set change, unconditionally — `summaryVersion` is gated
747
- * by the `recalculate` config, so it can't be used to answer "is the row set
748
- * still the one I last read?". `check().selectAll()` uses this to know whether
749
- * its cached drain is still valid.
750
- */
751
- let rowSetVersion = 0;
752
- const bumpSummary = (reason) => {
753
- rowSetVersion++;
754
- if (reason === "search" && !recalcSearching) return;
755
- if (reason === "data" && !recalcChangedData && summaryComputed >= 0) return;
756
- summaryVersion++;
757
- };
758
- function summaryText(field, type) {
759
- const spec = findSummarySpec(field, type) ?? {
760
- field,
761
- type: type ?? "sum"
762
- };
763
- return formatSummary(summary(field, type), spec);
764
- }
765
- function formatSummary(value, spec) {
766
- if (spec.format) return spec.format(value, ctrl.items);
767
- if (value === null || Number.isNaN(value)) return spec.emptyText ?? "—";
768
- const body = spec.precision != null ? value.toLocaleString(void 0, {
769
- minimumFractionDigits: spec.precision,
770
- maximumFractionDigits: spec.precision
771
- }) : value.toLocaleString();
772
- return `${spec.prefix ?? ""}${body}${spec.suffix ?? ""}`;
773
- }
774
- /** Aggregate one spec over `rows`. Non-numeric values are skipped. */
775
- function aggregate(spec, rows) {
776
- const type = spec.type ?? "sum";
777
- if (type === "count") return rows.length;
778
- const field = spec.field;
779
- if (!field) return null;
780
- const valuesOf = (row) => {
781
- const raw = getFieldValue(row, field);
782
- return Array.isArray(raw) ? raw : [raw];
783
- };
784
- if (type === "countDistinct") {
785
- const seen = /* @__PURE__ */ new Set();
786
- for (const row of rows) for (const v of valuesOf(row)) if (v != null && v !== "") seen.add(v);
787
- return seen.size;
788
- }
789
- const nums = [];
790
- for (const row of rows) for (const raw of valuesOf(row)) {
791
- if (raw == null || raw === "") continue;
792
- const n = Number(raw);
793
- if (Number.isFinite(n)) nums.push(n);
794
- }
795
- if (!nums.length) return null;
796
- switch (type) {
797
- case "sum": return nums.reduce((a, b) => a + b, 0);
798
- case "avg": return nums.reduce((a, b) => a + b, 0) / nums.length;
799
- case "min": return Math.min(...nums);
800
- case "max": return Math.max(...nums);
801
- default: return null;
802
- }
803
- }
804
- /**
805
- * Run the consumer's resolver, normalised to one value per spec.
806
- *
807
- * Declines (`null`) for every case the caller should not have to think about:
808
- * no resolver, no bound source, an ARRAY source (already in memory — a network
809
- * round trip for rows we hold would be absurd), a spec set no `$apply` can
810
- * express, or a resolver that returned the wrong shape.
811
- *
812
- * A THROW is caught and reported once rather than propagated: a failed totals
813
- * request should degrade to a slower total, not break the grid. It is warned
814
- * about exactly once per grid so a persistent fault is visible without
815
- * flooding the console on every reload.
816
- */
817
- let _summaryResolveWarned = false;
818
- /**
819
- * Swap the resolver after construction.
820
- *
821
- * The option form covers a grid you build yourself. This covers the other
822
- * case, which is at least as common: a grid built by a shared factory or
823
- * composable that does not forward `summary` — the consumer still owns the
824
- * fetcher, and needs somewhere to hand it over.
825
- *
826
- * Forces a recompute rather than waiting for the next data change, so a footer
827
- * already on screen with a locally-drained total corrects itself, and clears
828
- * the one-shot warning so a newly attached resolver gets a fair hearing.
829
- */
830
- function setSummaryResolver(resolve) {
831
- summaryResolve = resolve ?? null;
832
- _summaryResolveWarned = false;
833
- recomputeSummaries(true);
834
- }
835
- /**
836
- * A resolver's (or the server's) answer → one value per spec, in spec order.
837
- * Positional, or keyed by alias — falling back to the plain field name, which
838
- * is what an `$apply` aliases to whenever a field carries one aggregate (the
839
- * ordinary case). `null` when the shape cannot be read.
840
- */
841
- function summaryOutToValues(out) {
842
- const num = (v) => {
843
- const n = Number(v);
844
- return Number.isFinite(n) ? n : null;
845
- };
846
- if (Array.isArray(out)) return out.length === summarySpecs.length ? out.map(num) : null;
847
- if (!out || typeof out !== "object") return null;
848
- const row = out;
849
- const aliases = summaryAliases(summarySpecs);
850
- return summarySpecs.map((spec, i) => {
851
- const key = aliases[i];
852
- if (key in row) return num(row[key]);
853
- if (spec.field && spec.field in row) return num(row[spec.field]);
854
- return null;
855
- });
856
- }
857
- async function resolveSummaries() {
858
- if (!summaryResolve || !bound || Array.isArray(bound)) return null;
859
- const apply = buildSummaryApply(summarySpecs);
860
- if (!apply) return null;
861
- const filter = summaryFilterOf(bound);
862
- const aliases = summaryAliases(summarySpecs);
863
- try {
864
- const out = await summaryResolve({
865
- specs: summarySpecs,
866
- aliases,
867
- filter,
868
- odataFilter: arrayToODataString(filter) ?? "",
869
- apply,
870
- applyWithFilter: composeSummaryApply(apply, filter),
871
- source: bound
872
- });
873
- if (!out) return null;
874
- return summaryOutToValues(out);
875
- } catch (err) {
876
- if (!_summaryResolveWarned) {
877
- _summaryResolveWarned = true;
878
- console.warn("[mono-table] summary resolver failed; falling back to reading every row. Return \"null\" from the resolver to fall back without this warning.", err);
879
- }
880
- return null;
881
- }
882
- }
883
- /**
884
- * The totals as ONE `$apply=filter(…)/aggregate(…)` request through the bound
885
- * source's own store — the same clause the consumer resolver is offered, sent
886
- * by the grid itself when there is no resolver (or it declined).
887
- *
888
- * `null` is the decline: no store, the path switched off, a store that cannot
889
- * carry an `$apply` (`loadApply`), a response that is not one aggregated row
890
- * (a server that ignored the clause hands back entities, which carry none of
891
- * the aliases), or a failed request — which also reports to the gate, so a
892
- * backend that rejects `$apply` is asked exactly once.
893
- */
894
- async function resolveSummariesViaStore() {
895
- if (applyGate.skip || !bound || Array.isArray(bound) || !hasStore()) return null;
896
- const apply = buildSummaryApply(summarySpecs);
897
- if (!apply) return null;
898
- let rows;
899
- try {
900
- rows = await loadApply(bound.store(), composeSummaryApply(apply, summaryFilterOf(bound)));
901
- } catch (err) {
902
- applyGate.reject(err);
903
- return null;
904
- }
905
- if (!rows || rows.length !== 1 || !isRolledUp(rows, summaryAliases(summarySpecs))) return null;
906
- return summaryOutToValues(rows[0]);
907
- }
908
- let _summaryRun = 0;
909
- /**
910
- * Recompute every registered summary over the full filtered set and notify.
911
- * No-op when nothing is registered (an unused grid never fetches) or — unless
912
- * `force`d — when the set hasn't changed since the last compute (so sort/paging
913
- * don't refetch). A run token guards against overlapping async reads landing
914
- * out of order.
915
- *
916
- * BURST COALESCING: a footer mounts many `<mono-table-summary>` elements and each
917
- * registration forces a recompute — Lit elements upgrade across separate tasks, so a
918
- * 48-column footer used to fire one resolver request PER element, each carrying only
919
- * the specs registered so far (the `$apply` aggregate list visibly grew 1 → 2 → N).
920
- * While one recompute is in flight, later ones only mark `_summaryQueued` and return;
921
- * a single trailing recompute then runs with the FULL spec set. A mount burst is
922
- * capped at leading + trailing requests whatever N is, and the trailing run's cache
923
- * write is what the footer finally shows.
924
- */
925
- let _summaryInFlight = false;
926
- let _summaryQueued = false;
927
- async function recomputeSummaries(force = false) {
928
- if (!summarySpecs.length) return;
929
- if (!force && summaryComputed === summaryVersion) return;
930
- if (_summaryInFlight) {
931
- _summaryQueued = true;
932
- return;
933
- }
934
- _summaryInFlight = true;
935
- const run = ++_summaryRun;
936
- const captured = summaryVersion;
937
- try {
938
- let resolved = await resolveSummaries();
939
- if (run !== _summaryRun) return;
940
- if (!resolved) {
941
- resolved = await resolveSummariesViaStore();
942
- if (run !== _summaryRun) return;
943
- }
944
- if (resolved) {
945
- summarySpecs.forEach((spec, i) => {
946
- summaryCache.set(summaryKey(spec), resolved[i] ?? null);
947
- });
948
- summaryComputed = captured;
949
- notify();
950
- return;
951
- }
952
- const rows = await getData();
953
- if (run !== _summaryRun) return;
954
- for (const spec of summarySpecs) summaryCache.set(summaryKey(spec), aggregate(spec, rows));
955
- summaryComputed = captured;
956
- notify();
957
- } finally {
958
- _summaryInFlight = false;
959
- if (_summaryQueued) {
960
- _summaryQueued = false;
961
- recomputeSummaries(true);
962
- }
963
- }
964
- }
965
- function registerColumn(el) {
966
- columnEls.add(el);
967
- }
968
- function unregisterColumn(el) {
969
- columnEls.delete(el);
970
- if (columnEls.size === 0) unbindRowTrigger();
971
- }
972
- const detailEls = /* @__PURE__ */ new Set();
973
- function registerDetail(el) {
974
- detailEls.add(el);
975
- }
976
- function unregisterDetail(el) {
977
- detailEls.delete(el);
978
- }
979
- const detailHandle = {
980
- expandAll() {
981
- for (const el of detailEls) el.open = true;
982
- notify();
983
- },
984
- collapseAll() {
985
- for (const el of detailEls) if (!el.stayOpen) el.open = false;
986
- notify();
987
- },
988
- collapseOthers(except) {
989
- let changed = false;
990
- for (const el of detailEls) {
991
- if (el === except || el.stayOpen || !el.open) continue;
992
- el.open = false;
993
- changed = true;
994
- }
995
- if (changed) notify();
996
- },
997
- openCount() {
998
- let open = 0;
999
- for (const el of detailEls) if (el.open) open++;
1000
- return open;
1001
- },
1002
- getAll: () => domOrdered(Array.from(detailEls))
1003
- };
1004
- function detail() {
1005
- return detailHandle;
1006
- }
1007
- const checkedRows = /* @__PURE__ */ new Map();
1008
- let checkConfig = {};
1009
- let checkPending = false;
1010
- /**
1011
- * Bumped on every selection notify (a rejected toggle included — its subscribers repaint the
1012
- * revert), so a listener can tell a selection change from the grid's other notifies without
1013
- * diffing the store. See `MonoCheckHandle.version`.
1014
- */
1015
- let checkVersion = 0;
1016
- function checkChanged() {
1017
- checkVersion += 1;
1018
- notify();
1019
- }
1020
- /** `keyValue` as a list (empty = project nothing, i.e. keep the whole row). */
1021
- function checkKeys() {
1022
- const k = checkConfig.keyValue;
1023
- if (!k) return [];
1024
- return (Array.isArray(k) ? k : [k]).filter(Boolean);
1025
- }
1026
- /**
1027
- * Columns to fetch while draining, or `null` for "everything".
1028
- *
1029
- * `keyExpr` is normally included, even when the consumer only asked for other
1030
- * fields: `rowKeyOf` reads `row[keyExpr]` and falls back to the array INDEX
1031
- * when it's missing, so a drain without it would key every row by its position
1032
- * and collide with the loaded page. A `[*]` path can't be expressed as
1033
- * `$select` at all (it needs an expand), so any wildcard drops the optimisation.
1034
- *
1035
- * EXCEPT when `keyExpr` names a field the rows do not have — a misconfigured
1036
- * (or defaulted) key would otherwise put a non-existent column on the wire and
1037
- * the server rejects the whole drain: OData answers `$select=Id` on an entity
1038
- * without `Id` with a 400. A wrong key should cost a wider fetch, not a failed
1039
- * one, so drop the optimisation and read whole rows instead.
1040
- */
1041
- function checkSelect() {
1042
- const keys = checkKeys();
1043
- if (!keys.length) return null;
1044
- const cols = [];
1045
- for (const key of keys) {
1046
- if (parseFieldPath(key).hasWildcard) return null;
1047
- const selector = toODataSelector(key);
1048
- if (!selector) return null;
1049
- cols.push(selector);
1050
- }
1051
- if (!cols.includes(keyExpr)) {
1052
- if (!rowsHaveKeyExpr()) {
1053
- warnMissingKeyExpr();
1054
- return null;
1055
- }
1056
- cols.push(keyExpr);
1057
- }
1058
- return cols;
1059
- }
1060
- /** A positive integer limit, or undefined when the config has none. */
1061
- function limitOf(value) {
1062
- if (value == null) return void 0;
1063
- const n = Math.floor(Number(value));
1064
- return Number.isFinite(n) && n >= 0 ? n : void 0;
1065
- }
1066
- function checkLimits() {
1067
- const max = limitOf(checkConfig.max);
1068
- const min = limitOf(checkConfig.min);
1069
- return {
1070
- ...max !== void 0 ? { max } : {},
1071
- ...min !== void 0 ? { min } : {}
1072
- };
1073
- }
1074
- /**
1075
- * Add rows up to the cap. Rows already selected never count against the room
1076
- * left — re-selecting a drained set the user has partly unticked must still be
1077
- * a no-op for what is there. When `capped` is false (`replace`) the cap is
1078
- * ignored: a value the developer pushes in is theirs.
1079
- */
1080
- function addChecked(rows, capped = true) {
1081
- const max = capped ? checkLimits().max : void 0;
1082
- rows.forEach((row, i) => {
1083
- const key = rowKeyOf(row, i);
1084
- if (!checkedRows.has(key) && max !== void 0 && checkedRows.size >= max) return;
1085
- checkedRows.set(key, row);
1086
- });
1087
- }
1088
- /**
1089
- * The last completed drain, so "select all → untick one → select all again"
1090
- * costs nothing instead of walking the whole source a second time.
1091
- *
1092
- * Deliberately NOT an OData `not in (…)` query over the already-selected keys:
1093
- * the exclusion list is largest exactly when the selection is largest, so
1094
- * re-selecting after unticking one of 1,085 rows would mean putting 1,084 ids
1095
- * in a URL — past every practical URL limit, and slower than the paged drain it
1096
- * replaces. Remembering the drain is the cheap direction.
1097
- *
1098
- * One entry, invalidated whenever the ROW SET could differ: a different
1099
- * filter/search (compared by reference, since the controller builds a fresh
1100
- * expression for every real change), a different `$select` projection, or any
1101
- * `bumpSummary` (bind / reload / setData / search / filter). Sorting and paging
1102
- * deliberately do NOT invalidate — they reorder or window the same set.
1103
- */
1104
- let drainedRows = null;
1105
- let drainedFilter = null;
1106
- let drainedSearch = null;
1107
- let drainedTerms = "";
1108
- let drainedSelect = "";
1109
- let drainedVersion = -1;
1110
- function drainIsReusable() {
1111
- if (!drainedRows) return false;
1112
- return Object.is(drainedFilter, bound?.filter?.() ?? null) && Object.is(drainedSearch, bound?.searchValue?.() ?? null) && drainedTerms === JSON.stringify(ctrl.searchTerms) && drainedSelect === JSON.stringify(checkSelect()) && drainedVersion === rowSetVersion;
1113
- }
1114
- function rememberDrain(rows) {
1115
- drainedRows = rows;
1116
- drainedFilter = bound?.filter?.() ?? null;
1117
- drainedSearch = bound?.searchValue?.() ?? null;
1118
- drainedTerms = JSON.stringify(ctrl.searchTerms);
1119
- drainedSelect = JSON.stringify(checkSelect());
1120
- drainedVersion = rowSetVersion;
1121
- }
1122
- const checkHandle = {
1123
- getAll() {
1124
- const keys = checkKeys();
1125
- return [...checkedRows.values()].map((row) => projectFields(row, keys));
1126
- },
1127
- count: () => checkedRows.size,
1128
- rows: () => [...checkedRows.values()],
1129
- isChecked(row) {
1130
- return checkedRows.has(typeof row === "string" ? row : rowKey(row));
1131
- },
1132
- toggle(item, checked) {
1133
- if (item == null) return;
1134
- const key = rowKey(item);
1135
- const has = checkedRows.has(key);
1136
- const next = checked === void 0 ? !has : checked;
1137
- const { max, min } = checkLimits();
1138
- if (!(next && !has && max !== void 0 && checkedRows.size >= max || !next && has && min !== void 0 && checkedRows.size <= min)) {
1139
- if (next) checkedRows.set(key, item);
1140
- else checkedRows.delete(key);
1141
- }
1142
- checkChanged();
1143
- },
1144
- selectPage() {
1145
- addChecked(ctrl.items);
1146
- checkChanged();
1147
- },
1148
- replace(rows) {
1149
- checkedRows.clear();
1150
- addChecked(rows, false);
1151
- checkChanged();
1152
- },
1153
- async selectAll() {
1154
- if (checkPending) return;
1155
- if (drainIsReusable()) {
1156
- addChecked(drainedRows);
1157
- checkChanged();
1158
- return;
1159
- }
1160
- checkPending = true;
1161
- checkChanged();
1162
- try {
1163
- const select = checkSelect();
1164
- const rows = await readAllRows(bound, {
1165
- chunkSize: Math.max(1, Math.floor(checkConfig.chunk ?? 100)),
1166
- ...select ? { select } : {},
1167
- sort: mergedSort()
1168
- });
1169
- rememberDrain(rows);
1170
- addChecked(rows);
1171
- clearError();
1172
- } catch (err) {
1173
- captureError(err, "selectAll");
1174
- throw err;
1175
- } finally {
1176
- checkPending = false;
1177
- checkChanged();
1178
- }
1179
- },
1180
- clear() {
1181
- if (!checkedRows.size) return;
1182
- const min = checkLimits().min ?? 0;
1183
- if (min > 0) {
1184
- if (checkedRows.size <= min) {
1185
- checkChanged();
1186
- return;
1187
- }
1188
- const keep = [...checkedRows.entries()].slice(0, min);
1189
- checkedRows.clear();
1190
- for (const [key, row] of keep) checkedRows.set(key, row);
1191
- } else checkedRows.clear();
1192
- checkChanged();
1193
- },
1194
- limits: checkLimits,
1195
- get atMax() {
1196
- const max = checkLimits().max;
1197
- return max !== void 0 && checkedRows.size >= max;
1198
- },
1199
- get atMin() {
1200
- const min = checkLimits().min;
1201
- return min !== void 0 && checkedRows.size <= min;
1202
- },
1203
- get version() {
1204
- return checkVersion;
1205
- },
1206
- get pending() {
1207
- return checkPending;
1208
- },
1209
- get allChecked() {
1210
- const rows = ctrl.items;
1211
- return rows.length > 0 && rows.every((row, i) => checkedRows.has(rowKeyOf(row, i)));
1212
- },
1213
- get someChecked() {
1214
- const rows = ctrl.items;
1215
- if (!rows.length || !checkedRows.size) return false;
1216
- return !rows.every((row, i) => checkedRows.has(rowKeyOf(row, i)));
1217
- },
1218
- configure(config) {
1219
- for (const [k, v] of Object.entries(config)) if (v === null) delete checkConfig[k];
1220
- else if (v !== void 0) checkConfig[k] = v;
1221
- }
1222
- };
1223
- function check() {
1224
- return checkHandle;
1225
- }
1226
- /**
1227
- * Sort elements by document order so a handle reads left→right / top→bottom as
1228
- * displayed. Falls back to insertion order where `compareDocumentPosition` is
1229
- * unavailable (SSR / no DOM).
1230
- */
1231
- function domOrdered(els) {
1232
- return els.sort((a, b) => {
1233
- if (typeof a.compareDocumentPosition !== "function") return 0;
1234
- const pos = a.compareDocumentPosition(b);
1235
- if (pos & 4) return -1;
1236
- if (pos & 2) return 1;
1237
- return 0;
1238
- });
1239
- }
1240
- function orderedColumnEls() {
1241
- return Array.from(columnEls).sort((a, b) => {
1242
- if (typeof a.compareDocumentPosition !== "function") return 0;
1243
- const pos = a.compareDocumentPosition(b);
1244
- if (pos & 4) return -1;
1245
- if (pos & 2) return 1;
1246
- return 0;
1247
- });
1248
- }
1249
- function registeredColumns() {
1250
- return orderedColumnEls().map((el) => ({
1251
- field: el.field,
1252
- caption: el.caption,
1253
- sort: el.sort,
1254
- editable: el.editable,
1255
- headerFilter: el.headerFilter,
1256
- dateFilter: el.dateFilter
1257
- }));
1258
- }
1259
- function editableColumns() {
1260
- return registeredColumns().filter((c) => !!c.editable && !!c.field);
1261
- }
1262
- const columnDefs = [...opts.columns ?? []];
1263
- const columnsHandle = {
1264
- get: (field) => columnDefs.find((c) => c.field === field) ?? null,
1265
- getAll: () => columnDefs
1266
- };
1267
- const elementProps = { ...opts.props ?? {} };
1268
- {
1269
- const declared = opts.props?.th ?? [];
1270
- const byField = /* @__PURE__ */ new Map();
1271
- for (const c of columnDefs) if (c?.field) byField.set(c.field, { ...c });
1272
- for (const c of declared) {
1273
- if (!c?.field) continue;
1274
- byField.set(c.field, {
1275
- ...byField.get(c.field) ?? {},
1276
- ...c
1277
- });
1278
- }
1279
- const ordered = [];
1280
- for (const c of declared) if (c?.field && byField.has(c.field)) ordered.push(byField.get(c.field));
1281
- for (const [field, c] of byField) if (!ordered.some((o) => o.field === field)) ordered.push(c);
1282
- elementProps.th = ordered;
1283
- }
1284
- function props() {
1285
- return elementProps;
1286
- }
1287
- /** Snapshot written into `opts.state` on every notify (see `notify`). */
1288
- function propsSnapshot() {
1289
- return {
1290
- ...elementProps,
1291
- th: (elementProps.th ?? []).map((c) => ({ ...c }))
1292
- };
1293
- }
1294
- function columns() {
1295
- return columnsHandle;
1296
- }
1297
- /**
1298
- * Row → index and row key → index over `ctrl.items`, built lazily per items ARRAY.
1299
- *
1300
- * `rowKey()` / `indexOfRowKey()` were linear scans, and they are per-row calls: every
1301
- * `<mono-table-checkbox>` asks `check.isChecked(row)` on every notify, so a tick on a grid
1302
- * holding N rows (infinity scroll and page-size "all" hold the whole set) cost O(N²).
1303
- *
1304
- * A HIT is always verified against the live array, and a MISS falls back to the scan
1305
- * (rebuilding when the scan finds the row) — so an in-place mutation of `ctrl.items`, or a
1306
- * key that moved, costs one rebuild and is never answered wrong.
1307
- */
1308
- let indexFor = null;
1309
- let indexByRow = /* @__PURE__ */ new Map();
1310
- let keyIndexFor = null;
1311
- let indexByKey = /* @__PURE__ */ new Map();
1312
- function rebuildRowIndex(items) {
1313
- indexFor = items;
1314
- indexByRow = /* @__PURE__ */ new Map();
1315
- items.forEach((row, i) => {
1316
- if (!indexByRow.has(row)) indexByRow.set(row, i);
1317
- });
1318
- }
1319
- function indexOfRow(raw) {
1320
- const items = ctrl.items;
1321
- if (indexFor !== items) rebuildRowIndex(items);
1322
- const hit = indexByRow.get(raw);
1323
- if (hit !== void 0 && items[hit] === raw) return hit;
1324
- const i = items.indexOf(raw);
1325
- if (i >= 0) rebuildRowIndex(items);
1326
- return i;
1327
- }
1328
- function rebuildKeyIndex(items) {
1329
- keyIndexFor = items;
1330
- indexByKey = /* @__PURE__ */ new Map();
1331
- items.forEach((row, i) => {
1332
- const k = rowKeyOf(row, i);
1333
- if (!indexByKey.has(k)) indexByKey.set(k, i);
1334
- });
1335
- }
1336
- function indexOfRowKey(rowKey) {
1337
- const items = ctrl.items;
1338
- if (keyIndexFor !== items) rebuildKeyIndex(items);
1339
- const hit = indexByKey.get(rowKey);
1340
- if (hit !== void 0 && hit < items.length && rowKeyOf(items[hit], hit) === rowKey) return hit;
1341
- const i = items.findIndex((row, j) => rowKeyOf(row, j) === rowKey);
1342
- if (i >= 0) rebuildKeyIndex(items);
1343
- return i;
1344
- }
1345
- function rowByKey(rowKey) {
1346
- const i = indexOfRowKey(rowKey);
1347
- return i >= 0 ? ctrl.items[i] : void 0;
1348
- }
1349
- function _cssEscape(value) {
1350
- return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
1351
- }
1352
- /**
1353
- * Is this pointer event a press on `el`'s scrollbar rather than on its content?
1354
- *
1355
- * Overlay scrollbars are painted over the content box, so the event target is
1356
- * the element underneath and only the coordinates can tell the two apart. A
1357
- * band is live only on an axis that actually overflows, which keeps a genuine
1358
- * click on the last row from being mistaken for a scrollbar grab.
1359
- */
1360
- function _inScrollbarBand(el, e) {
1361
- const { clientX: x, clientY: y } = e;
1362
- if (typeof x !== "number" || typeof y !== "number") return false;
1363
- const BAND = 17;
1364
- const r = el.getBoundingClientRect();
1365
- const inX = x >= r.left && x <= r.right;
1366
- const inY = y >= r.top && y <= r.bottom;
1367
- if (el.scrollWidth > el.clientWidth + 1 && inX && y >= r.bottom - BAND && y <= r.bottom) return true;
1368
- if (el.scrollHeight > el.clientHeight + 1 && inY && x >= r.right - BAND && x <= r.right) return true;
1369
- return false;
1370
- }
1371
- let _outsidePointer = null;
1372
- function installOutsideExit() {
1373
- if (typeof document === "undefined" || _outsidePointer) return;
1374
- _outsidePointer = (e) => {
1375
- const key = ctrl.editingKey;
1376
- if (key == null) return;
1377
- const target = e.target;
1378
- if (!target?.closest) return;
1379
- if (target.closest(`[data-row-key="${_cssEscape(key)}"]`) || target.closest("[data-mono-popup-portal], .flatpickr-calendar")) return;
1380
- let node = target;
1381
- while (node) {
1382
- const s = getComputedStyle(node);
1383
- if (/(auto|scroll)/.test(s.overflowX + s.overflowY) && _inScrollbarBand(node, e)) return;
1384
- node = node.parentElement;
1385
- }
1386
- setTimeout(() => {
1387
- if (ctrl.editingKey === key) cancelEdit();
1388
- }, 0);
1389
- };
1390
- document.addEventListener("pointerdown", _outsidePointer, true);
1391
- }
1392
- function removeOutsideExit() {
1393
- if (!_outsidePointer || typeof document === "undefined") return;
1394
- document.removeEventListener("pointerdown", _outsidePointer, true);
1395
- _outsidePointer = null;
1396
- }
1397
- let _triggerEl = null;
1398
- let _triggerHandler = null;
1399
- let _triggerType = null;
1400
- const _onRowTrigger = (e) => {
1401
- const target = e.target;
1402
- if (!target?.closest) return;
1403
- if (target.closest("[data-edit-cell]")) return;
1404
- if (target.closest("button, a, input, select, textarea, label, [contenteditable=\"true\"]")) return;
1405
- const rowEl = target.closest("[data-row-key]");
1406
- if (!rowEl) return;
1407
- const key = rowEl.getAttribute("data-row-key");
1408
- if (key == null || ctrl.editingKey === key) return;
1409
- if (!editableColumns().length) return;
1410
- if (isEditableRow(rowByKey(key))) return;
1411
- beginEditRow(key, e);
1412
- };
1413
- /**
1414
- * Point the row trigger at the `<table>` that owns this grid's rows. Called by
1415
- * every `mono-table-th` on update; idempotent, so only a changed table element
1416
- * or a changed `editableTrigger` actually re-binds.
1417
- */
1418
- function bindRowTrigger(tableEl) {
1419
- if (typeof document === "undefined") return;
1420
- const type = ctrl.editableTrigger === "double-click" ? "dblclick" : "click";
1421
- if (tableEl === _triggerEl && type === _triggerType) return;
1422
- unbindRowTrigger();
1423
- if (!tableEl) return;
1424
- _triggerHandler = _onRowTrigger;
1425
- _triggerType = type;
1426
- _triggerEl = tableEl;
1427
- tableEl.addEventListener(type, _triggerHandler);
1428
- }
1429
- function unbindRowTrigger() {
1430
- if (_triggerEl && _triggerHandler && _triggerType) _triggerEl.removeEventListener(_triggerType, _triggerHandler);
1431
- _triggerEl = null;
1432
- _triggerHandler = null;
1433
- _triggerType = null;
1434
- }
1435
- function beginEditRow(rowKey, event) {
1436
- if (ctrl.editingKey === rowKey) return;
1437
- ctrl.editingKey = rowKey;
1438
- installOutsideExit();
1439
- installEditorKeys();
1440
- notify();
1441
- if (event) focusRowCell(event.target, rowKey, "first", { preferClicked: true });
1442
- }
1443
- function cancelEdit() {
1444
- removeOutsideExit();
1445
- removeEditorKeys();
1446
- if (ctrl.editingKey === null) return;
1447
- ctrl.editingKey = null;
1448
- notify();
1449
- }
1450
- function isEditingRow(rowKey) {
1451
- return ctrl.editingKey === rowKey;
1452
- }
1453
- /** A `form({ showForm: true })` row inserted for inline data entry. */
1454
- function isEditableRow(row) {
1455
- return !!row && row._Type === EDITABLE_MARK;
1456
- }
1457
- function isEditingCell(rowKeyStr, field) {
1458
- let editableCol = false;
1459
- for (const el of columnEls) if (el.field === field && el.editable) {
1460
- editableCol = true;
1461
- break;
1462
- }
1463
- if (!editableCol) return false;
1464
- return ctrl.editingKey === rowKeyStr || isEditableRow(rowByKey(rowKeyStr));
1465
- }
1466
- function commitCell(rowKey, field, value) {
1467
- ctrl.onCellChange?.({
1468
- rowKey,
1469
- field,
1470
- value,
1471
- row: rowByKey(rowKey)
1472
- });
1473
- }
1474
- function serverKeyOf(rowKey) {
1475
- const row = rowByKey(rowKey);
1476
- return row ? row[keyExpr] ?? rowKey : rowKey;
1477
- }
1478
- function stageCell(rowKey, field, value) {
1479
- const row = rowByKey(rowKey);
1480
- if (isEditableRow(row)) {
1481
- if (isPath(field)) setFieldValue(row, field, value);
1482
- else row[field] = value;
1483
- notify();
1484
- return;
1485
- }
1486
- const patch = pending.get(rowKey) ?? {};
1487
- patch[field] = value;
1488
- pending.set(rowKey, patch);
1489
- clearImported(rowKey, field);
1490
- notify();
1491
- }
1492
- const importedCells = /* @__PURE__ */ new Map();
1493
- function markImported(rowKey, fields) {
1494
- const set = importedCells.get(rowKey) ?? /* @__PURE__ */ new Set();
1495
- for (const f of fields) set.add(f);
1496
- importedCells.set(rowKey, set);
1497
- }
1498
- function clearImported(rowKey, field) {
1499
- const set = importedCells.get(rowKey);
1500
- if (!set) return;
1501
- set.delete(field);
1502
- if (!set.size) importedCells.delete(rowKey);
1503
- }
1504
- function isCellImported(rowKey, field) {
1505
- return !!importedCells.get(rowKey)?.has(field);
1506
- }
1507
- function isRowImported(rowKey) {
1508
- return !!importedCells.get(rowKey)?.size;
1509
- }
1510
- /** Staged value for a cell, or `fallback` when the cell isn't staged. */
1511
- function cellValue(rowKey, field, fallback) {
1512
- const row = rowByKey(rowKey);
1513
- if (isEditableRow(row)) {
1514
- const v = isPath(field) ? getFieldValue(row, field) : row[field];
1515
- return v === void 0 ? fallback : v;
1516
- }
1517
- const patch = pending.get(rowKey);
1518
- return patch && field in patch ? patch[field] : fallback;
1519
- }
1520
- function isCellDirty(rowKey, field) {
1521
- const patch = pending.get(rowKey);
1522
- return !!patch && field in patch;
1523
- }
1524
- function isRowDirty(rowKey) {
1525
- return pending.has(rowKey);
1526
- }
1527
- function pendingCount() {
1528
- return pending.size;
1529
- }
1530
- function hasChanges() {
1531
- return pending.size > 0;
1532
- }
1533
- /** The staged change set — for a consumer that sends its own (bulk) request. */
1534
- function changes() {
1535
- return Array.from(pending.entries()).map(([rowKey, patch]) => ({
1536
- rowKey,
1537
- key: serverKeyOf(rowKey),
1538
- patch: { ...patch },
1539
- row: rowByKey(rowKey)
1540
- }));
1541
- }
1542
- /** Drop the staged buffer and the import marks that shadow it. */
1543
- function clearPending() {
1544
- pending.clear();
1545
- importedCells.clear();
1546
- }
1547
- function discardChanges() {
1548
- if (pending.size === 0) return;
1549
- clearPending();
1550
- notify();
1551
- }
1552
- /**
1553
- * Flush every staged row to the bound source, then clear + reflect. Remote
1554
- * DataSource → `store.update(key, patch)` per row + `reload()`; array source →
1555
- * per-row `update`; else a `setData` merge of the current page. On error the
1556
- * buffer is KEPT so the save can be retried.
1557
- */
1558
- async function saveChanges() {
1559
- if (pending.size === 0) return;
1560
- const entries = Array.from(pending.entries());
1561
- const s = bound;
1562
- const store = s?.store?.();
1563
- if (store && typeof store.update === "function") {
1564
- for (const [rowKey, patch] of entries) {
1565
- if (Object.keys(patch).some(isPath)) console.warn("[monoDataGrid] saveChanges: a remote store.update patch contains a path key; nested PATCH bodies are not auto-built — send the nested shape your backend expects.");
1566
- await store.update(serverKeyOf(rowKey), patch);
1567
- }
1568
- clearPending();
1569
- await reload();
1570
- return;
1571
- }
1572
- if (s && typeof s.update === "function") {
1573
- for (const [rowKey, patch] of entries) await s.update(serverKeyOf(rowKey), patch);
1574
- clearPending();
1575
- notify();
1576
- return;
1577
- }
1578
- if (s && typeof s.setData === "function") {
1579
- const next = (s.data?.() ?? ctrl.items ?? []).map((row, i) => {
1580
- const patch = pending.get(rowKeyOf(row, i));
1581
- return patch ? mergePatch(row, patch) : row;
1582
- });
1583
- clearPending();
1584
- await setData(next);
1585
- return;
1586
- }
1587
- clearPending();
1588
- notify();
1589
- }
1590
- /** First existing row to infer an empty-row shape from (array backing → page → items). */
1591
- function formSample() {
1592
- const fromData = (bound?.data?.())?.[0];
1593
- const fromItems = (bound?.items?.())?.[0];
1594
- return fromData ?? fromItems ?? ctrl.items[0];
1595
- }
1596
- /** Build a typed-empty row from a sample: number→0, string→'', bool→false, array→[], Date→null, object→{}. */
1597
- function emptyLike(sample) {
1598
- const out = {};
1599
- for (const k in sample) {
1600
- const v = sample[k];
1601
- if (v == null) out[k] = null;
1602
- else if (typeof v === "number") out[k] = 0;
1603
- else if (typeof v === "string") out[k] = "";
1604
- else if (typeof v === "boolean") out[k] = false;
1605
- else if (Array.isArray(v)) out[k] = [];
1606
- else if (v instanceof Date) out[k] = null;
1607
- else if (typeof v === "object") out[k] = {};
1608
- else out[k] = null;
1609
- }
1610
- return out;
1611
- }
1612
- /** Strip OData response metadata off a staged row before it enters the data. */
1613
- function cleanRow(row) {
1614
- if (row && typeof row === "object") {
1615
- const r = row;
1616
- if ("@odata.context" in r) delete r["@odata.context"];
1617
- if ("@odata.url" in r) delete r["@odata.url"];
1618
- }
1619
- return row;
1620
- }
1621
- /**
1622
- * The row to actually commit: OData metadata stripped, and — for a `showForm`
1623
- * row — the `_Type` marker removed (the non-enumerable identity symbol is dropped
1624
- * by the spread). Returns a clean object; never carries the editable marker.
1625
- */
1626
- function commitRow(row) {
1627
- cleanRow(row);
1628
- if (isEditableRow(row)) {
1629
- const { _Type, ...rest } = row;
1630
- return rest;
1631
- }
1632
- return row;
1633
- }
1634
- const looseEq = (a, b) => a == b;
1635
- /** Replay one op into a full array (array source path). */
1636
- function replayArray(arr, op, key) {
1637
- const idxOf = (k) => arr.findIndex((e) => looseEq(e?.[key], k));
1638
- if (op.op === "add") for (const src of op.rows) {
1639
- const row = commitRow(src);
1640
- const i = idxOf(row?.[key]);
1641
- if (i >= 0) arr.splice(i, 1, row);
1642
- else arr.unshift(row);
1643
- }
1644
- else if (op.op === "edit") {
1645
- const i = idxOf(op.key);
1646
- if (i >= 0) arr.splice(i, 1, {
1647
- ...arr[i],
1648
- ...op.row
1649
- });
1650
- } else {
1651
- const i = idxOf(op.key);
1652
- if (i >= 0) arr.splice(i, 1);
1653
- }
1654
- }
1655
- /** Replay one op into the live page collection (remote source path). Returns the totalCount delta. */
1656
- function replayDs(coll, op, key, pushes) {
1657
- const idxOf = (k) => coll.findIndex((e) => looseEq(e?.[key], k));
1658
- if (op.op === "add") {
1659
- let delta = 0;
1660
- for (const src of op.rows) {
1661
- const row = commitRow(src);
1662
- const k = row?.[key];
1663
- const i = idxOf(k);
1664
- if (i >= 0) Object.assign(coll[i], row);
1665
- else {
1666
- coll.unshift(row);
1667
- pushes.push({
1668
- type: "insert",
1669
- data: row,
1670
- key: k
1671
- });
1672
- delta++;
1673
- }
1674
- }
1675
- return delta;
1676
- }
1677
- if (op.op === "edit") {
1678
- const i = idxOf(op.key);
1679
- if (i >= 0) {
1680
- Object.assign(coll[i], op.row);
1681
- pushes.push({
1682
- type: "update",
1683
- key: op.key,
1684
- data: op.row
1685
- });
1686
- }
1687
- return 0;
1688
- }
1689
- const i = idxOf(op.key);
1690
- if (i >= 0) {
1691
- coll.splice(i, 1);
1692
- pushes.push({
1693
- type: "remove",
1694
- key: op.key
1695
- });
1696
- return -1;
1697
- }
1698
- return 0;
1699
- }
1700
- /** Commit all staged form ops into the rendered data. Local only — no server call. Final: no undo. */
1701
- async function applyForm() {
1702
- if (formOps.length === 0) return;
1703
- const key = formKey ?? keyExpr;
1704
- const ops = formOps.splice(0, formOps.length);
1705
- const s = bound;
1706
- if (s && typeof s.setData === "function") {
1707
- const next = [...s.data?.() ?? ctrl.items.filter((r) => !isEditableRow(r)) ?? []];
1708
- for (const op of ops) replayArray(next, op, key);
1709
- await setData(next);
1710
- return;
1711
- }
1712
- const store = s?.store?.();
1713
- if (s && store) {
1714
- if (isGrouped() || serverMode()) console.warn("[monoDataGrid] form().apply() on a grouped/server grid updates only the flat page; reload to refresh groups.");
1715
- const coll = s.items?.() ?? [];
1716
- const pushes = [];
1717
- let delta = 0;
1718
- for (const op of ops) delta += replayDs(coll, op, key, pushes);
1719
- if (typeof store.push === "function" && pushes.length) store.push(pushes);
1720
- ctrl.items = [...coll];
1721
- ctrl.totalCount = Math.max(0, ctrl.totalCount + delta);
1722
- render();
1723
- return;
1724
- }
1725
- notify();
1726
- }
1727
- /** Tag a `showForm` row: visible marker + stable non-enumerable identity. */
1728
- function tagEditable(row) {
1729
- row._Type = EDITABLE_MARK;
1730
- Object.defineProperty(row, MONO_ROW_KEY, {
1731
- value: `__form_${++formTempSeq}`,
1732
- enumerable: false,
1733
- configurable: true,
1734
- writable: true
1735
- });
1736
- return row;
1737
- }
1738
- /** {@link MonoTableController.form} — memoized, chainable handle over one shared buffer. */
1739
- function form(config) {
1740
- if (config?.key) formKey = config.key;
1741
- formShowForm = !!config?.showForm;
1742
- if (formHandle) return formHandle;
1743
- const handle = {
1744
- add(row) {
1745
- let rows;
1746
- if (row === void 0) {
1747
- const sample = formSample();
1748
- rows = [sample ? emptyLike(sample) : {}];
1749
- } else rows = Array.isArray(row) ? row : [row];
1750
- if (formShowForm) {
1751
- rows.forEach(tagEditable);
1752
- ctrl.items.unshift(...rows);
1753
- ctrl.totalCount += rows.length;
1754
- render();
1755
- }
1756
- formOps.push({
1757
- op: "add",
1758
- rows
1759
- });
1760
- return handle;
1761
- },
1762
- edit(k, row) {
1763
- formOps.push({
1764
- op: "edit",
1765
- key: k,
1766
- row
1767
- });
1768
- return handle;
1769
- },
1770
- delete(k) {
1771
- formOps.push({
1772
- op: "delete",
1773
- key: k
1774
- });
1775
- return handle;
1776
- },
1777
- apply: applyForm,
1778
- revert(key) {
1779
- const k = String(key);
1780
- const items = ctrl.items;
1781
- const idx = items.findIndex((r, i) => rowKeyOf(r, i) === k);
1782
- if (idx >= 0 && isEditableRow(items[idx])) {
1783
- items.splice(idx, 1);
1784
- ctrl.totalCount = Math.max(0, ctrl.totalCount - 1);
1785
- }
1786
- for (let i = formOps.length - 1; i >= 0; i--) {
1787
- const op = formOps[i];
1788
- if (op.op !== "add") continue;
1789
- op.rows = op.rows.filter((r) => rowKeyOf(r, -1) !== k);
1790
- if (op.rows.length === 0) formOps.splice(i, 1);
1791
- }
1792
- render();
1793
- return handle;
1794
- },
1795
- revertAll() {
1796
- const shown = ctrl.items.filter((r) => isEditableRow(r)).length;
1797
- if (shown) {
1798
- ctrl.items = ctrl.items.filter((r) => !isEditableRow(r));
1799
- ctrl.totalCount = Math.max(0, ctrl.totalCount - shown);
1800
- }
1801
- for (let i = formOps.length - 1; i >= 0; i--) {
1802
- const op = formOps[i];
1803
- if (op.op !== "add") continue;
1804
- op.rows = op.rows.filter((r) => !isEditableRow(r));
1805
- if (op.rows.length === 0) formOps.splice(i, 1);
1806
- }
1807
- render();
1808
- return handle;
1809
- },
1810
- changes: () => formOps.map((o) => ({ ...o })),
1811
- discard(key) {
1812
- if (key === void 0) {
1813
- formOps.length = 0;
1814
- const shown = ctrl.items.filter((r) => isEditableRow(r)).length;
1815
- if (shown) {
1816
- ctrl.items = ctrl.items.filter((r) => !isEditableRow(r));
1817
- ctrl.totalCount = Math.max(0, ctrl.totalCount - shown);
1818
- render();
1819
- }
1820
- return handle;
1821
- }
1822
- const k = String(key);
1823
- const items = ctrl.items;
1824
- const idx = items.findIndex((r, i) => rowKeyOf(r, i) === k);
1825
- if (idx >= 0 && isEditableRow(items[idx])) {
1826
- items.splice(idx, 1);
1827
- ctrl.totalCount = Math.max(0, ctrl.totalCount - 1);
1828
- }
1829
- for (let i = formOps.length - 1; i >= 0; i--) {
1830
- const op = formOps[i];
1831
- if (op.op === "add") {
1832
- op.rows = op.rows.filter((r) => rowKeyOf(r, -1) !== k);
1833
- if (op.rows.length === 0) formOps.splice(i, 1);
1834
- } else if (String(op.key) === k) formOps.splice(i, 1);
1835
- }
1836
- render();
1837
- return handle;
1838
- },
1839
- discardAll() {
1840
- handle.discard();
1841
- clearPending();
1842
- cancelEdit();
1843
- render();
1844
- return handle;
1845
- }
1846
- };
1847
- formHandle = handle;
1848
- return handle;
1849
- }
1850
- /**
1851
- * Focus an editor in `rowKey`.
1852
- *
1853
- * `preventScroll` is the important part: a bare `.focus()` scrolls the element
1854
- * into view, and this runs inside a `requestAnimationFrame` — one frame AFTER
1855
- * the row re-rendered — so the browser scrolls `.mono-table-scroll`
1856
- * (`overflow-x: auto`) or the page a moment after the click lands. That
1857
- * delayed nudge is what reads as the editor "jumping".
1858
- *
1859
- * `reveal` opts back into a MINIMAL scroll for the cross-row Tab, where the
1860
- * next row genuinely may be off screen. `block: 'nearest'` moves the least
1861
- * possible amount and does nothing when the row is already visible — unlike
1862
- * the default focus scroll, which can centre it.
1863
- *
1864
- * `preferClicked` focuses the editor in the cell the user actually clicked
1865
- * (falling back to `edge` when that cell has none — an ID or actions column,
1866
- * or a click on the row's padding). Without it, opening a row by clicking its
1867
- * Job cell would drop the caret in Name, and the user has to Tab back across
1868
- * the row to reach the field they aimed at.
1869
- */
1870
- function focusRowCell(fromEl, rowKey, edge, opts = {}) {
1871
- const from = fromEl;
1872
- const table = from?.closest?.("table");
1873
- if (!table || typeof requestAnimationFrame === "undefined") return;
1874
- const clickedCell = opts.preferClicked ? from?.closest?.("td") : null;
1875
- requestAnimationFrame(() => {
1876
- const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(rowKey) : rowKey;
1877
- const cells = table.querySelectorAll(`[data-row-key="${escaped}"] [data-edit-cell]`);
1878
- const clicked = clickedCell?.closest(`[data-row-key="${escaped}"]`) && table.contains(clickedCell) ? clickedCell.querySelector("[data-edit-cell]") : null;
1879
- const target = (opts.field ? table.querySelector(`[data-row-key="${escaped}"] [data-edit-cell="${_cssEscape(opts.field)}"]`) : null) ?? clicked ?? (edge === "first" ? cells[0] : cells[cells.length - 1]);
1880
- const definesOwnFocus = typeof target?.focus === "function" && typeof HTMLElement !== "undefined" && target.focus !== HTMLElement.prototype.focus;
1881
- const focusable = target?.matches?.("input, textarea, select, button") ? target : definesOwnFocus ? target : target?.querySelector?.("input, textarea, select, button") ?? target;
1882
- if (opts.reveal) revealCell(target);
1883
- focusable?.focus?.({ preventScroll: true });
1884
- });
1885
- }
1886
- /** The nearest scrollable ancestor of `el` — the table's own scroll region first. */
1887
- function scrollParentOf(el) {
1888
- const region = el.closest(".mono-table-scroll, [mono-table-scroll]");
1889
- if (region) return region;
1890
- let node = el.parentElement;
1891
- while (node) {
1892
- const s = getComputedStyle(node);
1893
- if (/(auto|scroll)/.test(s.overflowX + s.overflowY)) return node;
1894
- node = node.parentElement;
1895
- }
1896
- return null;
1897
- }
1898
- /**
1899
- * Scroll `cell` into view by the SMALLEST delta that works — and no further.
1900
- *
1901
- * `scrollIntoView({ block: 'nearest', inline: 'nearest' })` looks like the
1902
- * answer but isn't: it knows nothing about pinned columns, so on a wide grid
1903
- * it happily parks the cell *underneath* a `.mono-table-sticky-left` column or
1904
- * the sticky header, leaving the user typing into something they can't see.
1905
- * So compute the genuinely visible band from the pinned elements' own rects
1906
- * and scroll only as far as that band requires. Each axis is handled
1907
- * independently, so stepping sideways never nudges the page vertically.
1908
- *
1909
- * This has to be explicit now: the keyboard path focuses with
1910
- * `preventScroll: true`, which is what stopped the browser from revealing the
1911
- * editor for us.
1912
- */
1913
- function revealCell(cell) {
1914
- if (!cell || typeof getComputedStyle === "undefined") return;
1915
- const scroller = scrollParentOf(cell);
1916
- if (!scroller) return;
1917
- const cellBox = cell.getBoundingClientRect();
1918
- const view = scroller.getBoundingClientRect();
1919
- const row = cell.closest("tr");
1920
- const table = cell.closest("table");
1921
- const MARGIN = 8;
1922
- const edgeOf = (sel, side) => {
1923
- const rects = Array.from((row ?? table)?.querySelectorAll(sel) ?? []).filter((n) => !n.contains(cell)).map((n) => n.getBoundingClientRect());
1924
- if (!rects.length) return null;
1925
- if (side === "left") return Math.max(...rects.map((r) => r.right));
1926
- if (side === "right") return Math.min(...rects.map((r) => r.left));
1927
- if (side === "top") return Math.max(...rects.map((r) => r.bottom));
1928
- return Math.min(...rects.map((r) => r.top));
1929
- };
1930
- const left = Math.max(view.left, edgeOf(".mono-table-sticky-left, [mono-sticky-left]", "left") ?? -Infinity);
1931
- const right = Math.min(view.right, edgeOf(".mono-table-sticky-right, [mono-sticky-right]", "right") ?? Infinity);
1932
- if (cellBox.left < left + MARGIN) scroller.scrollLeft -= left + MARGIN - cellBox.left;
1933
- else if (cellBox.right > right - MARGIN) scroller.scrollLeft += cellBox.right - (right - MARGIN);
1934
- if (scroller.scrollHeight <= scroller.clientHeight + 1) return;
1935
- const heads = Array.from(table?.querySelectorAll(".mono-table-sticky-head thead th, [mono-sticky-head] thead th, thead th") ?? []).map((n) => n.getBoundingClientRect());
1936
- const foots = Array.from(table?.querySelectorAll(".mono-table-sticky-foot tfoot th, .mono-table-sticky-foot tfoot td, [mono-sticky-foot] tfoot th, [mono-sticky-foot] tfoot td") ?? []).map((n) => n.getBoundingClientRect());
1937
- const top = Math.max(view.top, ...heads.map((r) => r.bottom).filter((v) => v <= view.bottom));
1938
- const bottom = Math.min(view.bottom, ...foots.map((r) => r.top).filter((v) => v >= view.top));
1939
- if (cellBox.top < top + MARGIN) scroller.scrollTop -= top + MARGIN - cellBox.top;
1940
- else if (cellBox.bottom > bottom - MARGIN) scroller.scrollTop += cellBox.bottom - (bottom - MARGIN);
1941
- }
1942
- /** Which `[data-edit-cell]` holds the caret right now. */
1943
- function activeEditorCell(fromEl) {
1944
- const byEvent = fromEl?.closest?.("[data-edit-cell]");
1945
- if (byEvent) return byEvent;
1946
- return (typeof document !== "undefined" ? document.activeElement : null)?.closest?.("[data-edit-cell]") ?? null;
1947
- }
1948
- /**
1949
- * Move the editor one cell and focus it. Clamps on both axes and returns
1950
- * `false` when the move is blocked, so the caller can swallow the key without
1951
- * anything happening — arrow navigation must NOT wrap around an edge. Only the
1952
- * plain Tab tap passes `wrap`, which lets left/right cross into the
1953
- * previous/next row in reading order (the long-standing Tab behaviour).
1954
- */
1955
- function moveEditor(dir, fromEl, opts = {}) {
1956
- const rowKey = ctrl.editingKey;
1957
- if (rowKey == null) return false;
1958
- const cell = activeEditorCell(fromEl);
1959
- const field = cell?.getAttribute("data-edit-cell");
1960
- const cols = editableColumns();
1961
- const colIdx = field ? cols.findIndex((c) => c.field === field) : -1;
1962
- const rowIdx = indexOfRowKey(rowKey);
1963
- if (colIdx === -1 || rowIdx === -1 || !cols.length) return false;
1964
- const rows = ctrl.items;
1965
- const anchor = cell ?? fromEl;
1966
- if (dir === "up" || dir === "down") {
1967
- const nextIdx = rowIdx + (dir === "down" ? 1 : -1);
1968
- const next = rows[nextIdx];
1969
- if (!next) return false;
1970
- const nextKey = rowKeyOf(next, nextIdx);
1971
- beginEditRow(nextKey);
1972
- focusRowCell(anchor, nextKey, "first", {
1973
- reveal: true,
1974
- field: cols[colIdx].field
1975
- });
1976
- return true;
1977
- }
1978
- const nextCol = colIdx + (dir === "right" ? 1 : -1);
1979
- if (nextCol >= 0 && nextCol < cols.length) {
1980
- focusRowCell(anchor, rowKey, "first", {
1981
- reveal: true,
1982
- field: cols[nextCol].field
1983
- });
1984
- return true;
1985
- }
1986
- if (!opts.wrap) return false;
1987
- const nextIdx = rowIdx + (dir === "right" ? 1 : -1);
1988
- const next = rows[nextIdx];
1989
- if (!next) return false;
1990
- const nextKey = rowKeyOf(next, nextIdx);
1991
- beginEditRow(nextKey);
1992
- focusRowCell(anchor, nextKey, dir === "right" ? "first" : "last", { reveal: true });
1993
- return true;
1994
- }
1995
- const ARROW_DIRS = {
1996
- ArrowLeft: "left",
1997
- ArrowRight: "right",
1998
- ArrowUp: "up",
1999
- ArrowDown: "down"
2000
- };
2001
- let _editorKeys = null;
2002
- let _tabHeld = false;
2003
- let _tabConsumed = false;
2004
- let _tabShift = false;
2005
- /**
2006
- * The control inside `cell` that owns a popup. `data-edit-cell` usually sits on
2007
- * the control itself, but allow a wrapper. Custom elements only, and `open`
2008
- * must be a METHOD — `<details>` and `<dialog>` both carry a boolean `open`
2009
- * property that isn't callable.
2010
- */
2011
- function popupEditorOf(cell) {
2012
- if (!cell) return null;
2013
- return [cell, ...Array.from(cell.querySelectorAll("*"))].find((el) => el.tagName.includes("-") && typeof el.open === "function") ?? null;
2014
- }
2015
- /**
2016
- * `Enter` / `Escape`, shared by the document-level machine and the per-editor
2017
- * `editorKeydown` so the two modes behave identically. Returns whether the key
2018
- * belonged to us.
2019
- */
2020
- function handleEditorActionKey(e, cell) {
2021
- if (e.key === "Escape") {
2022
- const host = popupEditorOf(cell);
2023
- if (host?.isOpen && typeof host.close === "function") {
2024
- e.preventDefault();
2025
- e.stopPropagation();
2026
- host.close();
2027
- return true;
2028
- }
2029
- cancelEdit();
2030
- return true;
2031
- }
2032
- if (e.key !== "Enter") return false;
2033
- const active = e.target;
2034
- if (active?.tagName === "TEXTAREA" || active?.isContentEditable) return false;
2035
- const host = popupEditorOf(cell);
2036
- if (host?.isOpen) return false;
2037
- e.preventDefault();
2038
- if (host) {
2039
- e.stopPropagation();
2040
- host.open();
2041
- }
2042
- return true;
2043
- }
2044
- /**
2045
- * The very last cell in the grid (or the very first, going back). Tab is left
2046
- * alone there so a keyboard user can still leave the table — trapping focus in
2047
- * the grid would be worse than losing Tab+Arrow from that one cell.
2048
- */
2049
- function atGridEdge(cell, back) {
2050
- const cols = editableColumns();
2051
- const colIdx = cols.findIndex((c) => c.field === cell.getAttribute("data-edit-cell"));
2052
- const rowIdx = ctrl.editingKey != null ? indexOfRowKey(ctrl.editingKey) : -1;
2053
- if (colIdx === -1 || rowIdx === -1) return true;
2054
- return back ? colIdx === 0 && rowIdx === 0 : colIdx === cols.length - 1 && rowIdx === ctrl.items.length - 1;
2055
- }
2056
- function installEditorKeys() {
2057
- if (typeof document === "undefined" || _editorKeys) return;
2058
- if (ctrl.editorNavKeys !== "tab-arrows") return;
2059
- const editingCell = (e) => {
2060
- const key = ctrl.editingKey;
2061
- if (key == null) return null;
2062
- const cell = e.target?.closest?.("[data-edit-cell]");
2063
- return cell?.closest(`[data-row-key="${_cssEscape(key)}"]`) ? cell : null;
2064
- };
2065
- const down = (evt) => {
2066
- const e = evt;
2067
- const cell = editingCell(e);
2068
- if (!cell) return;
2069
- if (handleEditorActionKey(e, cell)) return;
2070
- if (e.key === "Tab") {
2071
- if (atGridEdge(cell, e.shiftKey)) {
2072
- _tabHeld = false;
2073
- return;
2074
- }
2075
- e.preventDefault();
2076
- if (!e.repeat) {
2077
- _tabHeld = true;
2078
- _tabConsumed = false;
2079
- _tabShift = e.shiftKey;
2080
- }
2081
- return;
2082
- }
2083
- if (!_tabHeld) return;
2084
- const dir = ARROW_DIRS[e.key];
2085
- if (!dir) return;
2086
- e.preventDefault();
2087
- _tabConsumed = true;
2088
- moveEditor(dir, e.target);
2089
- };
2090
- const up = (evt) => {
2091
- const e = evt;
2092
- if (e.key !== "Tab" || !_tabHeld) return;
2093
- _tabHeld = false;
2094
- if (_tabConsumed) return;
2095
- moveEditor(_tabShift ? "left" : "right", e.target, { wrap: true });
2096
- };
2097
- document.addEventListener("keydown", down, true);
2098
- document.addEventListener("keyup", up, true);
2099
- _editorKeys = {
2100
- down,
2101
- up
2102
- };
2103
- }
2104
- function removeEditorKeys() {
2105
- _tabHeld = false;
2106
- _tabConsumed = false;
2107
- if (!_editorKeys || typeof document === "undefined") return;
2108
- document.removeEventListener("keydown", _editorKeys.down, true);
2109
- document.removeEventListener("keyup", _editorKeys.up, true);
2110
- _editorKeys = null;
2111
- }
2112
- function editorKeydown(event, rowKey, field) {
2113
- if (ctrl.editorNavKeys === "tab-arrows") return;
2114
- if (event.key === "Escape" || event.key === "Enter") {
2115
- const cell = event.target?.closest?.("[data-edit-cell]");
2116
- handleEditorActionKey(event, cell);
2117
- return;
2118
- }
2119
- if (event.key !== "Tab") return;
2120
- const cols = editableColumns();
2121
- const idx = cols.findIndex((c) => c.field === field);
2122
- if (idx === -1) return;
2123
- const rowIdx = indexOfRowKey(rowKey);
2124
- if (rowIdx === -1) return;
2125
- if (event.shiftKey) {
2126
- if (idx > 0) return;
2127
- const prev = ctrl.items[rowIdx - 1];
2128
- if (!prev) return;
2129
- event.preventDefault();
2130
- const prevKey = rowKeyOf(prev, rowIdx - 1);
2131
- beginEditRow(prevKey);
2132
- focusRowCell(event.target, prevKey, "last", { reveal: true });
2133
- } else {
2134
- if (idx < cols.length - 1) return;
2135
- const nextRow = ctrl.items[rowIdx + 1];
2136
- if (!nextRow) return;
2137
- event.preventDefault();
2138
- const nextKey = rowKeyOf(nextRow, rowIdx + 1);
2139
- beginEditRow(nextKey);
2140
- focusRowCell(event.target, nextKey, "first", { reveal: true });
2141
- }
2142
- }
2143
- function toFields(group) {
2144
- if (!group) return [];
2145
- return (Array.isArray(group) ? group : [group]).filter(Boolean);
2146
- }
2147
- /**
2148
- * Sort descriptor sent to the source. When grouping, the group fields lead
2149
- * (so each fetched/sliced page holds contiguous, ordered groups); the active
2150
- * column sort follows — applied to the group field itself if it is one.
2151
- */
2152
- function effectiveSort() {
2153
- const keys = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field)).map((s) => ({
2154
- field: s.field,
2155
- selector: declaredSortSelector(s.field) ?? s.field,
2156
- desc: s.order === "desc"
2157
- }));
2158
- const entry = (k) => ({
2159
- selector: k.selector,
2160
- desc: k.desc
2161
- });
2162
- if (!groupFields.length) return keys.length ? keys.map(entry) : null;
2163
- const list = groupFields.map((f) => ({
2164
- selector: f,
2165
- desc: keys.find((k) => k.field === f)?.desc ?? false
2166
- }));
2167
- for (const k of keys) if (!groupFields.includes(k.field)) list.push(entry(k));
2168
- return list;
2169
- }
2170
- /**
2171
- * Push the merged sort onto the bound source — the column sort plus the base
2172
- * sort as tiebreakers (see `mergedSort`). Remembers what it wrote so a sort the
2173
- * consumer sets on the source afterwards can be told apart and adopted.
2174
- */
2175
- function applySourceSort() {
2176
- const s = bound;
2177
- if (!s || typeof s.sort !== "function") return;
2178
- s.sort(mergedSort());
2179
- lastWrittenSort = s.sort() ?? null;
2180
- }
2181
- const notifier = createNotifier({ onFlush: () => {
2182
- syncMapped();
2183
- if (opts.state) opts.state.value = propsSnapshot();
2184
- } });
2185
- const notify = notifier.notify;
2186
- const keyExpr = opts.keyExpr ?? "Id";
2187
- const rowKeyOf = (row, i) => String(row?.[MONO_ROW_KEY] ?? row?.[keyExpr] ?? i);
2188
- /**
2189
- * Last-resort identity for a row we cannot place: stable per object, and never
2190
- * equal to another row's. See `rowKey` for why "-1 for everyone" is not an option.
2191
- */
2192
- const orphanKeys = /* @__PURE__ */ new WeakMap();
2193
- let orphanSeq = 0;
2194
- /**
2195
- * Does the loaded row set actually carry `keyExpr`? Drives the drain + the warning.
2196
- *
2197
- * `ctrl.items` is not always rows. Once grouping is on it holds group NODES
2198
- * (`{ key, items, count, level, path, … }`), and a node has none of the row's fields — so
2199
- * sampling it would report every correctly-configured grouped table as misconfigured. Dig one
2200
- * level down to the first group that actually has rows loaded instead; in server-group mode a
2201
- * group's rows arrive only when it is on screen, so "no rows yet" means not-yet-known, not
2202
- * missing, and takes the same benefit of the doubt as an empty ungrouped table.
2203
- */
2204
- function rowsHaveKeyExpr() {
2205
- /** A group node, not a row — the shape `syncGrouped` / `renderServer` put in `ctrl.items`. */
2206
- const isNode = (x) => !!x && typeof x === "object" && Array.isArray(x.items) && "path" in x;
2207
- /** First real row under `list`, descending as many group levels as there are. */
2208
- const firstRow = (list, depth = 0) => {
2209
- if (depth > 8) return void 0;
2210
- for (const entry of list) {
2211
- if (!isNode(entry)) return entry;
2212
- const found = firstRow(entry.items ?? [], depth + 1);
2213
- if (found != null) return found;
2214
- }
2215
- };
2216
- const sample = firstRow(ctrl.items);
2217
- if (sample == null || typeof sample !== "object") return true;
2218
- return keyExpr in sample;
2219
- }
2220
- let keyExprWarned = false;
2221
- /** Dev-only, once per controller: a wrong `keyExpr` otherwise fails completely silently. */
2222
- function warnMissingKeyExpr() {
2223
- if (keyExprWarned || ctrl.items.length === 0 || rowsHaveKeyExpr()) return;
2224
- keyExprWarned = true;
2225
- console.warn(`[mono-table] keyExpr "${keyExpr}" is not a property of the loaded rows${opts.keyExpr ? "" : " (nothing set one, so it defaulted to \"Id\")"}. Row keys fall back to identity, and a remote drain cannot \$select it. Set \`keyExpr\` to the row key field.`);
2226
- }
2227
- /**
2228
- * Public stable row key — prefers the `showForm` identity, else `keyExpr`.
2229
- *
2230
- * `unwrapReactive` first: Vue hands components a reactive PROXY of the row while
2231
- * `ctrl.items` holds the raw object, so a bare `indexOf(proxy)` is always `-1`.
2232
- * With `keyExpr` also missing that made EVERY row key `"-1"` — tick one row and
2233
- * the whole grid reads as ticked. A row we still cannot place gets its own
2234
- * WeakMap id rather than a shared sentinel, so two rows can never collide
2235
- * whatever the config. A row with a real index keeps its index-derived key, so
2236
- * array grids with no `keyExpr` are untouched.
2237
- */
2238
- function rowKey(row) {
2239
- const raw = unwrapReactive(row);
2240
- const i = indexOfRow(raw);
2241
- if (i >= 0) return rowKeyOf(raw, i);
2242
- const direct = rowKeyOf(raw, NaN);
2243
- if (direct !== "NaN") return direct;
2244
- warnMissingKeyExpr();
2245
- const obj = raw;
2246
- if (obj == null || typeof obj !== "object") return String(raw);
2247
- let id = orphanKeys.get(obj);
2248
- if (!id) {
2249
- id = `mono-orphan:${orphanSeq += 1}`;
2250
- orphanKeys.set(obj, id);
2251
- }
2252
- return id;
2253
- }
2254
- /** Columns that declare a per-column `map`, resolved once per mapping pass. */
2255
- function mapColumns() {
2256
- const out = [];
2257
- for (const c of elementProps.th ?? []) if (c?.field && typeof c.map === "function") out.push({
2258
- field: c.field,
2259
- map: c.map
2260
- });
2261
- return out;
2262
- }
2263
- /** True when anything at all would transform a row (lets the fast path skip). */
2264
- function hasMap() {
2265
- return typeof opts.map === "function" || mapColumns().length > 0;
2266
- }
2267
- /** One column's `map`, for the header filter's value labels. */
2268
- function columnMap(field) {
2269
- return mapColumns().find((c) => c.field === field)?.map;
2270
- }
2271
- /**
2272
- * Raw row → presentation row. The grid-level `map` builds the row, then each
2273
- * column's `map` overwrites its own field — reading the RAW value, deliberately
2274
- * not the grid map's output, so a column map behaves identically here and in the
2275
- * header-filter list (which can only hand it a raw distinct value).
2276
- *
2277
- * Returns the SAME object when nothing is configured, so no-map tables allocate
2278
- * nothing and `mapped === items` holds by reference.
2279
- */
2280
- function mapRow(row, index, cols = mapColumns()) {
2281
- const rowMap = typeof opts.map === "function" ? opts.map : null;
2282
- if (!rowMap && cols.length === 0) return row;
2283
- let out = rowMap ? rowMap(row, index) : { ...row };
2284
- if (cols.length && out && typeof out === "object") for (const c of cols) out[c.field] = c.map(row?.[c.field], row, index);
2285
- return out;
2286
- }
2287
- /** What `ctrl.mapped` was last built from — see `syncMapped`. */
2288
- let mappedFrom = [];
2289
- /**
2290
- * Recompute `ctrl.mapped` from `ctrl.items`. Cheap no-op without a map.
2291
- *
2292
- * Skipped when nothing it reads has moved: the same `items` array (same length), the same
2293
- * row set (`rowSetVersion`) and the same map config. It runs on EVERY notifier flush, so a
2294
- * checkbox tick or a loading flip re-mapped every row — every accumulated row under infinity
2295
- * scroll or page-size "all" — and handed out new `mapped` identities each time.
2296
- *
2297
- * Never skipped while a row can be edited IN PLACE (an inline edit, a `showForm` row, form
2298
- * ops): those write straight into the row objects, which no identity check can see.
2299
- */
2300
- function syncMapped() {
2301
- if (!hasMap()) {
2302
- ctrl.mapped = ctrl.items;
2303
- mappedFrom = [];
2304
- return;
2305
- }
2306
- const from = [
2307
- ctrl.items,
2308
- ctrl.items.length,
2309
- rowSetVersion,
2310
- elementProps.th,
2311
- opts.map
2312
- ];
2313
- if (!(formHandle != null || formOps.length > 0 || ctrl.editingKey != null) && from.length === mappedFrom.length && from.every((v, i) => Object.is(v, mappedFrom[i]))) return;
2314
- const cols = mapColumns();
2315
- ctrl.mapped = ctrl.items.map((row, i) => mapRow(row, i, cols));
2316
- mappedFrom = from;
2317
- }
2318
- /** Flatten `ctrl.items` into the ready-to-render, keyed `displayRows` list. */
2319
- function buildDisplayRows() {
2320
- const out = [];
2321
- const cols = mapColumns();
2322
- if (!isGrouped()) {
2323
- syncMapped();
2324
- const mapped = ctrl.mapped;
2325
- ctrl.items.forEach((row, i) => out.push({
2326
- key: `r:${rowKeyOf(row, i)}`,
2327
- kind: "row",
2328
- level: 0,
2329
- row,
2330
- mapped: mapped[i]
2331
- }));
2332
- return out;
2333
- }
2334
- const footerOn = !pageSizeAll && (serverMode() || defaultGroupRowPageSize > 0 || groupRowPaging.size > 0);
2335
- const walk = (nodes) => {
2336
- for (const node of nodes) {
2337
- out.push({
2338
- key: `g:${node.path}`,
2339
- kind: "group",
2340
- level: node.level,
2341
- node
2342
- });
2343
- if (node.collapsed) continue;
2344
- const children = node.items;
2345
- if (children.length > 0 && isGroupNode(children[0])) walk(children);
2346
- else {
2347
- children.forEach((row, i) => out.push({
2348
- key: `r:${node.path}:${rowKeyOf(row, i)}`,
2349
- kind: "row",
2350
- level: node.level + 1,
2351
- row,
2352
- mapped: mapRow(row, i, cols)
2353
- }));
2354
- if (footerOn) out.push({
2355
- key: `f:${node.path}`,
2356
- kind: "footer",
2357
- level: node.level,
2358
- node
2359
- });
2360
- }
2361
- }
2362
- };
2363
- walk(ctrl.items);
2364
- return out;
2365
- }
2366
- /** Rebuild `displayRows` from the current `items`, then notify subscribers. */
2367
- function render() {
2368
- ctrl.displayRows = buildDisplayRows();
2369
- notify();
2370
- }
2371
- /** Build the full group tree from every matched row the source holds. */
2372
- function fullGroups() {
2373
- const src = bound ? bound.items?.() ?? [] : [];
2374
- const from = [
2375
- src,
2376
- src.length,
2377
- groupFields.join("\0"),
2378
- rowSetVersion
2379
- ];
2380
- if (!(!(formHandle != null || formOps.length > 0 || ctrl.editingKey != null) && from.length === groupTreeFrom.length && from.every((v, i) => Object.is(v, groupTreeFrom[i])))) {
2381
- groupTree = buildGroups(flattenLeaves([...src]), groupFields, collapsed);
2382
- groupTreeFrom = from;
2383
- return groupTree;
2384
- }
2385
- const stamp = (nodes) => {
2386
- for (const node of nodes) {
2387
- node.collapsed = collapsed.has(node.path);
2388
- if (node.items.length && isGroupNode(node.items[0])) stamp(node.items);
2389
- }
2390
- };
2391
- stamp(groupTree);
2392
- return groupTree;
2393
- }
2394
- let groupTreeFrom = [];
2395
- let groupTree = [];
2396
- /** Index every group node by path (full, unsliced) for `groupPageInfo`. */
2397
- function indexPaths(nodes) {
2398
- for (const node of nodes) {
2399
- pathIndex.set(node.path, node);
2400
- if (node.items.length && isGroupNode(node.items[0])) indexPaths(node.items);
2401
- }
2402
- }
2403
- /** Clone nodes, slicing each group's items to its registered group-page. */
2404
- function toDisplay(nodes) {
2405
- return nodes.map((node) => {
2406
- let items = node.items.length > 0 && isGroupNode(node.items[0]) ? toDisplay(node.items) : node.items;
2407
- const pg = pageSizeAll ? null : groupRowPagingFor(node.path);
2408
- if (pg) {
2409
- const start = pg.pageIndex * pg.pageSize;
2410
- items = items.slice(start, start + pg.pageSize);
2411
- }
2412
- return {
2413
- ...node,
2414
- items
2415
- };
2416
- });
2417
- }
2418
- function syncGrouped() {
2419
- const all = fullGroups();
2420
- pathIndex.clear();
2421
- indexPaths(all);
2422
- const total = all.length;
2423
- const size = pageSizeAll ? Math.max(1, total) : Math.max(1, groupPageSize);
2424
- const pageCount = Math.max(1, Math.ceil(total / size));
2425
- if (groupPageIndex > pageCount - 1) groupPageIndex = pageCount - 1;
2426
- if (groupPageIndex < 0) groupPageIndex = 0;
2427
- const start = groupPageIndex * size;
2428
- ctrl.grouped = true;
2429
- ctrl.items = toDisplay(all.slice(start, start + size));
2430
- ctrl.totalCount = total;
2431
- ctrl.pageSize = size;
2432
- ctrl.pageSizeAll = pageSizeAll;
2433
- ctrl.pageIndex = groupPageIndex;
2434
- ctrl.pageCount = pageCount;
2435
- render();
2436
- }
2437
- /** Effective row paging for a group in server mode (always has a size). */
2438
- function serverRowPaging(path) {
2439
- return groupRowPagingFor(path) ?? {
2440
- pageIndex: 0,
2441
- pageSize: defaultGroupRowPageSize || groupPageSize
2442
- };
2443
- }
2444
- /** Re-render from the in-memory group list + loaded row pages (no fetch). */
2445
- function renderServer() {
2446
- pathIndex.clear();
2447
- const total = serverGroups.length;
2448
- const size = pageSizeAll ? Math.max(1, total) : Math.max(1, groupPageSize);
2449
- const pageCount = Math.max(1, Math.ceil(total / size));
2450
- if (groupPageIndex > pageCount - 1) groupPageIndex = pageCount - 1;
2451
- if (groupPageIndex < 0) groupPageIndex = 0;
2452
- const start = groupPageIndex * size;
2453
- const nodes = serverGroups.slice(start, start + size).map((meta) => {
2454
- const path = String(meta.key);
2455
- const node = {
2456
- key: meta.key,
2457
- items: collapsed.has(path) ? [] : groupRows.get(path) ?? [],
2458
- count: meta.count,
2459
- level: 0,
2460
- path,
2461
- collapsed: collapsed.has(path),
2462
- meta
2463
- };
2464
- pathIndex.set(path, node);
2465
- return node;
2466
- });
2467
- ctrl.grouped = true;
2468
- ctrl.items = nodes;
2469
- ctrl.totalCount = total;
2470
- ctrl.pageSize = size;
2471
- ctrl.pageSizeAll = pageSizeAll;
2472
- ctrl.pageIndex = groupPageIndex;
2473
- ctrl.pageCount = pageCount;
2474
- ctrl.loading = groupRowLoading.size > 0;
2475
- render();
2476
- }
2477
- /** Fetch a group's current page — or, in "all" mode, every row (chunked). */
2478
- async function loadGroupRows(meta, pg, want) {
2479
- if (!serverGroup) return;
2480
- const path = String(meta.key);
2481
- groupRowLoading.add(path);
2482
- renderServer();
2483
- try {
2484
- if (pageSizeAll) {
2485
- const acc = [];
2486
- for (let skip = 0;; skip += chunkSize) {
2487
- const chunk = await serverGroup.loadRows(meta.key, {
2488
- skip,
2489
- take: chunkSize,
2490
- search: searchValue,
2491
- ...serverCtx()
2492
- });
2493
- acc.push(...chunk ?? []);
2494
- if (!chunk || chunk.length < chunkSize || acc.length >= meta.count) break;
2495
- if (skip > 1e7) break;
2496
- }
2497
- groupRows.set(path, acc);
2498
- } else {
2499
- const rows = await serverGroup.loadRows(meta.key, {
2500
- skip: pg.pageIndex * pg.pageSize,
2501
- take: pg.pageSize,
2502
- search: searchValue,
2503
- ...serverCtx()
2504
- });
2505
- groupRows.set(path, rows ?? []);
2506
- }
2507
- groupRowsKey.set(path, want);
2508
- } catch (err) {
2509
- captureError(err, "groupRows");
2510
- } finally {
2511
- groupRowLoading.delete(path);
2512
- renderServer();
2513
- }
2514
- }
2515
- /** Fetch rows for every visible, expanded group missing its current page. */
2516
- async function fetchVisibleRows() {
2517
- if (!serverGroup) return;
2518
- const size = pageSizeAll ? Math.max(1, serverGroups.length) : Math.max(1, groupPageSize);
2519
- const start = groupPageIndex * size;
2520
- const visible = serverGroups.slice(start, start + size);
2521
- const jobs = [];
2522
- for (const meta of visible) {
2523
- const path = String(meta.key);
2524
- if (collapsed.has(path)) continue;
2525
- const pg = serverRowPaging(path);
2526
- const want = pageSizeAll ? "all" : `${pg.pageIndex}:${pg.pageSize}`;
2527
- if (groupRowsKey.get(path) === want || groupRowLoading.has(path)) continue;
2528
- jobs.push(loadGroupRows(meta, pg, want));
2529
- }
2530
- if (jobs.length) await Promise.all(jobs);
2531
- }
2532
- /** Reload the group list (cheap groupby), then the visible groups' rows. */
2533
- async function loadGroupsServer() {
2534
- if (!serverGroup) return;
2535
- if (bound) applyBase(bound);
2536
- const seq = ++groupSeq;
2537
- ctrl.loading = true;
2538
- notify();
2539
- let metas = [];
2540
- try {
2541
- metas = await serverGroup.loadGroups({
2542
- search: searchValue,
2543
- ...serverCtx()
2544
- }) ?? [];
2545
- clearError();
2546
- } catch (err) {
2547
- captureError(err, "group");
2548
- metas = [];
2549
- }
2550
- if (seq !== groupSeq) return;
2551
- serverGroups = metas;
2552
- clearRowCaches();
2553
- renderServer();
2554
- await fetchVisibleRows();
2555
- }
2556
- /**
2557
- * Republish `ctrl` from the bound source.
2558
- *
2559
- * `initial` marks the one call `bind()` makes for itself. Every OTHER call
2560
- * happens because the source produced a result — a load, a search, a filter, a
2561
- * sort, a page — so this is the funnel where `hasLoaded` latches, rather than
2562
- * the `changed` handler: a source that resolves `load()` without emitting still
2563
- * reaches here, and `bind()`'s own pass must NOT count, because reflecting
2564
- * whatever a source already holds is not the same as having asked it for
2565
- * anything.
2566
- */
2567
- function sync(initial = false) {
2568
- if (!initial) ctrl.hasLoaded = true;
2569
- warnMissingKeyExpr();
2570
- if (serverMode()) {
2571
- renderServer();
2572
- return;
2573
- }
2574
- if (isGrouped()) {
2575
- syncGrouped();
2576
- return;
2577
- }
2578
- if (scrollMode !== "off") {
2579
- syncScroll();
2580
- return;
2581
- }
2582
- if (browserRows && browserSortActive()) {
2583
- syncBrowser();
2584
- return;
2585
- }
2586
- if (pageSizeAll) {
2587
- ctrl.grouped = false;
2588
- ctrl.items = allRows;
2589
- ctrl.pageIndex = 0;
2590
- ctrl.pageSize = allRows.length || 1;
2591
- ctrl.pageSizeAll = true;
2592
- ctrl.totalCount = allRows.length;
2593
- ctrl.pageCount = 1;
2594
- render();
2595
- return;
2596
- }
2597
- const s = bound;
2598
- const rawItems = s ? [...s.items?.() ?? []] : [];
2599
- const total = s?.totalCount?.();
2600
- const pageSize = s?.pageSize?.() ?? rawItems.length;
2601
- const pageIndex = s?.pageIndex?.() ?? 0;
2602
- const hasTotal = typeof total === "number" && total >= 0;
2603
- ctrl.grouped = false;
2604
- ctrl.items = rawItems;
2605
- ctrl.pageIndex = pageIndex;
2606
- ctrl.pageSize = pageSize;
2607
- ctrl.pageSizeAll = false;
2608
- ctrl.totalCount = hasTotal ? total : rawItems.length;
2609
- if (hasTotal && pageSize > 0) ctrl.pageCount = Math.max(1, Math.ceil(total / pageSize));
2610
- else if (s?.isLastPage?.()) ctrl.pageCount = pageIndex + 1;
2611
- else ctrl.pageCount = pageIndex + 2;
2612
- render();
2613
- }
2614
- function syncLoading() {
2615
- ctrl.loading = bound?.isLoading?.() ?? false;
2616
- notify();
2617
- }
2618
- const onChanged = () => {
2619
- if (suppressSync) return;
2620
- sync();
2621
- recomputeSummaries();
2622
- };
2623
- const onLoadingChanged = () => syncLoading();
2624
- function detach() {
2625
- if (!bound) return;
2626
- bound.off("changed", onChanged);
2627
- bound.off("loadingChanged", onLoadingChanged);
2628
- bound.off("loadError", onLoadingChanged);
2629
- bound = null;
2630
- ctrl.hasLoaded = false;
2631
- ctrl.error = null;
2632
- }
2633
- function bind(next) {
2634
- const source = Array.isArray(next) ? monoArraySource(next, {
2635
- pageSize: opts.pageSize,
2636
- keyExpr: opts.keyExpr,
2637
- searchExpr: plainSearchColumns(searchFields()),
2638
- group: groupFields
2639
- }) : next;
2640
- if (bound === source) return;
2641
- detach();
2642
- bound = source;
2643
- ctrl.dataSource = source;
2644
- bumpSummary("data");
2645
- externalBase = source?.filter?.() ?? null;
2646
- lastWrittenFilter = externalBase;
2647
- lastComposed = void 0;
2648
- composeMemo = null;
2649
- lastWrittenSort = source?.sort?.() ?? null;
2650
- externalSort = normalizeSortList(lastWrittenSort);
2651
- resolvedKey = null;
2652
- appliedKnobs.clear();
2653
- if (!source) {
2654
- sync(true);
2655
- return;
2656
- }
2657
- rebuildGridExpr();
2658
- if (!opts.serverGroup && groupFields.length === 1 && typeof source.store === "function") serverGroup = storeGroupSource(source, {
2659
- groupField: groupFields[0],
2660
- select: opts.select,
2661
- searchExpr: plainSearchColumns(searchFields()),
2662
- searchOperation: opts.searchOperation,
2663
- groupSummary: opts.groupSummary
2664
- });
2665
- source.on("changed", onChanged);
2666
- source.on("loadingChanged", onLoadingChanged);
2667
- source.on("loadError", onLoadingChanged);
2668
- if (preferredPageSize != null) {
2669
- const cur = source.pageSize?.();
2670
- if (typeof cur === "number" && cur > 0) sourcePageSize = cur;
2671
- source.pageSize?.(preferredPageSize);
2672
- }
2673
- if (groupFields.length && !serverMode()) applySourceSort();
2674
- sync(true);
2675
- }
2676
- /**
2677
- * Record a failed operation so something can render it.
2678
- *
2679
- * A FRESH object every time, never a reused one. `<mono-table-error>` dismisses
2680
- * per failure rather than per message — it remembers the object it dismissed
2681
- * and re-shows when a different one arrives — so two identical failures have to
2682
- * be distinguishable, and object identity is the only thing that reliably is
2683
- * (a store rejecting with the plain string `'Network error'` gives `===` values
2684
- * for two entirely unrelated attempts).
2685
- */
2686
- function captureError(raw, source) {
2687
- const { message, status, detail } = describeError(raw, opts.errorMessages);
2688
- ctrl.error = {
2689
- raw,
2690
- message,
2691
- status,
2692
- detail,
2693
- source,
2694
- at: Date.now()
2695
- };
2696
- if (errorMode === "clear-list" && source !== "selectAll") {
2697
- clear();
2698
- return;
2699
- }
2700
- notify();
2701
- }
2702
- /**
2703
- * Drop a recorded error because an operation just succeeded.
2704
- *
2705
- * Called at each operation's own success point rather than from the render
2706
- * funnels (`sync` / `renderServer`), and that is not a stylistic choice:
2707
- * `loadGroupsServer` calls `renderServer()` immediately AFTER its catch, so a
2708
- * clear there would wipe the error it had just captured, every time.
2709
- *
2710
- * Guarded, so a table that has never failed does not publish a notify on every
2711
- * successful load for a field nobody changed.
2712
- */
2713
- function clearError() {
2714
- if (!ctrl.error) return;
2715
- ctrl.error = null;
2716
- notify();
2717
- }
2718
- async function runLoad(fn, source = "load") {
2719
- if (!bound) return;
2720
- try {
2721
- await Promise.resolve(fn());
2722
- clearError();
2723
- } catch (err) {
2724
- if (isCanceled(err)) return;
2725
- captureError(err, source);
2726
- throw err;
2727
- }
2728
- }
2729
- /**
2730
- * Whether the source pages. Client group mode pages the groups in memory, so
2731
- * it loads everything; otherwise `dataSourceOptions.paginate` decides, and the
2732
- * default is to page — a flat table always did.
2733
- */
2734
- function sourcePaginate() {
2735
- if (isGrouped()) return false;
2736
- return resolvedOpts.paginate ?? true;
2737
- }
2738
- async function load() {
2739
- const s = bound;
2740
- if (s) applyBase(s);
2741
- if (serverMode()) {
2742
- await loadGroupsServer();
2743
- return;
2744
- }
2745
- if (scrollActive()) {
2746
- await loadScrollPage(true);
2747
- return;
2748
- }
2749
- if (browserSortActive()) {
2750
- await loadBrowserSorted();
2751
- return;
2752
- }
2753
- browserRows = null;
2754
- if (pageSizeAll && !isGrouped()) {
2755
- await loadAllChunked();
2756
- return;
2757
- }
2758
- if (!s) return;
2759
- s.paginate?.(sourcePaginate());
2760
- await runLoad(() => s.load());
2761
- }
2762
- async function reload() {
2763
- bumpSummary("data");
2764
- const s = bound;
2765
- if (s) applyBase(s);
2766
- if (serverMode()) {
2767
- clearRowCaches();
2768
- await loadGroupsServer();
2769
- return;
2770
- }
2771
- if (scrollActive()) {
2772
- await loadScrollPage(true);
2773
- return;
2774
- }
2775
- if (browserSortActive()) {
2776
- await loadBrowserSorted();
2777
- return;
2778
- }
2779
- browserRows = null;
2780
- if (pageSizeAll && !isGrouped()) {
2781
- await loadAllChunked();
2782
- return;
2783
- }
2784
- if (!s) return;
2785
- s.paginate?.(sourcePaginate());
2786
- await runLoad(() => s.reload ? s.reload() : s.load(), "reload");
2787
- }
2788
- function clear() {
2789
- scrollToken++;
2790
- suppressSync = false;
2791
- accumulated = [];
2792
- allRows = [];
2793
- scrollNextPage = 0;
2794
- scrollHasMore = false;
2795
- vStart = vEnd = vPadTop = vPadBottom = 0;
2796
- serverGroups = [];
2797
- clearRowCaches();
2798
- ctrl.items = [];
2799
- ctrl.loading = false;
2800
- ctrl.totalCount = 0;
2801
- ctrl.pageIndex = 0;
2802
- ctrl.pageCount = 1;
2803
- ctrl.loadedCount = 0;
2804
- ctrl.hasMore = false;
2805
- ctrl.virtualStart = ctrl.virtualEnd = 0;
2806
- ctrl.virtualPadTop = ctrl.virtualPadBottom = 0;
2807
- render();
2808
- }
2809
- async function setPage(pageIndex) {
2810
- if (isGrouped()) {
2811
- groupPageIndex = Math.max(0, pageIndex);
2812
- sync();
2813
- if (serverMode()) await fetchVisibleRows();
2814
- return;
2815
- }
2816
- if (browserRows && browserSortActive()) {
2817
- browserPageIndex = Math.max(0, pageIndex);
2818
- syncBrowser();
2819
- return;
2820
- }
2821
- const s = bound;
2822
- if (!s) return;
2823
- s.pageIndex?.(Math.max(0, pageIndex));
2824
- await load();
2825
- }
2826
- async function setPageSize(pageSize) {
2827
- const all = pageSize === "all";
2828
- pageSizeAll = all;
2829
- if (isGrouped()) {
2830
- if (!all) groupPageSize = Math.max(1, pageSize);
2831
- groupPageIndex = 0;
2832
- sync();
2833
- if (serverMode()) await fetchVisibleRows();
2834
- return;
2835
- }
2836
- const s = bound;
2837
- if (!s) return;
2838
- if (browserRows && browserSortActive()) {
2839
- if (!all) {
2840
- browserPageSize = Math.max(1, pageSize);
2841
- s.pageSize?.(browserPageSize);
2842
- }
2843
- browserPageIndex = 0;
2844
- syncBrowser();
2845
- return;
2846
- }
2847
- if (all) {
2848
- await loadAllChunked();
2849
- return;
2850
- }
2851
- s.pageSize?.(pageSize);
2852
- s.pageIndex?.(0);
2853
- await load();
2854
- }
2855
- /**
2856
- * Ungrouped "all": pull every row, in `chunkSize` requests, accumulate, and
2857
- * show on one page. Works with a capped backend (e.g. 100 rows/request).
2858
- */
2859
- async function loadAllChunked() {
2860
- const s = bound;
2861
- if (!s) {
2862
- allRows = [];
2863
- sync();
2864
- return;
2865
- }
2866
- suppressSync = true;
2867
- ctrl.loading = true;
2868
- notify();
2869
- const acc = [];
2870
- try {
2871
- applyBase(s);
2872
- s.paginate?.(true);
2873
- s.pageSize?.(chunkSize);
2874
- let page = 0;
2875
- for (;;) {
2876
- s.pageIndex?.(page);
2877
- await runLoad(() => s.load());
2878
- const items = [...s.items?.() ?? []];
2879
- acc.push(...items);
2880
- const total = s.totalCount?.();
2881
- const reachedTotal = typeof total === "number" && total >= 0 && acc.length >= total;
2882
- if ((s.isLastPage?.() ?? items.length < chunkSize) || reachedTotal || items.length === 0) break;
2883
- page += 1;
2884
- if (page > 1e5) break;
2885
- }
2886
- } finally {
2887
- suppressSync = false;
2888
- }
2889
- allRows = acc;
2890
- ctrl.loading = bound?.isLoading?.() ?? false;
2891
- sync();
2892
- }
2893
- const BROWSER_SORT_MAX_ROWS = 5e3;
2894
- /** The active sort keys when ANY of them sorts in the browser, else `null`. */
2895
- function browserSortKeys() {
2896
- if (!ctrl.sorts.some((s) => isBrowserSortKey(s.field))) return null;
2897
- return ctrl.sorts.map((s) => {
2898
- const browser = isBrowserSortKey(s.field);
2899
- return {
2900
- field: s.field,
2901
- selector: declaredSortSelector(s.field) ?? s.field,
2902
- desc: s.order === "desc",
2903
- aggregate: browser ? declaredSortConfig(s.field).aggregate ?? "sum" : null
2904
- };
2905
- });
2906
- }
2907
- /** Browser sort applies: a browser-sorted column is active on a flat, paged, bound table. */
2908
- function browserSortActive() {
2909
- return !!bound && !isGrouped() && !serverMode() && !scrollActive() && !!browserSortKeys();
2910
- }
2911
- /** The smallest `maxRows` any active browser-sorted column declares (default 5000). */
2912
- function browserSortCap() {
2913
- const caps = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => Number(declaredSortConfig(s.field).maxRows)).filter((n) => Number.isFinite(n) && n > 0);
2914
- return caps.length ? Math.min(...caps) : BROWSER_SORT_MAX_ROWS;
2915
- }
2916
- /** A row's matches for `selector` reduced to one sortable value. */
2917
- function browserSortValue(row, selector, aggregate) {
2918
- const raw = getFieldValue(row, selector);
2919
- if (!aggregate) return raw;
2920
- const list = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
2921
- if (aggregate === "count") return list.length;
2922
- const nums = list.map(Number).filter((n) => Number.isFinite(n));
2923
- if (!nums.length) return aggregate === "sum" ? 0 : null;
2924
- if (aggregate === "min") return Math.min(...nums);
2925
- if (aggregate === "max") return Math.max(...nums);
2926
- const sum = nums.reduce((a, b) => a + b, 0);
2927
- return aggregate === "avg" ? sum / nums.length : sum;
2928
- }
2929
- /** Empties last in either direction; numbers numerically; text numeric-aware. */
2930
- function compareBrowserValues(a, b) {
2931
- const ea = a == null || a === "";
2932
- const eb = b == null || b === "";
2933
- if (ea || eb) return ea === eb ? 0 : ea ? 1 : -1;
2934
- if (typeof a === "number" && typeof b === "number") return a - b;
2935
- if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
2936
- return String(a).localeCompare(String(b), void 0, { numeric: true });
2937
- }
2938
- /** Every matching row, sorted by the active keys (stable). */
2939
- function sortBrowserRows(rows) {
2940
- const keys = browserSortKeys() ?? [];
2941
- const decorated = rows.map((row, i) => ({
2942
- row,
2943
- i,
2944
- v: keys.map((k) => browserSortValue(row, k.selector, k.aggregate))
2945
- }));
2946
- decorated.sort((x, y) => {
2947
- for (let k = 0; k < keys.length; k++) {
2948
- const ex = x.v[k] == null || x.v[k] === "";
2949
- const ey = y.v[k] == null || y.v[k] === "";
2950
- const c = compareBrowserValues(x.v[k], y.v[k]);
2951
- if (c) return ex || ey ? c : keys[k].desc ? -c : c;
2952
- }
2953
- return x.i - y.i;
2954
- });
2955
- return decorated.map((d) => d.row);
2956
- }
2957
- /** Too many rows to sort in the browser: drop those keys, warn, load normally. */
2958
- async function refuseBrowserSort(count, cap) {
2959
- const fields = ctrl.sorts.filter((s) => isBrowserSortKey(s.field)).map((s) => s.field);
2960
- console.warn(`[monoDataGrid] sort: "${fields.join("\", \"")}" sorts in the browser and needs every row, but ${count} match (maxRows ${cap}) — sort cleared.`);
2961
- ctrl.sorts = ctrl.sorts.filter((s) => !isBrowserSortKey(s.field));
2962
- syncPrimarySort();
2963
- browserRows = null;
2964
- applySourceSort();
2965
- notify();
2966
- await load();
2967
- }
2968
- /** Read every matching row, sort it in memory, show the first page. */
2969
- async function loadBrowserSorted() {
2970
- const s = bound;
2971
- if (!s) return;
2972
- const cap = browserSortCap();
2973
- const known = s.totalCount?.();
2974
- if (typeof known === "number" && known > cap) {
2975
- await refuseBrowserSort(known, cap);
2976
- return;
2977
- }
2978
- if (!browserPageSize) browserPageSize = Math.max(1, s.pageSize?.() || ctrl.pageSize || 20);
2979
- ctrl.loading = true;
2980
- notify();
2981
- let rows = [];
2982
- try {
2983
- await runLoad(async () => {
2984
- rows = await readAllFromSource();
2985
- });
2986
- } finally {
2987
- ctrl.loading = false;
2988
- }
2989
- if (rows.length > cap) {
2990
- await refuseBrowserSort(rows.length, cap);
2991
- return;
2992
- }
2993
- browserRows = sortBrowserRows(rows);
2994
- browserPageIndex = 0;
2995
- sync();
2996
- recomputeSummaries();
2997
- }
2998
- /** Map the in-memory sorted rows (current page, or all) onto `ctrl`. */
2999
- function syncBrowser() {
3000
- const rows = browserRows ?? [];
3001
- const size = pageSizeAll ? Math.max(1, rows.length) : Math.max(1, browserPageSize);
3002
- const pageCount = Math.max(1, Math.ceil(rows.length / size));
3003
- browserPageIndex = Math.min(Math.max(0, browserPageIndex), pageCount - 1);
3004
- const start = browserPageIndex * size;
3005
- ctrl.grouped = false;
3006
- ctrl.items = rows.slice(start, start + size);
3007
- ctrl.pageIndex = browserPageIndex;
3008
- ctrl.pageSize = size;
3009
- ctrl.pageSizeAll = pageSizeAll;
3010
- ctrl.totalCount = rows.length;
3011
- ctrl.pageCount = pageCount;
3012
- render();
3013
- }
3014
- /** Scroll modes only apply to a flat (ungrouped, non-server) grid. */
3015
- function scrollActive() {
3016
- return scrollMode !== "off" && !isGrouped() && !serverMode();
3017
- }
3018
- /** Map the accumulator (+ virtual window) onto `ctrl` and render. */
3019
- function syncScroll() {
3020
- const s = bound;
3021
- const total = s?.totalCount?.();
3022
- const hasTotal = typeof total === "number" && total >= 0;
3023
- ctrl.grouped = false;
3024
- ctrl.pageSizeAll = false;
3025
- ctrl.scrollMode = scrollMode;
3026
- ctrl.loading = s?.isLoading?.() ?? false;
3027
- ctrl.loadedCount = accumulated.length;
3028
- ctrl.hasMore = scrollHasMore;
3029
- ctrl.totalCount = hasTotal ? total : accumulated.length;
3030
- ctrl.pageIndex = 0;
3031
- ctrl.pageSize = accumulated.length || 1;
3032
- ctrl.pageCount = 1;
3033
- if (scrollMode === "virtual") {
3034
- const end = Math.min(Math.max(vStart, vEnd), accumulated.length);
3035
- const start = Math.min(Math.max(0, vStart), end);
3036
- ctrl.items = accumulated.slice(start, end);
3037
- ctrl.virtualStart = start;
3038
- ctrl.virtualEnd = end;
3039
- ctrl.virtualPadTop = Math.max(0, vPadTop);
3040
- ctrl.virtualPadBottom = Math.max(0, vPadBottom);
3041
- } else {
3042
- ctrl.items = accumulated;
3043
- ctrl.virtualStart = 0;
3044
- ctrl.virtualEnd = accumulated.length;
3045
- ctrl.virtualPadTop = 0;
3046
- ctrl.virtualPadBottom = 0;
3047
- }
3048
- render();
3049
- }
3050
- /**
3051
- * Load one source page and append it to the accumulator. `reset` clears the
3052
- * accumulator and starts from page 0 (a fresh query / mode enable).
3053
- *
3054
- * Concurrency: every entry claims a `scrollToken`. Only the holder of the newest one may
3055
- * append, advance the page counter or release the shared load flags -- a reset (filter change,
3056
- * search, rebind) routinely lands while a `loadNext()` is still awaiting its page, and
3057
- * `runLoad` swallows the cancellation that would otherwise stop the loser.
3058
- */
3059
- async function loadScrollPage(reset) {
3060
- const s = bound;
3061
- if (!s) {
3062
- accumulated = [];
3063
- scrollNextPage = 0;
3064
- scrollHasMore = false;
3065
- syncScroll();
3066
- return;
3067
- }
3068
- const token = ++scrollToken;
3069
- const mine = () => token === scrollToken;
3070
- if (reset) {
3071
- applyBase(s);
3072
- accumulated = [];
3073
- scrollNextPage = 0;
3074
- vStart = vEnd = vPadTop = vPadBottom = 0;
3075
- ctrl.items = [];
3076
- ctrl.loadedCount = 0;
3077
- ctrl.hasMore = false;
3078
- ctrl.virtualStart = ctrl.virtualEnd = 0;
3079
- ctrl.virtualPadTop = ctrl.virtualPadBottom = 0;
3080
- }
3081
- suppressSync = true;
3082
- ctrl.loading = true;
3083
- notify();
3084
- try {
3085
- s.paginate?.(true);
3086
- const page = scrollNextPage;
3087
- s.pageIndex?.(page);
3088
- await runLoad(() => s.load());
3089
- if (!mine()) return;
3090
- const rows = [...s.items?.() ?? []];
3091
- accumulated.push(...rows);
3092
- const total = s.totalCount?.();
3093
- const size = s.pageSize?.() ?? rows.length;
3094
- const reachedTotal = typeof total === "number" && total >= 0 && accumulated.length >= total;
3095
- const isLast = s.isLastPage?.() ?? rows.length < (size || 1);
3096
- scrollHasMore = !(reachedTotal || isLast || rows.length === 0);
3097
- scrollNextPage = page + 1;
3098
- } finally {
3099
- if (mine()) suppressSync = false;
3100
- }
3101
- if (!mine()) return;
3102
- ctrl.loading = bound?.isLoading?.() ?? false;
3103
- syncScroll();
3104
- }
3105
- async function setScrollPaging(mode, opts = {}) {
3106
- const reload = opts.reload !== false;
3107
- if (mode !== "off" && (isGrouped() || serverMode())) {
3108
- if (typeof console !== "undefined") console.warn(`[mono-table] scroll paging ("${mode}") requires a flat (ungrouped) table; keeping standard paging.`);
3109
- if (scrollMode !== "off") {
3110
- scrollMode = "off";
3111
- ctrl.scrollMode = "off";
3112
- }
3113
- return;
3114
- }
3115
- if (scrollMode === mode) return;
3116
- scrollMode = mode;
3117
- ctrl.scrollMode = mode;
3118
- if (mode === "off") {
3119
- if (!reload) {
3120
- syncScroll();
3121
- return;
3122
- }
3123
- accumulated = [];
3124
- await load();
3125
- return;
3126
- }
3127
- if (!bound) {
3128
- syncScroll();
3129
- return;
3130
- }
3131
- if (accumulated.length) {
3132
- syncScroll();
3133
- return;
3134
- }
3135
- await loadScrollPage(true);
3136
- }
3137
- async function loadNext() {
3138
- if (!scrollActive() || !scrollHasMore || scrollLoading) return;
3139
- scrollLoading = true;
3140
- try {
3141
- await loadScrollPage(false);
3142
- } finally {
3143
- scrollLoading = false;
3144
- }
3145
- }
3146
- function setVirtualWindow(start, end, padTop, padBottom) {
3147
- if (scrollMode !== "virtual") return;
3148
- const ns = Math.max(0, Math.floor(start));
3149
- const ne = Math.max(ns, Math.floor(end));
3150
- const nt = Math.max(0, Math.round(padTop));
3151
- const nb = Math.max(0, Math.round(padBottom));
3152
- if (ns === vStart && ne === vEnd && nt === vPadTop && nb === vPadBottom) return;
3153
- vStart = ns;
3154
- vEnd = ne;
3155
- vPadTop = nt;
3156
- vPadBottom = nb;
3157
- syncScroll();
3158
- }
3159
- async function setPreferredPageSize(pageSize) {
3160
- const v = typeof pageSize === "number" && pageSize > 0 ? Math.floor(pageSize) : null;
3161
- if (v === preferredPageSize) return;
3162
- const wasOverriding = preferredPageSize != null;
3163
- preferredPageSize = v;
3164
- const s = bound;
3165
- if (!s) return;
3166
- if (v != null) {
3167
- if (!wasOverriding) {
3168
- const cur = s.pageSize?.();
3169
- sourcePageSize = typeof cur === "number" && cur > 0 ? cur : null;
3170
- }
3171
- s.pageSize?.(v);
3172
- } else {
3173
- const base = resolvedOpts.pageSize;
3174
- if (typeof base === "number" && base > 0) s.pageSize?.(base);
3175
- else if (sourcePageSize != null) s.pageSize?.(sourcePageSize);
3176
- sourcePageSize = null;
3177
- }
3178
- s.pageIndex?.(0);
3179
- await load();
3180
- }
3181
- /**
3182
- * Rows a `*` pattern resolves against. The full backing array when the source
3183
- * has one (an in-memory source exposes `data()`), else the loaded page — which
3184
- * is all a remote source can offer, and is enough to read the shape.
3185
- */
3186
- function searchSampleRows() {
3187
- return bound?.data?.() ?? ctrl.items ?? [];
3188
- }
3189
- /**
3190
- * searchExpr as a list of entries — plain column names, `{ field, custom }` and
3191
- * `*` patterns resolved against the data. `textOnly` for a remote source, whose
3192
- * `$filter` can only `contains` a string column.
3193
- */
3194
- function searchEntries() {
3195
- return resolveSearchEntries(effectiveSearchFields(), {
3196
- rows: searchSampleRows(),
3197
- textOnly: hasStore()
3198
- });
3199
- }
3200
- /**
3201
- * The entries an unbound term fans out over, or the single entry a
3202
- * column-bound term targets. A bound field with no `searchExpr` entry keeps the
3203
- * old behaviour and is treated as a plain column.
3204
- */
3205
- function entriesForTerm(field, all) {
3206
- if (!field) return all;
3207
- return [searchEntryFor(searchFields(), field) ?? field];
3208
- }
3209
- /** Whether a remote search must bypass devextreme folding (a path/wildcard column). */
3210
- function searchIsPath() {
3211
- return hasStore() && plainSearchColumns(searchFields()).some(isPath);
3212
- }
3213
- /** Current live search string, held when the controller owns the search filter (path mode). */
3214
- let searchTerm = null;
3215
- /**
3216
- * Remote OData search filter expr, built by the controller when a searchExpr
3217
- * column is a path/wildcard (devextreme's own search folding can't emit `Job/Name`
3218
- * navigation or `Nav/any(...)` lambdas). Returns `null` when folding handles it.
3219
- */
3220
- /** Join expressions with `and` / `or`, returning the lone one unwrapped. */
3221
- function joinExprs(parts, join) {
3222
- const kept = parts.filter((p) => p != null);
3223
- if (!kept.length) return null;
3224
- if (kept.length === 1) return kept[0];
3225
- const out = [];
3226
- kept.forEach((p, i) => {
3227
- if (i) out.push(join);
3228
- out.push(p);
3229
- });
3230
- return out;
3231
- }
3232
- /**
3233
- * Group the live terms by `field` — a term with no field covers every
3234
- * `searchExpr` column. Preserves first-seen order so the emitted filter is
3235
- * stable and diffable.
3236
- */
3237
- function groupedSearchTerms() {
3238
- const order = [];
3239
- const byField = /* @__PURE__ */ new Map();
3240
- for (const t of ctrl.searchTerms) {
3241
- const v = String(t?.value ?? "");
3242
- if (!v) continue;
3243
- const key = t.field || void 0;
3244
- if (!byField.has(key)) {
3245
- byField.set(key, []);
3246
- order.push(key);
3247
- }
3248
- byField.get(key).push(v);
3249
- }
3250
- return order.map((field) => ({
3251
- field,
3252
- values: byField.get(field)
3253
- }));
3254
- }
3255
- /**
3256
- * Remote OData search filter built by the controller rather than devextreme's
3257
- * own `searchValue` folding. Used when folding CAN'T express the search:
3258
- * - a `searchExpr` column is a path/wildcard (`Job/Name`, `Nav/any(...)`), or
3259
- * - there is more than one term, or any term is bound to a single column —
3260
- * devextreme's `searchValue` holds exactly one string across one column set.
3261
- *
3262
- * Shape: OR within a field, AND across fields.
3263
- */
3264
- function remoteSearchExpr() {
3265
- const groups = groupedSearchTerms();
3266
- if (!groups.length || !controllerOwnsSearch()) return null;
3267
- const op = opts.searchOperation ?? "contains";
3268
- const all = searchEntries();
3269
- return joinExprs(groups.map((g) => {
3270
- const entries = entriesForTerm(g.field, all);
3271
- const clauses = [];
3272
- for (const value of g.values) {
3273
- const expr = joinExprs(entries.map((e) => typeof e === "string" ? toODataClause(e, op, value) : customRemoteClause(e, value, op)).filter((c) => c != null), "or");
3274
- if (expr != null) clauses.push(expr);
3275
- }
3276
- return joinExprs(clauses, "or");
3277
- }), "and");
3278
- }
3279
- /**
3280
- * Whether the controller (not devextreme) must build the search filter. One
3281
- * plain unbound term over plain columns can still ride devextreme's folding.
3282
- */
3283
- function controllerOwnsSearch() {
3284
- const groups = groupedSearchTerms();
3285
- if (!groups.length) return false;
3286
- if (!hasStore()) return false;
3287
- if (searchIsPath()) return true;
3288
- if (hasCustomSearch(effectiveSearchFields()) || hasWildcardSearch(effectiveSearchFields())) return true;
3289
- return !(groups.length === 1 && groups[0].values.length === 1 && !groups[0].field);
3290
- }
3291
- /** Client-side equivalent: OR within a field, AND across fields. */
3292
- function searchPredicate() {
3293
- const groups = groupedSearchTerms();
3294
- if (!groups.length) return null;
3295
- const op = opts.searchOperation ?? "contains";
3296
- const matches = (row, col, needle) => {
3297
- const raw = isPath(col) ? getFieldValue(row, col) : row?.[col];
3298
- return (Array.isArray(raw) ? raw : [raw]).some((v) => {
3299
- const s = String(v ?? "").toLowerCase();
3300
- const n = needle.toLowerCase();
3301
- if (op === "startswith") return s.startsWith(n);
3302
- if (op === "endswith") return s.endsWith(n);
3303
- if (op === "=" || op === "equals") return s === n;
3304
- return s.includes(n);
3305
- });
3306
- };
3307
- const all = searchEntries();
3308
- const perGroup = groups.map((g) => entriesForTerm(g.field, all));
3309
- return (row) => groups.every((g, i) => {
3310
- const entries = perGroup[i];
3311
- if (!entries.length) return true;
3312
- return g.values.some((needle) => entries.some((e) => {
3313
- if (typeof e === "string") return matches(row, e, needle);
3314
- const pred = customPredicate(e, needle, op);
3315
- return pred ? pred(row) : false;
3316
- }));
3317
- });
3318
- }
3319
- /**
3320
- * Whether an ARRAY source needs the controller's predicate. One plain unbound
3321
- * term can stay on the source's own `searchValue`/`searchExpr` — which matters
3322
- * when `searchExpr` was configured on `monoArraySource` and not on the grid.
3323
- */
3324
- function controllerOwnsArraySearch() {
3325
- const groups = groupedSearchTerms();
3326
- if (!groups.length) return false;
3327
- if (hasCustomSearch(effectiveSearchFields()) || hasWildcardSearch(effectiveSearchFields())) return true;
3328
- if (groups.length === 1 && groups[0].values.length === 1 && !groups[0].field) return false;
3329
- return true;
3330
- }
3331
- /** Single-term shorthand — the classic search box. */
3332
- async function setSearch(value) {
3333
- await setSearchTerms(value ? [{ value }] : []);
3334
- }
3335
- /**
3336
- * Replace the searched fields. `undefined`/`null` drops the override so the
3337
- * `searchExpr` / `search-value` option takes over again.
3338
- *
3339
- * Only re-runs the query when a search is actually live — changing the fields
3340
- * with an empty box has nothing to re-filter, and a needless `load()` would
3341
- * make `<mono-table-search>` refetch on every connect.
3342
- */
3343
- async function setSearchExpr(expr) {
3344
- searchFieldsOverride = expr == null ? void 0 : mergeSearchFields({ searchExpr: expr });
3345
- if (!ctrl.searchTerms.length) {
3346
- notify();
3347
- return;
3348
- }
3349
- await setSearchTerms(ctrl.searchTerms);
3350
- }
3351
- async function setSearchTerms(terms) {
3352
- ctrl.searchTerms = (terms ?? []).filter((t) => t && String(t.value ?? "") !== "");
3353
- groupPageIndex = 0;
3354
- bumpSummary("search");
3355
- if (serverMode()) {
3356
- searchValue = ctrl.searchTerms.map((t) => t.value).join(" ") || null;
3357
- notify();
3358
- await loadGroupsServer();
3359
- return;
3360
- }
3361
- const s = bound;
3362
- if (!s) return;
3363
- searchTerm = ctrl.searchTerms[0]?.value ?? null;
3364
- if (!hasStore()) {
3365
- if (controllerOwnsArraySearch()) s.searchValue?.(null);
3366
- else {
3367
- s.searchOperation?.(opts.searchOperation ?? "contains");
3368
- if (searchFieldsConfigured()) s.searchExpr?.(plainSearchColumns(searchFields()));
3369
- s.searchValue?.(searchTerm);
3370
- }
3371
- applyFilter();
3372
- } else if (controllerOwnsSearch()) {
3373
- s.searchValue?.(null);
3374
- applyFilter();
3375
- } else {
3376
- s.searchOperation?.(opts.searchOperation ?? "contains");
3377
- if (searchFieldsConfigured()) s.searchExpr?.(plainSearchColumns(searchFields()));
3378
- s.searchValue?.(searchTerm);
3379
- applyFilter();
3380
- }
3381
- s.pageIndex?.(0);
3382
- notify();
3383
- await load();
3384
- }
3385
- /**
3386
- * The grid's explicit filter — ONE layer, not a replacement.
3387
- *
3388
- * This is what the slotted filter builder writes, and it used to be a bare
3389
- * `s.filter(x)`: it replaced the consumer's own source filter, the column
3390
- * filters and the search in one go, and the next keystroke then replaced IT
3391
- * with the search alone. It is now composed like everything else — under the
3392
- * base, beside the columns and the search — and `null` clears only this layer.
3393
- * That is also why it is NOT the adopted external base: the builder's *Clear*
3394
- * would otherwise wipe the scope the consumer set on the source.
3395
- */
3396
- async function setFilter(filter) {
3397
- if (!bound) return;
3398
- bumpSummary("search");
3399
- explicitFilter = hasStore() ? filter ?? null : compileOrPredicate(filter);
3400
- applyFilter();
3401
- groupPageIndex = 0;
3402
- await load();
3403
- }
3404
- const columnFilters = /* @__PURE__ */ new Map();
3405
- /** The combine gesture, remembered — see `MonoSetColumnFilterOptions.sticky`. */
3406
- let filterCombining = false;
3407
- /** Whether the bound source is a remote DataSource (has a devextreme store). */
3408
- function hasStore() {
3409
- return typeof bound?.store === "function" && !!bound.store();
3410
- }
3411
- /**
3412
- * OR-join a column's value clauses into a devextreme filter expression (path-aware).
3413
- *
3414
- * A value is normally a plain equality; a `{ from, to }` range (what the DATE
3415
- * filter stores) becomes `[[f,'>=',from],'and',[f,'<',to]]` — half-open, so
3416
- * a "March" range never touches April 1st 00:00:00.
3417
- */
3418
- function orClauses(field, values) {
3419
- const clauses = values.map((v) => {
3420
- if (isDateRange(v)) {
3421
- const ge = toODataClause(field, ">=", v.from);
3422
- const lt = toODataClause(field, "<", v.to);
3423
- return ge && lt ? [
3424
- ge,
3425
- "and",
3426
- lt
3427
- ] : null;
3428
- }
3429
- return toODataClause(field, "=", v);
3430
- }).filter((c) => c != null);
3431
- if (!clauses.length) return null;
3432
- if (clauses.length === 1) return clauses[0];
3433
- const out = [];
3434
- clauses.forEach((c, i) => {
3435
- if (i) out.push("or");
3436
- out.push(c);
3437
- });
3438
- return out;
3439
- }
3440
- /**
3441
- * Rebuild the grid's OWN filter layers from the column filters and the
3442
- * controller-owned search — `columnExpr` (columns only, what a server-group
3443
- * source gets) and `gridExpr` (columns AND search, what the source gets).
3444
- *
3445
- * Only builds; it writes nothing. Until this was split out, the write was here
3446
- * too, and it wrote THIS expression alone — so a filter the consumer had put on
3447
- * the source was gone on the first search keystroke and "restored" to nothing
3448
- * on clear. The write now goes through `writeFilter()`, which composes these on
3449
- * top of the base.
3450
- */
3451
- /** The active column filters, optionally leaving one column out (its own panel). */
3452
- function activeColumnFilters(except) {
3453
- return [...columnFilters.entries()].filter(([f, v]) => v.length && f !== except);
3454
- }
3455
- /** Remote: the column filters as ONE devextreme expression — AND across columns, OR within. */
3456
- function columnFilterExpr(except) {
3457
- const cols = activeColumnFilters(except).map(([f, v]) => orClauses(f, v)).filter((c) => c != null);
3458
- if (!cols.length) return null;
3459
- if (cols.length === 1) return cols[0];
3460
- const out = [];
3461
- cols.forEach((c, i) => {
3462
- if (i) out.push("and");
3463
- out.push(c);
3464
- });
3465
- return out;
3466
- }
3467
- /**
3468
- * Array / client: the same as a predicate. Path fields resolve nested/wildcard
3469
- * values (wildcard = match any element).
3470
- */
3471
- function columnFilterPredicate(except) {
3472
- const active = activeColumnFilters(except);
3473
- if (!active.length) return null;
3474
- return (row) => active.every(([f, vals]) => {
3475
- const raw = isPath(f) ? getFieldValue(row, f) : row?.[f];
3476
- return (Array.isArray(raw) ? raw : [raw]).some((rv) => vals.some((v) => {
3477
- if (isDateRange(v)) {
3478
- const d = parseDateValue(rv);
3479
- if (!d) return false;
3480
- const from = v.from instanceof Date ? v.from : new Date(v.from);
3481
- const to = v.to instanceof Date ? v.to : new Date(v.to);
3482
- return inRange(d, {
3483
- from,
3484
- to
3485
- });
3486
- }
3487
- return String(rv) === String(v);
3488
- }));
3489
- });
3490
- }
3491
- function rebuildGridExpr() {
3492
- if (hasStore()) {
3493
- const expr = columnFilterExpr();
3494
- columnExpr = expr;
3495
- gridExpr = andFilters(expr, remoteSearchExpr());
3496
- } else {
3497
- const colPred = columnFilterPredicate();
3498
- const sPred = controllerOwnsArraySearch() ? searchPredicate() : null;
3499
- columnExpr = colPred;
3500
- gridExpr = andPredicates(colPred, sPred);
3501
- }
3502
- }
3503
- /**
3504
- * A grid-driven filter change: search, column filter, `setFilter`. Adopts
3505
- * anything the consumer changed on the source in the meantime, rebuilds the
3506
- * grid's layers, writes the composed filter and goes back to page 0.
3507
- */
3508
- function applyFilter() {
3509
- const s = bound;
3510
- if (!s) return;
3511
- syncExternalFilter(s);
3512
- rebuildGridExpr();
3513
- writeFilter(s);
3514
- s.pageIndex?.(0);
3515
- }
3516
- async function setColumnFilter(field, values, options) {
3517
- if (!field) return;
3518
- const multi = options?.multi === true;
3519
- if (!multi) columnFilters.clear();
3520
- if (!values || !values.length) columnFilters.delete(field);
3521
- else columnFilters.set(field, [...values]);
3522
- filterCombining = columnFilters.size > 0 && multi && (options?.sticky === true || filterCombining);
3523
- groupPageIndex = 0;
3524
- bumpSummary("search");
3525
- notify();
3526
- applyFilter();
3527
- await load();
3528
- }
3529
- function columnFilter(field) {
3530
- return [...columnFilters.get(field) ?? []];
3531
- }
3532
- function columnFilterCombining() {
3533
- return filterCombining;
3534
- }
3535
- function filteredColumns() {
3536
- return [...columnFilters.entries()].filter(([, v]) => v.length).map(([f]) => f);
3537
- }
3538
- let dsOptionsSource = opts.dataSourceOptions;
3539
- let odataOptionsSource = opts.odataOptions;
3540
- /** The merged, normalised options as of the last resolve. */
3541
- let resolvedOpts = {};
3542
- /** Change key of `resolvedOpts` — `null` until the first resolve, which always counts. */
3543
- let resolvedKey = null;
3544
- /** A function-valued `filter` cannot go through the key; compared by reference instead. */
3545
- let optionsFilterRef = void 0;
3546
- let optionsFilter = null;
3547
- let optionsSort = [];
3548
- /** Knobs `applyBaseOptions` has written, so a knob that DISAPPEARS is cleared, not left behind. */
3549
- const appliedKnobs = /* @__PURE__ */ new Set();
3550
- let externalBase = null;
3551
- let explicitFilter = null;
3552
- let columnExpr = null;
3553
- let gridExpr = null;
3554
- /** What the source HELD after the grid's last write (read back — a source may normalise). */
3555
- let lastWrittenFilter = null;
3556
- /** What the grid last composed and handed to the source. */
3557
- let lastComposed = void 0;
3558
- let composeMemo = null;
3559
- let externalSort = [];
3560
- let lastWrittenSort = null;
3561
- let rawFilterWarned = false;
3562
- /** Array-source filter layer: a predicate as given, an array compiled, a raw OData string dropped. */
3563
- function compileOrPredicate(filter) {
3564
- if (filter == null) return null;
3565
- if (typeof filter === "function") return filter;
3566
- if (Array.isArray(filter) && filter.length === 1 && typeof filter[0] === "string") {
3567
- if (!rawFilterWarned) {
3568
- rawFilterWarned = true;
3569
- console.warn("[monoDataGrid] a raw OData $filter string cannot filter an array source — ignored.");
3570
- }
3571
- return null;
3572
- }
3573
- return compileFilterPredicate(filter);
3574
- }
3575
- /**
3576
- * Re-read the two option bags. Returns whether anything changed since the last
3577
- * resolve — the getters hand back a fresh object every time, so the comparison
3578
- * is by content, not identity.
3579
- */
3580
- function resolveDsOptions() {
3581
- const merged = resolveDataSourceOptions(dsOptionsSource, odataOptionsSource);
3582
- const fnFilter = typeof merged.filter === "function" ? merged.filter : void 0;
3583
- const key = dataSourceOptionsKey(merged);
3584
- if (key === resolvedKey && fnFilter === optionsFilterRef) return false;
3585
- resolvedKey = key;
3586
- optionsFilterRef = fnFilter;
3587
- resolvedOpts = merged;
3588
- optionsFilter = hasStore() ? merged.filter ?? null : compileOrPredicate(merged.filter);
3589
- optionsSort = normalizeSortList(merged.sort);
3590
- return true;
3591
- }
3592
- /**
3593
- * Adopt a filter the consumer set on the source directly: anything the source
3594
- * holds that is not what the grid last put there. One comparison is enough
3595
- * because `writeFilter` records what the source HOLDS after a write, not what
3596
- * was passed — so a source that ignores writes (a getter-only fake) reads back
3597
- * as its own last write and is never adopted, and a source that normalises
3598
- * compares against its normalised value.
3599
- */
3600
- function syncExternalFilter(s) {
3601
- if (typeof s.filter !== "function") return false;
3602
- const live = s.filter() ?? null;
3603
- if (live === lastWrittenFilter) return false;
3604
- externalBase = live;
3605
- return true;
3606
- }
3607
- /** The same for `ds.sort(...)`. */
3608
- function syncExternalSort(s) {
3609
- if (typeof s.sort !== "function") return false;
3610
- const live = s.sort() ?? null;
3611
- if (live === lastWrittenSort) return false;
3612
- lastWrittenSort = live;
3613
- externalSort = normalizeSortList(live);
3614
- return true;
3615
- }
3616
- /** options ∧ external ∧ explicit — everything under the grid's own layers. */
3617
- function baseFilter() {
3618
- return hasStore() ? andFilters(andFilters(optionsFilter, externalBase), explicitFilter) : andPredicates(optionsFilter, externalBase, explicitFilter);
3619
- }
3620
- /** What a server-group source gets: the base plus the column filters, NOT the search (it folds that itself). */
3621
- function baseFilterWithColumns() {
3622
- return hasStore() ? andFilters(baseFilter(), columnExpr) : andPredicates(baseFilter(), columnExpr);
3623
- }
3624
- function composeFilter() {
3625
- const m = composeMemo;
3626
- if (m && m.o === optionsFilter && m.e === externalBase && m.x === explicitFilter && m.g === gridExpr) return m.out;
3627
- const out = hasStore() ? andFilters(baseFilter(), gridExpr) : andPredicates(baseFilter(), gridExpr);
3628
- composeMemo = {
3629
- o: optionsFilter,
3630
- e: externalBase,
3631
- x: explicitFilter,
3632
- g: gridExpr,
3633
- out
3634
- };
3635
- return out;
3636
- }
3637
- /** Write the composed filter — only when it is not what the source already holds. */
3638
- function writeFilter(s) {
3639
- if (typeof s.filter !== "function") return false;
3640
- const next = composeFilter();
3641
- if (next === lastComposed && (s.filter() ?? null) === lastWrittenFilter) return false;
3642
- s.filter(next);
3643
- lastComposed = next;
3644
- lastWrittenFilter = s.filter() ?? null;
3645
- return true;
3646
- }
3647
- /**
3648
- * The sort actually sent: the user's column sort first (it wins), then the
3649
- * options' sort, then whatever the consumer set on the source — later entries
3650
- * only for selectors not already sorted, so they act as tiebreakers. Clearing
3651
- * the column sort therefore falls back to the base instead of to nothing.
3652
- */
3653
- function mergedSort() {
3654
- let keys = effectiveSort() ?? [];
3655
- if (hasStore()) keys = keys.map((s) => {
3656
- if (!isPath(s.selector)) return s;
3657
- const selector = toODataSelector(s.selector);
3658
- if (selector) return {
3659
- ...s,
3660
- selector
3661
- };
3662
- console.warn(`[monoDataGrid] sort: path "${s.selector}" is not an OData selector — skipped.`);
3663
- return null;
3664
- }).filter((s) => !!s);
3665
- const out = [...keys];
3666
- for (const e of [...optionsSort, ...externalSort]) if (!out.some((k) => k.selector === e.selector)) out.push({
3667
- selector: e.selector,
3668
- desc: !!e.desc
3669
- });
3670
- return out.length ? out : null;
3671
- }
3672
- /** Push the non-filter knobs of `resolvedOpts` onto the source. Only called when they changed. */
3673
- function applyBaseOptions(s) {
3674
- const o = resolvedOpts;
3675
- const knob = (name, present, write) => {
3676
- if (present) {
3677
- write(false);
3678
- appliedKnobs.add(name);
3679
- } else if (appliedKnobs.has(name)) {
3680
- write(true);
3681
- appliedKnobs.delete(name);
3682
- }
3683
- };
3684
- knob("select", o.select !== void 0, (clear) => s.select?.(clear ? null : toList(o.select) ?? null));
3685
- knob("requireTotalCount", typeof o.requireTotalCount === "boolean", (clear) => s.requireTotalCount?.(clear ? false : !!o.requireTotalCount));
3686
- if (!searchFieldsConfigured()) {
3687
- knob("searchExpr", o.searchExpr !== void 0, (clear) => s.searchExpr?.(clear ? null : o.searchExpr));
3688
- knob("searchOperation", typeof o.searchOperation === "string", (clear) => s.searchOperation?.(clear ? "contains" : String(o.searchOperation)));
3689
- }
3690
- if (preferredPageSize == null && typeof o.pageSize === "number" && o.pageSize > 0) s.pageSize?.(o.pageSize);
3691
- const lo = typeof s.loadOptions === "function" ? s.loadOptions() : null;
3692
- if (lo && typeof lo === "object") {
3693
- const extras = extraLoadOptions(o);
3694
- for (const k of Object.keys(extras)) knob(`lo:${k}`, extras[k] !== void 0, (clear) => {
3695
- if (clear) delete lo[k];
3696
- else lo[k] = extras[k];
3697
- });
3698
- for (const name of [...appliedKnobs]) if (name.startsWith("lo:") && !(name.slice(3) in extras)) {
3699
- delete lo[name.slice(3)];
3700
- appliedKnobs.delete(name);
3701
- }
3702
- }
3703
- if (optionsSort.length || appliedKnobs.has("sort")) {
3704
- applySourceSort();
3705
- if (optionsSort.length) appliedKnobs.add("sort");
3706
- else appliedKnobs.delete("sort");
3707
- }
3708
- }
3709
- /**
3710
- * The one call every load funnel makes first: re-read the options, adopt what
3711
- * the consumer changed on the source, push the knobs, write the composed
3712
- * filter. Never loads — the funnel does that — and writes nothing when nothing
3713
- * moved, so a plain `setPage(n)` costs no filter write and keeps its page.
3714
- */
3715
- function applyBase(s) {
3716
- const optChanged = resolveDsOptions();
3717
- const extChanged = syncExternalFilter(s);
3718
- const sortAdopted = syncExternalSort(s);
3719
- if (optChanged) applyBaseOptions(s);
3720
- else if (sortAdopted && (ctrl.sorts.length || optionsSort.length)) applySourceSort();
3721
- if (optChanged || extChanged) {
3722
- bumpSummary("search");
3723
- groupPageIndex = 0;
3724
- }
3725
- writeFilter(s);
3726
- return optChanged || extChanged;
3727
- }
3728
- /** The `MonoServerGroupCtx` for the current query. */
3729
- function serverCtx() {
3730
- const loadOptions = {};
3731
- if (resolvedOpts.expand !== void 0) loadOptions.expand = resolvedOpts.expand;
3732
- if (resolvedOpts.customQueryParams !== void 0) loadOptions.customQueryParams = resolvedOpts.customQueryParams;
3733
- const filter = baseFilterWithColumns();
3734
- const select = opts.select ?? toList(resolvedOpts.select);
3735
- return {
3736
- sort: mergedSort(),
3737
- ...filter != null ? { filter } : {},
3738
- ...select ? { select } : {},
3739
- ...Object.keys(loadOptions).length ? { loadOptions } : {}
3740
- };
3741
- }
3742
- async function setDataSourceOptions(next) {
3743
- dsOptionsSource = next;
3744
- await refresh();
3745
- }
3746
- async function setOdataOptions(next) {
3747
- odataOptionsSource = next;
3748
- await refresh();
3749
- }
3750
- async function refresh() {
3751
- const s = bound;
3752
- if (!s) return;
3753
- if (applyBase(s)) s.pageIndex?.(0);
3754
- await load();
3755
- }
3756
- function resolvedDataSourceOptions() {
3757
- return resolvedOpts;
3758
- }
3759
- /** The panel's order: nulls first, numeric before lexical. Shared by both distinct paths. */
3760
- function sortColumnValues(values) {
3761
- return values.sort((a, b) => {
3762
- const av = a.value, bv = b.value;
3763
- if (av == null) return bv == null ? 0 : -1;
3764
- if (bv == null) return 1;
3765
- const an = Number(av), bn = Number(bv);
3766
- if (!Number.isNaN(an) && !Number.isNaN(bn)) return an - bn;
3767
- return String(av).localeCompare(String(bv));
3768
- });
3769
- }
3770
- /**
3771
- * Distinct values as ONE `$apply=groupby((field),aggregate($count as count))`
3772
- * request through the store, or `null` when that is not on.
3773
- *
3774
- * `null` — not `[]` — is the fallback signal, and it comes from four places:
3775
- * the path switched off (`serverApply: false`, or a backend that already
3776
- * rejected an `$apply` — the gate remembers), a wildcard path (there is no
3777
- * selector to group by), a store that cannot carry an `$apply` at all (a
3778
- * CustomStore — `loadApply` sends nothing) or a request that failed, and a
3779
- * response that is not grouped (a server that ignored the clause hands back
3780
- * plain entities, which have no `count`). An EMPTY grouped result is a real
3781
- * answer (nothing in scope) and is returned as such.
3782
- *
3783
- * The request carries `$apply` and nothing else: `$select` beside `$apply` is
3784
- * rejected by most servers, a sibling `$filter` would be applied a second time
3785
- * (the base folds INSIDE the clause), and `$top` only if the column asked for
3786
- * a cap. `loadApply` puts the clause INTO the url (`urlOverride` —
3787
- * `customQueryParams` is quoted as a literal by devextreme and becomes a
3788
- * function call on v4), so this rides the store's URL, `beforeSend` and auth
3789
- * exactly as every other request does.
3790
- */
3791
- async function distinctViaApply(store, field, options) {
3792
- if (applyGate.skip) return null;
3793
- if (parseFieldPath(field).hasWildcard) return null;
3794
- const selector = isPath(field) ? toODataSelector(field) : field;
3795
- if (!selector) return null;
3796
- const apply = `groupby((${selector}),aggregate($count as count))`;
3797
- const odataFilter = options?.filter != null ? arrayToODataString(options.filter) : "";
3798
- const clause = odataFilter ? `filter(${odataFilter})/${apply}` : apply;
3799
- const top = typeof options?.take === "number" && options.take >= 0 ? options.take : void 0;
3800
- let rows;
3801
- try {
3802
- rows = await loadApply(store, clause, { top });
3803
- } catch (err) {
3804
- applyGate.reject(err);
3805
- return null;
3806
- }
3807
- if (!rows) return null;
3808
- if (!rows.length) return [];
3809
- const first = rows[0];
3810
- if (!first || typeof first !== "object" || typeof first.count !== "number") return null;
3811
- if (!isRolledUp(rows, [field, "count"])) return null;
3812
- const path = isPath(field);
3813
- return sortColumnValues(rows.map((r) => ({
3814
- value: path ? getFieldValue(r, field) : r[field],
3815
- count: Number(r.count) || 0
3816
- })));
3817
- }
3818
- /** Count occurrences of `field` across `rows` into a sorted `{value,count}[]`. */
3819
- function distinctFromRows(rows, field) {
3820
- const counts = /* @__PURE__ */ new Map();
3821
- const bump = (v) => {
3822
- counts.set(v, (counts.get(v) ?? 0) + 1);
3823
- };
3824
- const path = isPath(field);
3825
- for (const r of rows) if (path) {
3826
- const v = getFieldValue(r, field);
3827
- if (Array.isArray(v)) v.forEach(bump);
3828
- else bump(v);
3829
- } else bump(r?.[field]);
3830
- return sortColumnValues([...counts.entries()].map(([value, count]) => ({
3831
- value,
3832
- count
3833
- })));
3834
- }
3835
- async function distinctValues(field, options) {
3836
- if (!field) return [];
3837
- resolveDsOptions();
3838
- if (bound) syncExternalFilter(bound);
3839
- const base = baseFilter();
3840
- const cascade = opts.headerFilterCascade !== false;
3841
- let filter;
3842
- if (hasStore()) {
3843
- filter = andFilters(base, options?.filter ?? null);
3844
- if (cascade) {
3845
- const search = remoteSearchExpr() ?? searchFilterOf(bound) ?? null;
3846
- filter = andFilters(filter, andFilters(columnFilterExpr(field), search));
3847
- }
3848
- } else {
3849
- filter = andPredicates(base, typeof options?.filter === "function" ? options.filter : null);
3850
- if (cascade) filter = andPredicates(andPredicates(filter, columnFilterPredicate(field)), searchPredicate());
3851
- }
3852
- const scoped = filter == null ? options : {
3853
- ...options ?? {},
3854
- filter
3855
- };
3856
- if (typeof opts.distinctValues === "function") {
3857
- const filter = hasStore() ? scoped?.filter ?? null : null;
3858
- const odataFilter = filter != null ? arrayToODataString(filter) : "";
3859
- const apply = `groupby((${isPath(field) ? toODataSelector(field) ?? field : field}),aggregate($count as count))`;
3860
- const ctx = {
3861
- filter,
3862
- odataFilter,
3863
- apply,
3864
- applyWithFilter: odataFilter ? `filter(${odataFilter})/${apply}` : apply,
3865
- source: bound
3866
- };
3867
- return (await opts.distinctValues(field, scoped, ctx) ?? []).map((r) => {
3868
- const o = r;
3869
- return o && typeof o === "object" && "value" in o ? {
3870
- value: o.value,
3871
- count: Number(o.count ?? 0) || 0
3872
- } : {
3873
- value: r,
3874
- count: 0
3875
- };
3876
- });
3877
- }
3878
- const s = bound;
3879
- if (!hasStore()) {
3880
- let rows = s?.data?.() ?? ctrl.items;
3881
- if (typeof scoped?.filter === "function") rows = rows.filter(scoped.filter);
3882
- if (typeof scoped?.take === "number" && scoped.take >= 0) rows = rows.slice(0, scoped.take);
3883
- return distinctFromRows(rows, field);
3884
- }
3885
- const store = s.store();
3886
- const grouped = await distinctViaApply(store, field, scoped);
3887
- if (grouped) return grouped;
3888
- const loadOpts = {};
3889
- if (!isPath(field)) loadOpts.select = [field];
3890
- for (const [key, value] of Object.entries(scoped ?? {})) if (value == null) delete loadOpts[key];
3891
- else loadOpts[key] = value;
3892
- const res = await store.load(loadOpts);
3893
- return distinctFromRows(storeRows(res), field);
3894
- }
3895
- async function setData(next) {
3896
- const s = bound;
3897
- if (typeof s?.setData !== "function") return;
3898
- bumpSummary("data");
3899
- await runLoad(() => s.setData(next));
3900
- if (scrollActive()) await loadScrollPage(true);
3901
- }
3902
- /** The combine gesture, remembered — see `MonoSetSortOptions.sticky`. */
3903
- let sortCombineMode = false;
3904
- function sortCombining() {
3905
- return sortCombineMode;
3906
- }
3907
- /** This column's direction, or null when it isn't part of the sort. */
3908
- function sortOf(field) {
3909
- return ctrl.sorts.find((s) => s.field === field)?.order ?? null;
3910
- }
3911
- /** 1-based precedence in `sorts`; `0` when the column isn't sorted. */
3912
- function sortIndex(field) {
3913
- return ctrl.sorts.findIndex((s) => s.field === field) + 1;
3914
- }
3915
- /**
3916
- * The 1-based precedence a column DECLARED via `sort.index`, if any. Central
3917
- * `props.th` config wins over the element's own `:sort.prop`, matching how every
3918
- * other key resolves.
3919
- */
3920
- function declaredSortIndex(field) {
3921
- const fromProps = (elementProps.th ?? []).find((c) => c?.field === field)?.sort;
3922
- if (typeof fromProps === "object" && typeof fromProps?.index === "number") return fromProps.index;
3923
- for (const el of columnEls) if (el.field === field && typeof el.sort === "object" && typeof el.sort?.index === "number") return el.sort.index;
3924
- }
3925
- /**
3926
- * Re-order `sorts` so any column declaring `sort.index` sits at that slot.
3927
- * Unindexed columns keep their click order and fill the remaining slots — the
3928
- * comparator falls back to the current position, so the sort is stable.
3929
- * No-ops (and allocates nothing) when no column declares an index.
3930
- */
3931
- function applyDeclaredSortOrder() {
3932
- if (ctrl.sorts.length < 2) return;
3933
- const marked = ctrl.sorts.map((s, i) => ({
3934
- s,
3935
- i,
3936
- hint: declaredSortIndex(s.field)
3937
- }));
3938
- if (!marked.some((m) => typeof m.hint === "number")) return;
3939
- marked.sort((a, b) => {
3940
- const ah = a.hint ?? Number.POSITIVE_INFINITY;
3941
- const bh = b.hint ?? Number.POSITIVE_INFINITY;
3942
- return ah === bh ? a.i - b.i : ah - bh;
3943
- });
3944
- ctrl.sorts = marked.map((m) => m.s);
3945
- }
3946
- /**
3947
- * The source selector a column DECLARED via `sort.selector`, if any — what the
3948
- * DataSource / array source orders by in place of the field. Resolved like
3949
- * `declaredSortIndex`: central `props.th` config first, then the element's own
3950
- * `:sort.prop`. A blank string counts as not declared.
3951
- */
3952
- function declaredSortSelector(field) {
3953
- const sel = declaredSortConfig(field).selector;
3954
- return typeof sel === "string" && sel.trim() ? sel.trim() : void 0;
3955
- }
3956
- /**
3957
- * A column's declared sort OBJECT — the element's `:sort.prop` with central
3958
- * `props.th` config merged over it (props win key by key, as everywhere). `{}`
3959
- * for `sort: true` / no config.
3960
- */
3961
- function declaredSortConfig(field) {
3962
- const asObj = (sort) => typeof sort === "object" && sort ? sort : {};
3963
- let fromEl = {};
3964
- for (const el of columnEls) if (el.field === field && typeof el.sort === "object" && el.sort) {
3965
- fromEl = asObj(el.sort);
3966
- break;
3967
- }
3968
- return {
3969
- ...fromEl,
3970
- ...asObj((elementProps.th ?? []).find((c) => c?.field === field)?.sort)
3971
- };
3972
- }
3973
- /**
3974
- * Whether a sorted column is sorted IN THE BROWSER — its selector fans out over a
3975
- * collection (`[*]`, which no source can order by) or it declares an `aggregate`.
3976
- */
3977
- function isBrowserSortKey(field) {
3978
- const sel = declaredSortSelector(field);
3979
- return !!sel && parseFieldPath(sel).hasWildcard || !!declaredSortConfig(field).aggregate;
3980
- }
3981
- /** Keep the single-value mirrors pointing at the primary key. */
3982
- function syncPrimarySort() {
3983
- ctrl.sortField = ctrl.sorts[0]?.field ?? null;
3984
- ctrl.sortOrder = ctrl.sorts[0]?.order ?? null;
3985
- }
3986
- function clearSort() {
3987
- return setSort(null);
3988
- }
3989
- async function setSort(field, order, options) {
3990
- if (!field) {
3991
- ctrl.sorts = [];
3992
- sortCombineMode = false;
3993
- } else {
3994
- const multi = options?.multi === true;
3995
- const current = sortOf(field);
3996
- const next = order !== void 0 ? order : current === "asc" ? "desc" : current === "desc" ? null : "asc";
3997
- if (!next) ctrl.sorts = multi ? ctrl.sorts.filter((s) => s.field !== field) : [];
3998
- else if (multi) {
3999
- const at = ctrl.sorts.findIndex((s) => s.field === field);
4000
- ctrl.sorts = at >= 0 ? ctrl.sorts.map((s) => s.field === field ? {
4001
- field,
4002
- order: next
4003
- } : s) : [...ctrl.sorts, {
4004
- field,
4005
- order: next
4006
- }];
4007
- } else ctrl.sorts = [{
4008
- field,
4009
- order: next
4010
- }];
4011
- sortCombineMode = ctrl.sorts.length > 0 && multi && (options?.sticky === true || sortCombineMode);
4012
- }
4013
- applyDeclaredSortOrder();
4014
- syncPrimarySort();
4015
- notify();
4016
- groupPageIndex = 0;
4017
- if (serverMode()) {
4018
- await loadGroupsServer();
4019
- return;
4020
- }
4021
- const s = bound;
4022
- if (!s) return;
4023
- applySourceSort();
4024
- s.pageIndex?.(0);
4025
- await load();
4026
- }
4027
- async function setGroup(group) {
4028
- groupFields = toFields(group);
4029
- ctrl.grouped = groupFields.length > 0;
4030
- collapsed.clear();
4031
- groupRowPaging.clear();
4032
- groupPageIndex = 0;
4033
- if (serverMode()) {
4034
- clearRowCaches();
4035
- await loadGroupsServer();
4036
- return;
4037
- }
4038
- const s = bound;
4039
- if (!s) {
4040
- sync();
4041
- return;
4042
- }
4043
- applySourceSort();
4044
- s.pageIndex?.(0);
4045
- await load();
4046
- }
4047
- function toggleGroup(target) {
4048
- const path = typeof target === "string" ? target : target?.path;
4049
- if (!path) return;
4050
- if (collapsed.has(path)) collapsed.delete(path);
4051
- else collapsed.add(path);
4052
- sync();
4053
- if (serverMode()) fetchVisibleRows();
4054
- }
4055
- function isGroupCollapsed(path) {
4056
- return collapsed.has(path);
4057
- }
4058
- function expandAllGroups() {
4059
- if (!collapsed.size) return;
4060
- collapsed.clear();
4061
- sync();
4062
- if (serverMode()) fetchVisibleRows();
4063
- }
4064
- function collapseAllGroups() {
4065
- if (!groupFields.length) return;
4066
- if (serverMode()) {
4067
- for (const meta of serverGroups) collapsed.add(String(meta.key));
4068
- sync();
4069
- return;
4070
- }
4071
- const raw = bound ? [...bound.items?.() ?? []] : [];
4072
- const tree = buildGroups(flattenLeaves(raw), groupFields);
4073
- for (const path of collectGroupPaths(tree)) collapsed.add(path);
4074
- sync();
4075
- }
4076
- function setGroupPageSize(path, pageSize) {
4077
- if (!path) return;
4078
- const size = Math.max(1, Math.floor(pageSize) || 1);
4079
- const cur = groupRowPaging.get(path);
4080
- if (cur && cur.pageSize === size) return;
4081
- groupRowPaging.set(path, {
4082
- pageIndex: 0,
4083
- pageSize: size
4084
- });
4085
- if (serverMode()) {
4086
- groupRowsKey.delete(path);
4087
- sync();
4088
- fetchVisibleRows();
4089
- return;
4090
- }
4091
- sync();
4092
- }
4093
- function setGroupPage(path, pageIndex) {
4094
- if (!path) return;
4095
- const pageSize = groupRowPaging.get(path)?.pageSize ?? (defaultGroupRowPageSize || groupPageSize);
4096
- groupRowPaging.set(path, {
4097
- pageIndex: Math.max(0, pageIndex),
4098
- pageSize
4099
- });
4100
- if (serverMode()) {
4101
- groupRowsKey.delete(path);
4102
- sync();
4103
- fetchVisibleRows();
4104
- return;
4105
- }
4106
- sync();
4107
- }
4108
- function clearGroupPaging(path) {
4109
- if (groupRowPaging.delete(path)) {
4110
- if (serverMode()) {
4111
- groupRowsKey.delete(path);
4112
- sync();
4113
- fetchVisibleRows();
4114
- } else sync();
4115
- }
4116
- }
4117
- function groupPageInfo(path) {
4118
- if (pageSizeAll) {
4119
- const node = pathIndex.get(path);
4120
- const total = serverMode() ? serverGroups.find((g) => String(g.key) === path)?.count ?? 0 : node?.items.length ?? 0;
4121
- return {
4122
- pageIndex: 0,
4123
- pageCount: 1,
4124
- pageSize: Math.max(1, total),
4125
- total
4126
- };
4127
- }
4128
- if (serverMode()) {
4129
- const total = serverGroups.find((g) => String(g.key) === path)?.count ?? 0;
4130
- const pg = groupRowPagingFor(path);
4131
- const pageSize = Math.max(1, pg?.pageSize ?? (defaultGroupRowPageSize || total || 1));
4132
- const pageCount = Math.max(1, Math.ceil(total / pageSize));
4133
- return {
4134
- pageIndex: Math.min(Math.max(0, pg?.pageIndex ?? 0), pageCount - 1),
4135
- pageCount,
4136
- pageSize,
4137
- total
4138
- };
4139
- }
4140
- const node = pathIndex.get(path);
4141
- const total = node ? node.items.length : 0;
4142
- const pg = groupRowPagingFor(path);
4143
- const pageSize = Math.max(1, pg?.pageSize ?? total ?? 1);
4144
- const pageCount = Math.max(1, Math.ceil(total / pageSize));
4145
- return {
4146
- pageIndex: Math.min(Math.max(0, pg?.pageIndex ?? 0), pageCount - 1),
4147
- pageCount,
4148
- pageSize,
4149
- total
4150
- };
4151
- }
4152
- function groupNode(path) {
4153
- return pathIndex.get(path);
4154
- }
4155
- /**
4156
- * Every row matching the current query, without disturbing what's on screen.
4157
- *
4158
- * The chunked read itself lives in `readAllRows` (shared with the report
4159
- * engine's `data` resolver), which also reconstructs the DataSource's active
4160
- * **search** — `searchValue` is a DataSource-level option the store knows
4161
- * nothing about, so reading the store directly would otherwise hand back rows
4162
- * the user filtered away.
4163
- *
4164
- * `suppressSync` wraps the call because an array-backed source has to be
4165
- * re-paged to be read: the detour fires `changed`, and subscribers must not
4166
- * see the intermediate full-list state.
4167
- */
4168
- async function getData() {
4169
- if (browserRows && browserSortActive()) return [...browserRows];
4170
- return readAllFromSource();
4171
- }
4172
- /** `getData`'s read — every matching row from the source, in its own order. */
4173
- async function readAllFromSource() {
4174
- const s = bound;
4175
- if (!s) return [];
4176
- const loadOptions = {};
4177
- if (resolvedOpts.expand !== void 0) loadOptions.expand = resolvedOpts.expand;
4178
- if (resolvedOpts.customQueryParams !== void 0) loadOptions.customQueryParams = resolvedOpts.customQueryParams;
4179
- suppressSync = true;
4180
- try {
4181
- return await readAllRows(s, {
4182
- chunkSize,
4183
- select: opts.select ?? toList(resolvedOpts.select),
4184
- sort: mergedSort(),
4185
- ...Object.keys(loadOptions).length ? { loadOptions } : {}
4186
- });
4187
- } finally {
4188
- suppressSync = false;
4189
- }
4190
- }
4191
- /**
4192
- * Group `rows` (or everything {@link getData} returns) by the grid's own
4193
- * group fields. Named `buildGroupTree` internally so it doesn't shadow the
4194
- * imported `buildGroups` helper that `syncGrouped` and friends rely on.
4195
- */
4196
- async function buildGroupTree(rows) {
4197
- if (!groupFields.length) return [];
4198
- const source = rows ?? await getData();
4199
- return buildGroups(source, groupFields);
4200
- }
4201
- async function importSheet(options) {
4202
- const { importTable } = await import("./import-Dc4eXC02.js");
4203
- return importTable(options, {
4204
- getData,
4205
- rowKeyOf,
4206
- serverKeyOf: (row) => row?.[keyExpr] ?? rowKeyOf(row, -1),
4207
- stageImported: (changes) => {
4208
- for (const change of changes) {
4209
- const patch = pending.get(change.rowKey) ?? {};
4210
- Object.assign(patch, change.patch);
4211
- pending.set(change.rowKey, patch);
4212
- markImported(change.rowKey, Object.keys(change.patch));
4213
- }
4214
- notify();
4215
- }
4216
- });
4217
- }
4218
- async function exportReport(options) {
4219
- const { exportTable } = await import("./export-ixRdCS2y.js");
4220
- return exportTable(options);
4221
- }
4222
- function subscribe(cb) {
4223
- return notifier.subscribe(cb);
4224
- }
4225
- function dispose() {
4226
- detach();
4227
- removeOutsideExit();
4228
- unbindRowTrigger();
4229
- clearRowCaches();
4230
- formOps.length = 0;
4231
- formHandle = null;
4232
- ctrl.filterBuilder?.dispose();
4233
- notifier.clear();
4234
- }
4235
- if (ds) bind(ds);
4236
- return ctrl;
4237
- }
4238
-
4239
- //#endregion
4240
- export { seedFromRanges as a, minimize as i, buildDateTree as n, setAllChecked as o, checkState as r, setChecked as s, monoDataGrid as t };