@dou-ui/pivot 0.0.0-stage → 0.1.0-alpha.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/assets/default-PXde2wzm.js.map +1 -0
  4. package/dist/core/format.js +116 -0
  5. package/dist/core/format.js.map +1 -0
  6. package/dist/core/labels.js +28 -0
  7. package/dist/core/labels.js.map +1 -0
  8. package/dist/core/layout.js +190 -0
  9. package/dist/core/layout.js.map +1 -0
  10. package/dist/core/source.js +46 -0
  11. package/dist/core/source.js.map +1 -0
  12. package/dist/core/table.js +154 -0
  13. package/dist/core/table.js.map +1 -0
  14. package/dist/engine/aggregate.js +51 -0
  15. package/dist/engine/aggregate.js.map +1 -0
  16. package/dist/engine/compute.js +735 -0
  17. package/dist/engine/compute.js.map +1 -0
  18. package/dist/engine/engine.js +46 -0
  19. package/dist/engine/engine.js.map +1 -0
  20. package/dist/engine/formula.js +225 -0
  21. package/dist/engine/formula.js.map +1 -0
  22. package/dist/engine/group.js +107 -0
  23. package/dist/engine/group.js.map +1 -0
  24. package/dist/engine/hash.js +108 -0
  25. package/dist/engine/hash.js.map +1 -0
  26. package/dist/engine/index.js +8 -0
  27. package/dist/engine/show.js +132 -0
  28. package/dist/engine/show.js.map +1 -0
  29. package/dist/engine/source.js +339 -0
  30. package/dist/engine/source.js.map +1 -0
  31. package/dist/engine/tree.js +226 -0
  32. package/dist/engine/tree.js.map +1 -0
  33. package/dist/fields/fields.js +6 -0
  34. package/dist/fields/fields.js.map +1 -0
  35. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js +881 -0
  36. package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/fields/filter-list.js +6 -0
  38. package/dist/fields/filter-list.js.map +1 -0
  39. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js +184 -0
  40. package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
  41. package/dist/fields/formula-dialog.js +6 -0
  42. package/dist/fields/formula-dialog.js.map +1 -0
  43. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js +165 -0
  44. package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  45. package/dist/fields/model.js +200 -0
  46. package/dist/fields/model.js.map +1 -0
  47. package/dist/fields/top-dialog.js +6 -0
  48. package/dist/fields/top-dialog.js.map +1 -0
  49. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js +174 -0
  50. package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
  51. package/dist/index.js +16 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/locale.js +508 -0
  54. package/dist/locale.js.map +1 -0
  55. package/dist/pivot.js +6 -0
  56. package/dist/pivot.js.map +1 -0
  57. package/dist/pivot.vue_vue_type_script_setup_true_lang.js +1711 -0
  58. package/dist/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
  59. package/dist/style.css +1 -0
  60. package/dist/style.d.ts +1 -0
  61. package/dist/style.js +13 -0
  62. package/dist/types/core/format.d.ts +26 -0
  63. package/dist/types/core/labels.d.ts +14 -0
  64. package/dist/types/core/layout.d.ts +72 -0
  65. package/dist/types/core/source.d.ts +7 -0
  66. package/dist/types/core/table.d.ts +73 -0
  67. package/dist/types/engine/aggregate.d.ts +35 -0
  68. package/dist/types/engine/compute.d.ts +173 -0
  69. package/dist/types/engine/engine.d.ts +29 -0
  70. package/dist/types/engine/formula.d.ts +44 -0
  71. package/dist/types/engine/group.d.ts +34 -0
  72. package/dist/types/engine/hash.d.ts +36 -0
  73. package/dist/types/engine/index.d.ts +15 -0
  74. package/dist/types/engine/show.d.ts +27 -0
  75. package/dist/types/engine/source.d.ts +108 -0
  76. package/dist/types/engine/tree.d.ts +87 -0
  77. package/dist/types/engine/types.d.ts +148 -0
  78. package/dist/types/fields/fields.vue.d.ts +26 -0
  79. package/dist/types/fields/filter-list.vue.d.ts +20 -0
  80. package/dist/types/fields/formula-dialog.vue.d.ts +30 -0
  81. package/dist/types/fields/model.d.ts +52 -0
  82. package/dist/types/fields/top-dialog.vue.d.ts +25 -0
  83. package/dist/types/index.d.ts +458 -0
  84. package/dist/types/locale.d.ts +184 -0
  85. package/dist/types/pivot.vue.d.ts +29 -0
  86. package/dist/types/types.d.ts +199 -0
  87. package/dist/types/worker/client.d.ts +19 -0
  88. package/dist/types/worker/default.d.ts +1 -0
  89. package/dist/types/worker/index.d.ts +5 -0
  90. package/dist/types/worker/protocol.d.ts +60 -0
  91. package/dist/types/worker/serve.d.ts +22 -0
  92. package/dist/worker/client.js +191 -0
  93. package/dist/worker/client.js.map +1 -0
  94. package/dist/worker/default.js +20 -0
  95. package/dist/worker/default.js.map +1 -0
  96. package/dist/worker/index.js +3 -0
  97. package/dist/worker/serve.js +101 -0
  98. package/dist/worker/serve.js.map +1 -0
  99. package/package.json +64 -4
@@ -0,0 +1,1711 @@
1
+ import { BUILTIN_AGGREGATORS, FAST_AGGREGATES } from "./engine/aggregate.js";
2
+ import { PivotSource } from "./engine/source.js";
3
+ import { PERCENT_SHOWS, computePivot, markOf, showOf } from "./engine/compute.js";
4
+ import { collapsedNodes, columnLines, groupPaths, headerSpans, rowLines, samePath, spansIn, totalLines } from "./core/layout.js";
5
+ import { labelFormatter, valueFormatter, valueNumberOptions } from "./core/format.js";
6
+ import { format, usePivotMessages } from "./locale.js";
7
+ import { valueLabel } from "./core/labels.js";
8
+ import { usePivotSource } from "./core/source.js";
9
+ import { SHEET_STYLES, gridMerges, gridToCSV, gridToHTML, gridToSheetCells, gridToTSV, sheetFormat } from "./core/table.js";
10
+ import { useElementSize, useLocale } from "dou-ui";
11
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, isMemoSame, markRaw, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderList, shallowRef, toDisplayString, toRaw, unref, useId, useModel, watch } from "vue";
12
+ import { ArrowDownIcon, ArrowUpIcon, ChevronRightIcon } from "@dou-ui/icons";
13
+ // src/pivot.vue?vue&type=script&setup=true&lang.ts
14
+ var _hoisted_1 = [
15
+ "aria-label",
16
+ "aria-rowcount",
17
+ "aria-colcount",
18
+ "aria-activedescendant",
19
+ "aria-busy"
20
+ ];
21
+ var _hoisted_2 = { class: "dou-pivot__corner-label" };
22
+ var _hoisted_3 = {
23
+ key: 0,
24
+ class: "dou-pivot__corner-sep",
25
+ "aria-hidden": "true"
26
+ };
27
+ var _hoisted_4 = ["data-sort-field", "aria-label"];
28
+ var _hoisted_5 = {
29
+ key: 0,
30
+ class: "dou-pivot__corner-values"
31
+ };
32
+ var _hoisted_6 = {
33
+ key: 1,
34
+ class: "dou-pivot__corner-values"
35
+ };
36
+ var _hoisted_7 = [
37
+ "aria-label",
38
+ "aria-valuenow",
39
+ "aria-valuemin",
40
+ "aria-valuemax",
41
+ "tabindex",
42
+ "title"
43
+ ];
44
+ var _hoisted_8 = [
45
+ "aria-sort",
46
+ "aria-expanded",
47
+ "data-head",
48
+ "title"
49
+ ];
50
+ var _hoisted_9 = { class: "dou-pivot__col-label" };
51
+ var _hoisted_10 = ["aria-label"];
52
+ var _hoisted_11 = { class: "dou-pivot__text" };
53
+ var _hoisted_12 = [
54
+ "data-resize",
55
+ "aria-label",
56
+ "aria-valuenow",
57
+ "tabindex",
58
+ "title"
59
+ ];
60
+ var _hoisted_13 = [
61
+ "id",
62
+ "aria-selected",
63
+ "data-row"
64
+ ];
65
+ var _hoisted_14 = ["data-toggle-node"];
66
+ var _hoisted_15 = {
67
+ key: 1,
68
+ class: "dou-pivot__toggle-space"
69
+ };
70
+ var _hoisted_16 = { class: "dou-pivot__text" };
71
+ var _hoisted_17 = {
72
+ key: 1,
73
+ class: "dou-pivot__toggle-space"
74
+ };
75
+ var _hoisted_18 = { class: "dou-pivot__text" };
76
+ var _hoisted_19 = { class: "dou-pivot__text" };
77
+ var _hoisted_20 = [
78
+ "id",
79
+ "aria-selected",
80
+ "aria-colindex",
81
+ "data-row",
82
+ "data-col"
83
+ ];
84
+ var _hoisted_21 = { class: "dou-pivot__text" };
85
+ var _hoisted_22 = ["aria-rowindex"];
86
+ var _hoisted_23 = [
87
+ "id",
88
+ "aria-selected",
89
+ "data-row"
90
+ ];
91
+ var _hoisted_24 = {
92
+ key: 0,
93
+ class: "dou-pivot__text"
94
+ };
95
+ var _hoisted_25 = { class: "dou-pivot__text" };
96
+ var _hoisted_26 = [
97
+ "id",
98
+ "aria-selected",
99
+ "aria-colindex",
100
+ "data-row",
101
+ "data-col"
102
+ ];
103
+ var _hoisted_27 = { class: "dou-pivot__text" };
104
+ var _hoisted_28 = {
105
+ key: 0,
106
+ class: "dou-pivot__empty",
107
+ role: "status"
108
+ };
109
+ var _hoisted_29 = {
110
+ class: "dou-pivot__live",
111
+ role: "status",
112
+ "aria-live": "polite"
113
+ };
114
+ var _hoisted_30 = {
115
+ key: 0,
116
+ class: "dou-pivot__veil",
117
+ role: "status"
118
+ };
119
+ var _hoisted_31 = { class: "dou-pivot__busy" };
120
+ var VALUE_HEADER_WIDTH = 120;
121
+ var WORKER_FROM = 1e5;
122
+ /** How long a worker may take before the table says it is busy, in ms. */
123
+ var BUSY_AFTER = 120;
124
+ /** Set when the table's own worker cannot start (under a strict CSP, say); it then sums up here. */
125
+ var OVERSCAN_ROWS = 6;
126
+ var OVERSCAN_COLS = 2;
127
+ /** What a header cell says: a group, a subtotal, the grand total or a value. */
128
+ var MIN_COLUMN = 56;
129
+ var MAX_COLUMN = 640;
130
+ /** A row's labels, one per row field: the group on its own line, as the table shows it. */
131
+ var pivot_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
132
+ name: "DouPivot",
133
+ __name: "pivot",
134
+ props: /*@__PURE__*/ mergeModels({
135
+ data: {},
136
+ fields: {},
137
+ config: {},
138
+ collapsed: {},
139
+ maxHeight: { default: 480 },
140
+ size: { default: "md" },
141
+ rowHeaderWidth: { default: 200 },
142
+ columnWidth: { default: 120 },
143
+ layout: { default: "compact" },
144
+ resizable: {
145
+ type: Boolean,
146
+ default: true
147
+ },
148
+ aggregators: {},
149
+ worker: {
150
+ type: [Boolean, String],
151
+ default: "auto"
152
+ },
153
+ engine: {},
154
+ highlight: {},
155
+ ariaLabel: {},
156
+ messages: {}
157
+ }, {
158
+ "config": { default: () => ({}) },
159
+ "configModifiers": {},
160
+ "collapsed": { default: () => ({}) },
161
+ "collapsedModifiers": {}
162
+ }),
163
+ emits: /*@__PURE__*/ mergeModels(["cellClick"], ["update:config", "update:collapsed"]),
164
+ setup(__props, { expose: __expose, emit: __emit }) {
165
+ const props = __props;
166
+ const config = useModel(__props, "config");
167
+ const collapsed = useModel(__props, "collapsed");
168
+ const emit = __emit;
169
+ const { locale } = useLocale();
170
+ const messages = usePivotMessages(() => props.messages);
171
+ const uid = useId();
172
+ const ROW_HEIGHT = {
173
+ sm: 28,
174
+ md: 32,
175
+ lg: 40
176
+ };
177
+ const source = usePivotSource(() => props.data, () => props.fields);
178
+ /** Stays the same object while the aggregators do, even when the array is written inline. */
179
+ let lastAggregators = {};
180
+ const aggregatorMap = computed(() => {
181
+ const list = props.aggregators ?? [];
182
+ const last = Object.values(lastAggregators);
183
+ if (list.length === last.length && list.every((a) => lastAggregators[a.name] === toRaw(a))) return lastAggregators;
184
+ const map = {};
185
+ for (const aggregator of list) map[aggregator.name] = toRaw(aggregator);
186
+ return lastAggregators = map;
187
+ });
188
+ const keyReplacer = (_key, value) => typeof value === "function" ? void 0 : value;
189
+ const dimensionKey = (d) => typeof d === "string" ? { field: d } : {
190
+ field: d.field,
191
+ sort: d.sort,
192
+ subtotal: d.subtotal,
193
+ by: d.by
194
+ };
195
+ /**
196
+ * The part of the config that changes what is summed up. Sorting, totals,
197
+ * labels and formats leave it as it is, so they never sum up again.
198
+ */
199
+ let engineCache = null;
200
+ const engineConfig = computed(() => {
201
+ const c = config.value;
202
+ const custom = aggregatorMap.value;
203
+ const values = (c.values ?? []).filter((v) => {
204
+ const name = typeof v === "string" ? void 0 : v.aggregate;
205
+ if (!name || FAST_AGGREGATES.has(name) || BUILTIN_AGGREGATORS[name] || custom[name]) return true;
206
+ return false;
207
+ });
208
+ const next = {
209
+ rows: (c.rows ?? []).map(dimensionKey),
210
+ columns: (c.columns ?? []).map(dimensionKey),
211
+ values: values.map((v) => typeof v === "string" ? { field: v } : v.formula !== void 0 ? { formula: v.formula } : {
212
+ field: v.field,
213
+ aggregate: v.aggregate
214
+ }),
215
+ filters: c.filters
216
+ };
217
+ const key = JSON.stringify(next, keyReplacer);
218
+ if (engineCache?.key === key) return engineCache.config;
219
+ engineCache = {
220
+ key,
221
+ config: next
222
+ };
223
+ return next;
224
+ });
225
+ /** Records from which `'auto'` sums up in a worker. */
226
+ const ownFailed = ref(false);
227
+ const away = computed(() => !!props.engine || !ownFailed.value && (props.worker === true || props.worker === "auto" && source.value.length > WORKER_FROM && typeof Worker !== "undefined"));
228
+ /** The last result summed up here, shown while a worker sums up the first time. */
229
+ let lastHere = null;
230
+ const here = computed(() => {
231
+ if (away.value) return null;
232
+ const next = computePivot(source.value, engineConfig.value, { aggregators: aggregatorMap.value });
233
+ return lastHere = markRaw(next);
234
+ });
235
+ /** What a worker last sent, and whether a newer one is on its way. */
236
+ const fromAway = shallowRef(null);
237
+ const busy = ref(false);
238
+ /** Groups with no records yet: the shape of the table while the first result is on its way. */
239
+ const shapeOnly = computed(() => markRaw(computePivot(new PivotSource([], { fields: [...source.value.fields.values()] }), {})));
240
+ const result = computed(() => here.value ?? fromAway.value ?? shapeOnly.value);
241
+ let ownEngine = null;
242
+ async function startOwnEngine() {
243
+ const [{ createPivotWorker }, { default: PivotWorker }] = await Promise.all([import("./worker/client.js"), import("./worker/default.js")]);
244
+ return createPivotWorker(() => new PivotWorker());
245
+ }
246
+ let ownEngineReady = null;
247
+ const engineFor = () => props.engine ? Promise.resolve(props.engine) : ownEngineReady ??= startOwnEngine().then((engine) => ownEngine = engine);
248
+ let job = 0;
249
+ let stop = null;
250
+ watch([
251
+ away,
252
+ source,
253
+ engineConfig,
254
+ () => props.engine
255
+ ], async ([isAway, src, cfg]) => {
256
+ stop?.abort();
257
+ stop = null;
258
+ const id = ++job;
259
+ if (!isAway) {
260
+ busy.value = false;
261
+ fromAway.value = null;
262
+ return;
263
+ }
264
+ if (!fromAway.value && lastHere) fromAway.value = lastHere;
265
+ lastHere = null;
266
+ const controller = stop = new AbortController();
267
+ const slow = setTimeout(() => id === job && (busy.value = true), BUSY_AFTER);
268
+ try {
269
+ const next = await (await engineFor()).compute(src, cfg, {
270
+ aggregators: aggregatorMap.value,
271
+ signal: controller.signal
272
+ });
273
+ if (id !== job) return;
274
+ fromAway.value = markRaw(next);
275
+ } catch (error) {
276
+ if (id !== job || error.name === "AbortError") return;
277
+ if (props.engine) console.error("[@dou-ui/pivot] the engine passed to DouPivot failed in compute(); the table keeps the last result", error);
278
+ else {
279
+ console.warn("[@dou-ui/pivot] the worker is not available, so the table sums up on the page from now on", error);
280
+ ownEngine?.dispose?.();
281
+ ownEngine = null;
282
+ ownEngineReady = null;
283
+ ownFailed.value = true;
284
+ }
285
+ } finally {
286
+ clearTimeout(slow);
287
+ if (id === job) busy.value = false;
288
+ }
289
+ }, { immediate: true });
290
+ onBeforeUnmount(() => {
291
+ stop?.abort();
292
+ job++;
293
+ ownEngine?.dispose?.();
294
+ });
295
+ const valueCount = computed(() => result.value.values.length);
296
+ const valuesOnRows = computed(() => config.value.valuesOn === "rows" && valueCount.value > 1);
297
+ const showRowTotal = computed(() => config.value.totals?.rows !== false);
298
+ const showColumnTotal = computed(() => config.value.totals?.columns !== false);
299
+ const rowCollapsed = computed(() => collapsedNodes(result.value.rows, collapsed.value.rows));
300
+ const columnCollapsed = computed(() => collapsedNodes(result.value.columns, collapsed.value.columns));
301
+ const rowOrder = computed(() => result.value.rowOrder(config.value.sort));
302
+ /** Each row field in a column of its own, open groups' totals on a line after their children. */
303
+ const tabular = computed(() => props.layout === "tabular" && result.value.rows.levels.length > 0);
304
+ const bodyLines = computed(() => rowLines(result.value.rows, rowOrder.value, rowCollapsed.value, {
305
+ values: valueCount.value,
306
+ valuesHere: valuesOnRows.value,
307
+ total: showRowTotal.value,
308
+ totalsAfter: tabular.value
309
+ }));
310
+ const footLines = computed(() => totalLines(result.value.rows, {
311
+ values: valueCount.value,
312
+ valuesHere: valuesOnRows.value,
313
+ total: showRowTotal.value
314
+ }));
315
+ const colLines = computed(() => columnLines(result.value.columns, columnCollapsed.value, {
316
+ values: valueCount.value,
317
+ valuesHere: !valuesOnRows.value,
318
+ total: showColumnTotal.value
319
+ }));
320
+ /**
321
+ * The row naming each column's value: left out when the values run along the
322
+ * rows, and when one value sits under column groups (each would repeat it).
323
+ */
324
+ const valueRow = computed(() => result.value.columns.levels.length === 0 || !valuesOnRows.value && valueCount.value > 1);
325
+ const spans = computed(() => headerSpans(result.value.columns, colLines.value, valueRow.value));
326
+ /** How each value is headed, shown, marked and written: the config's, on what was summed up. */
327
+ const valueDisplay = computed(() => {
328
+ const m = messages.value;
329
+ return result.value.values.map((value, i) => {
330
+ const given = config.value.values?.[i];
331
+ const spec = typeof given === "object" ? given : void 0;
332
+ const show = showOf(spec?.show);
333
+ const own = show && (PERCENT_SHOWS.has(show.as) || show.as === "rank");
334
+ const shownValue = {
335
+ ...value,
336
+ show,
337
+ format: spec?.format ?? (own ? void 0 : value.format)
338
+ };
339
+ const options = valueNumberOptions(shownValue);
340
+ return {
341
+ label: valueLabel(shownValue, spec?.label, m),
342
+ write: valueFormatter(shownValue, locale.value.name),
343
+ /** The same format for a sheet, when there is one. */
344
+ sheetFormat: options ? sheetFormat(options, locale.value.name) : void 0,
345
+ show: show ?? null,
346
+ mark: markOf(spec?.mark)
347
+ };
348
+ });
349
+ });
350
+ const labelWriters = (tree) => {
351
+ const m = messages.value;
352
+ const texts = {
353
+ empty: m.empty,
354
+ yes: m.yes,
355
+ no: m.no,
356
+ year: m.yearLabel,
357
+ quarter: m.quarterLabel,
358
+ week: m.weekLabel,
359
+ range: m.rangeLabel
360
+ };
361
+ return tree.levels.map((level) => labelFormatter(level.field, locale.value.name, texts, level.by));
362
+ };
363
+ const rowWriters = computed(() => labelWriters(result.value.rows));
364
+ const colWriters = computed(() => labelWriters(result.value.columns));
365
+ const nodeText = (tree, writers, node) => node <= 0 ? messages.value.total : writers[tree.depth[node] - 1](tree.label(node));
366
+ const rowHeight = computed(() => ROW_HEIGHT[props.size] ?? 32);
367
+ /** A row per column field, and one more for the row fields (and the values, when shown). */
368
+ const headRows = computed(() => result.value.columns.levels.length + 1);
369
+ const headHeight = computed(() => headRows.value * rowHeight.value);
370
+ /** Set by dragging the edge of the corner; `0` for the width worked out from the props. */
371
+ const rowHeadDragged = ref(0);
372
+ const rowLevelCount = computed(() => tabular.value ? result.value.rows.levels.length : 1);
373
+ /**
374
+ * The row headers' width. A tabular layout gives each row field a column of
375
+ * 60% of `rowHeaderWidth`. On a narrow screen the row headers give up some
376
+ * width so the numbers still show.
377
+ */
378
+ const rowHeadWidth = computed(() => {
379
+ if (rowHeadDragged.value) return rowHeadDragged.value;
380
+ const want = tabular.value ? rowLevelCount.value * Math.round(props.rowHeaderWidth * .6) : props.rowHeaderWidth;
381
+ return Math.min(want, Math.max(120, Math.floor(viewWidth.value * (tabular.value ? .5 : .4))));
382
+ });
383
+ /** The width of each row field's column in the tabular layout. */
384
+ const levelWidth = computed(() => Math.floor(rowHeadWidth.value / rowLevelCount.value));
385
+ const headWidth = computed(() => rowHeadWidth.value + (valuesOnRows.value ? VALUE_HEADER_WIDTH : 0));
386
+ /** Columns widen to fill a wide table, up to twice their width; past that the bands run on alone. */
387
+ const colWidth = computed(() => {
388
+ const count = colLines.value.length;
389
+ const fill = count ? Math.floor((viewWidth.value - headWidth.value) / count) : 0;
390
+ return Math.max(props.columnWidth, Math.min(fill, props.columnWidth * 2));
391
+ });
392
+ /** Widths set by dragging a column's edge, by the column's groups, kind and value. */
393
+ const dragged = shallowRef(/* @__PURE__ */ new Map());
394
+ const colKey = (line) => `${line.kind}|${line.value}|${line.kind === "total" ? "" : JSON.stringify(result.value.columns.path(line.node))}`;
395
+ /** Where each column starts; the last entry is where the last one ends. */
396
+ const colStarts = computed(() => {
397
+ const lines = colLines.value;
398
+ const starts = new Float64Array(lines.length + 1);
399
+ const base = colWidth.value;
400
+ const map = dragged.value;
401
+ for (let i = 0; i < lines.length; i++) starts[i + 1] = starts[i] + (map.size ? map.get(colKey(lines[i])) ?? base : base);
402
+ return starts;
403
+ });
404
+ /** The column at `x` px from the left of the columns. */
405
+ function columnAt(x) {
406
+ const starts = colStarts.value;
407
+ let low = 0;
408
+ let high = starts.length - 2;
409
+ while (low < high) {
410
+ const mid = low + high + 1 >> 1;
411
+ if (starts[mid] <= x) low = mid;
412
+ else high = mid - 1;
413
+ }
414
+ return Math.max(0, low);
415
+ }
416
+ const columnsWidth = computed(() => colStarts.value[colLines.value.length]);
417
+ const totalWidth = computed(() => headWidth.value + columnsWidth.value);
418
+ const bodyHeight = computed(() => bodyLines.value.length * rowHeight.value);
419
+ const footHeight = computed(() => footLines.value.length * rowHeight.value);
420
+ const root = ref();
421
+ const viewport = ref();
422
+ const { width: viewWidth, height: viewHeight } = useElementSize(viewport, { initial: {
423
+ width: 960,
424
+ height: 480
425
+ } });
426
+ const scroll = shallowRef({
427
+ top: 0,
428
+ left: 0
429
+ });
430
+ /** The height of the horizontal scrollbar, when there is one that takes room. */
431
+ const gutter = ref(0);
432
+ watch([
433
+ viewWidth,
434
+ viewHeight,
435
+ () => totalWidth.value
436
+ ], () => nextTick(() => {
437
+ const el = viewport.value;
438
+ if (el) gutter.value = el.offsetHeight - el.clientHeight;
439
+ }), { flush: "post" });
440
+ function onScroll() {
441
+ const el = viewport.value;
442
+ if (!el) return;
443
+ if (el.scrollTop !== scroll.value.top || el.scrollLeft !== scroll.value.left) scroll.value = {
444
+ top: el.scrollTop,
445
+ left: el.scrollLeft
446
+ };
447
+ }
448
+ const rowWindow = computed(() => {
449
+ const h = rowHeight.value;
450
+ const space = Math.max(h, viewHeight.value - headHeight.value - footHeight.value);
451
+ return {
452
+ from: Math.max(0, Math.floor(scroll.value.top / h) - OVERSCAN_ROWS),
453
+ to: Math.min(bodyLines.value.length, Math.ceil((scroll.value.top + space) / h) + OVERSCAN_ROWS)
454
+ };
455
+ });
456
+ const colWindow = computed(() => {
457
+ const space = Math.max(colWidth.value, viewWidth.value - headWidth.value);
458
+ const count = colLines.value.length;
459
+ const from = Math.max(0, columnAt(scroll.value.left) - OVERSCAN_COLS);
460
+ const to = count ? Math.min(count, columnAt(scroll.value.left + space) + 1 + OVERSCAN_COLS) : 0;
461
+ const last = lastColWindow;
462
+ if (last && last.from === from && last.to === to) return last;
463
+ return lastColWindow = {
464
+ from,
465
+ to
466
+ };
467
+ });
468
+ let lastColWindow = null;
469
+ const visibleRows = computed(() => {
470
+ const { from, to } = rowWindow.value;
471
+ const out = [];
472
+ for (let i = from; i < to; i++) out.push({
473
+ line: bodyLines.value[i],
474
+ index: i
475
+ });
476
+ return out;
477
+ });
478
+ const visibleFoot = computed(() => footLines.value.map((line, i) => ({
479
+ line,
480
+ index: bodyLines.value.length + i
481
+ })));
482
+ const visibleCols = computed(() => {
483
+ const { from, to } = colWindow.value;
484
+ return colLines.value.slice(from, to).map((line, i) => ({
485
+ line,
486
+ index: from + i
487
+ }));
488
+ });
489
+ const visibleSpans = computed(() => {
490
+ const { from, to } = colWindow.value;
491
+ return spans.value.map((list) => spansIn(list, from, to));
492
+ });
493
+ const rowTree = computed(() => result.value.rows);
494
+ /** Whether a group's own row is blank because its field has no subtotals and it is open. */
495
+ const rowBlank = (line) => {
496
+ if (line.kind !== "group") return false;
497
+ const tree = rowTree.value;
498
+ if (!tree.isGroup(line.node) || rowCollapsed.value.has(line.node)) return false;
499
+ return !tree.levels[tree.depth[line.node] - 1].subtotal;
500
+ };
501
+ const isFirstOfNode = (line) => line.value <= 0;
502
+ /** A group whose label shows on a line: in the tabular layout, one per row field. */
503
+ const lineToggles = (line) => (line.kind === "group" || line.kind === "subtotal" && !tabular.value) && rowTree.value.isGroup(line.node);
504
+ function rowHead(line, index) {
505
+ const tree = rowTree.value;
506
+ const group = lineToggles(line);
507
+ const head = {
508
+ text: line.kind === "total" ? messages.value.total : line.kind === "subtotal" ? format(messages.value.subtotalRow, { label: nodeText(tree, rowWriters.value, line.node) }) : nodeText(tree, rowWriters.value, line.node),
509
+ depth: line.kind === "total" ? 0 : tree.depth[line.node] - 1,
510
+ group,
511
+ open: group && !rowCollapsed.value.has(line.node),
512
+ valueText: line.value >= 0 ? valueDisplay.value[line.value].label : "",
513
+ firstOfNode: isFirstOfNode(line),
514
+ parts: []
515
+ };
516
+ if (!tabular.value || !head.firstOfNode) return head;
517
+ const parts = Array.from({ length: tree.levels.length }, () => null);
518
+ if (line.kind === "total" || line.kind === "subtotal") parts[head.depth] = {
519
+ text: head.text,
520
+ toggle: group ? line.node : -1,
521
+ open: head.open
522
+ };
523
+ else {
524
+ const before = index > 0 ? bodyLines.value[index - 1] : void 0;
525
+ for (let at = line.node; at > 0; at = tree.parent[at]) {
526
+ const level = tree.depth[at] - 1;
527
+ if (at !== line.node && before && before.kind !== "total" && tree.within(before.node, at)) break;
528
+ const isGroup = tree.isGroup(at);
529
+ parts[level] = {
530
+ text: nodeText(tree, rowWriters.value, at),
531
+ toggle: isGroup ? at : -1,
532
+ open: isGroup && !rowCollapsed.value.has(at)
533
+ };
534
+ }
535
+ }
536
+ head.parts = parts;
537
+ return head;
538
+ }
539
+ function markStyle(value, valueIndex) {
540
+ const { mark, show } = valueDisplay.value[valueIndex];
541
+ if (!mark) return "";
542
+ const { min, max } = result.value.range(valueIndex, show);
543
+ if (min !== min) return "";
544
+ const tone = `--dou-pivot-tone:var(--dou-color-${mark.tone})`;
545
+ if (mark.type === "scale") return `;${tone};--dou-pivot-mark:${(max === min ? 1 : (value - min) / (max - min)).toFixed(3)}`;
546
+ const lo = Math.min(0, min);
547
+ const span = Math.max(0, max) - lo || 1;
548
+ return `;${tone};--dou-pivot-bar-start:${((Math.min(value, 0) - lo) / span).toFixed(3)};--dou-pivot-bar-size:${(Math.abs(value) / span).toFixed(3)}`;
549
+ }
550
+ function rowCells(line, row) {
551
+ const res = result.value;
552
+ const sel = selection.value;
553
+ const picked = sel && row >= sel.top && row <= sel.bottom ? sel : null;
554
+ const blank = rowBlank(line);
555
+ const display = valueDisplay.value;
556
+ const starts = colStarts.value;
557
+ const innerRow = res.rows.depth[line.node] === res.rows.levels.length && !blank;
558
+ return visibleCols.value.map(({ line: col, index }) => {
559
+ const v = line.value >= 0 ? line.value : Math.max(0, col.value);
560
+ const n = blank ? NaN : res.shown(line.node, col.node, v, display[v].show);
561
+ let cls = "dou-pivot__cell";
562
+ if (col.kind !== "group") cls += " is-total";
563
+ if (col.kind === "group" && col.node > 0 && res.columns.isGroup(col.node)) cls += " is-closed";
564
+ const selected = !!picked && index >= picked.left && index <= picked.right;
565
+ if (selected) cls += " is-selected";
566
+ if (col.node === highlightCol.value) cls += " is-highlighted";
567
+ let style = `width:${starts[index + 1] - starts[index]}px`;
568
+ const mark = display[v].mark;
569
+ if (mark && innerRow && n === n && res.columns.depth[col.node] === res.columns.levels.length) {
570
+ cls += mark.type === "bar" ? " has-bar" : " has-scale";
571
+ style += markStyle(n, v);
572
+ }
573
+ return {
574
+ key: index,
575
+ col: index,
576
+ text: display[v].write(n),
577
+ cls,
578
+ style,
579
+ selected
580
+ };
581
+ });
582
+ }
583
+ function spanText(span) {
584
+ const tree = result.value.columns;
585
+ if (span.kind === "value") return span.value >= 0 ? valueDisplay.value[span.value].label : messages.value.total;
586
+ if (span.kind === "total") return messages.value.total;
587
+ if (span.kind === "subtotal") return format(messages.value.subtotal, { label: nodeText(tree, colWriters.value, span.node) });
588
+ return nodeText(tree, colWriters.value, span.node);
589
+ }
590
+ const columnPath = (line) => line.kind === "total" ? [] : result.value.columns.path(line.node);
591
+ const headCells = computed(() => {
592
+ const tree = result.value.columns;
593
+ const starts = colStarts.value;
594
+ const h = rowHeight.value;
595
+ const sort = config.value.sort;
596
+ const out = [];
597
+ const rows = headRows.value;
598
+ visibleSpans.value.forEach((list) => {
599
+ for (const span of list) {
600
+ const reachesBottom = span.level + span.rows === rows;
601
+ const single = span.to - span.from === 1;
602
+ const line = colLines.value[span.from];
603
+ const sortable = span.kind === "value" || reachesBottom && single && !valueRow.value;
604
+ const value = span.kind === "value" ? Math.max(0, span.value) : Math.max(0, line.value);
605
+ const sorted = sortable && sort && sort.valueIndex === value && samePath(sort.column ?? [], columnPath(line)) ? sort.order : null;
606
+ const canToggle = span.kind === "group" && tree.isGroup(span.node);
607
+ const text = spanText(span);
608
+ let cls = `dou-pivot__col-head is-${span.kind}`;
609
+ if (span.kind !== "group" || !canToggle && single) cls += " is-end";
610
+ if (span.kind === "value" && line.kind !== "group") cls += " is-total";
611
+ if (sorted) cls += " is-sorted";
612
+ out.push({
613
+ key: `${span.level}:${span.from}:${span.kind}`,
614
+ span,
615
+ text,
616
+ style: `left:${starts[span.from]}px;top:${span.level * h}px;width:${starts[span.to] - starts[span.from]}px;height:${span.rows * h}px`,
617
+ resize: reachesBottom && single && props.resizable ? span.from : -1,
618
+ toggle: canToggle,
619
+ open: canToggle && !columnCollapsed.value.has(span.node),
620
+ sortable,
621
+ sorted,
622
+ cls
623
+ });
624
+ }
625
+ });
626
+ return out;
627
+ });
628
+ const rowFieldNames = computed(() => rowFields.value.map((f) => f.label).join(" / "));
629
+ const rowFields = computed(() => result.value.rows.levels.map((level, i) => ({
630
+ index: i,
631
+ label: level.field.label,
632
+ sort: level.sort
633
+ })));
634
+ const columnFields = computed(() => result.value.columns.levels.map((level) => level.field.label));
635
+ function sortByColumn(span) {
636
+ const line = colLines.value[span.from];
637
+ const value = span.kind === "value" ? Math.max(0, span.value) : Math.max(0, line.value);
638
+ const column = columnPath(line);
639
+ const current = config.value.sort;
640
+ const order = !(current && current.valueIndex === value && samePath(current.column ?? [], column)) ? "desc" : current.order === "desc" ? "asc" : null;
641
+ config.value = {
642
+ ...config.value,
643
+ sort: order ? {
644
+ valueIndex: value,
645
+ column,
646
+ order
647
+ } : null
648
+ };
649
+ }
650
+ function sortByField(index) {
651
+ const rows = (config.value.rows ?? []).map((d, i) => {
652
+ const spec = typeof d === "string" ? { field: d } : { ...d };
653
+ if (i !== index) return d;
654
+ spec.sort = !!config.value.sort ? spec.sort ?? "asc" : spec.sort === "desc" ? "asc" : "desc";
655
+ return spec;
656
+ });
657
+ config.value = {
658
+ ...config.value,
659
+ rows,
660
+ sort: null
661
+ };
662
+ }
663
+ function setCollapsed(axis, paths) {
664
+ collapsed.value = {
665
+ ...collapsed.value,
666
+ [axis]: paths
667
+ };
668
+ }
669
+ function toggle(axis, path, open) {
670
+ const list = collapsed.value[axis] ?? [];
671
+ const at = list.findIndex((p) => samePath(p, path));
672
+ const isOpen = at < 0;
673
+ const next = open ?? !isOpen;
674
+ if (next === isOpen) return;
675
+ setCollapsed(axis, next ? list.filter((_, i) => i !== at) : [...list, path]);
676
+ }
677
+ function toggleNode(axis, node, all = false) {
678
+ const tree = axis === "rows" ? result.value.rows : result.value.columns;
679
+ const open = (axis === "rows" ? rowCollapsed.value : columnCollapsed.value).has(node);
680
+ const label = nodeText(tree, axis === "rows" ? rowWriters.value : colWriters.value, node);
681
+ announce(format(open ? messages.value.expanded : messages.value.collapsed, { label }));
682
+ if (!all) return toggle(axis, tree.path(node), open);
683
+ const depth = tree.depth[node];
684
+ const same = [];
685
+ for (let at = 1; at < tree.size; at++) if (tree.depth[at] === depth && tree.isGroup(at)) same.push(tree.path(at));
686
+ const rest = (collapsed.value[axis] ?? []).filter((p) => !same.some((q) => samePath(p, q)));
687
+ setCollapsed(axis, open ? rest : [...rest, ...same]);
688
+ }
689
+ function expandAll({ axis } = {}) {
690
+ const next = { ...collapsed.value };
691
+ for (const a of axis ? [axis] : ["rows", "columns"]) next[a] = [];
692
+ collapsed.value = next;
693
+ }
694
+ function collapseAll({ axis, depth = 1 } = {}) {
695
+ const next = { ...collapsed.value };
696
+ for (const a of axis ? [axis] : ["rows", "columns"]) next[a] = groupPaths(a === "rows" ? result.value.rows : result.value.columns, depth);
697
+ collapsed.value = next;
698
+ }
699
+ let justResized = false;
700
+ const widthOf = (col) => col < 0 ? rowHeadWidth.value : colStarts.value[col + 1] - colStarts.value[col];
701
+ const minWidthOf = (col) => col < 0 ? 80 * rowLevelCount.value : MIN_COLUMN;
702
+ const maxWidthOf = (col) => col < 0 ? 480 * rowLevelCount.value : MAX_COLUMN;
703
+ function setWidth(col, width) {
704
+ const w = Math.round(Math.min(Math.max(width, minWidthOf(col)), maxWidthOf(col)));
705
+ if (col < 0) {
706
+ rowHeadDragged.value = w;
707
+ return;
708
+ }
709
+ const next = new Map(dragged.value);
710
+ next.set(colKey(colLines.value[col]), w);
711
+ dragged.value = next;
712
+ }
713
+ /** Dragging the edge of a column header (or of the corner, for the row headers). */
714
+ function onResizeDown(event) {
715
+ const handle = event.target.closest("[data-resize]");
716
+ if (!handle || event.button !== 0) return;
717
+ event.preventDefault();
718
+ event.stopPropagation();
719
+ const col = Number(handle.dataset.resize);
720
+ const startX = event.clientX;
721
+ const from = widthOf(col);
722
+ let moved = false;
723
+ handle.setPointerCapture?.(event.pointerId);
724
+ const onMove = (e) => {
725
+ const dx = e.clientX - startX;
726
+ if (!moved && Math.abs(dx) < 2) return;
727
+ moved = true;
728
+ setWidth(col, from + dx);
729
+ };
730
+ const onUp = () => {
731
+ window.removeEventListener("pointermove", onMove);
732
+ window.removeEventListener("pointerup", onUp);
733
+ window.removeEventListener("pointercancel", onUp);
734
+ if (moved) {
735
+ justResized = true;
736
+ setTimeout(() => justResized = false);
737
+ }
738
+ };
739
+ window.addEventListener("pointermove", onMove);
740
+ window.addEventListener("pointerup", onUp);
741
+ window.addEventListener("pointercancel", onUp);
742
+ }
743
+ const measurer = ref();
744
+ /** The widest of some texts in the table's font, measured once by the browser. */
745
+ function widest(texts) {
746
+ const el = measurer.value;
747
+ if (!el) return 0;
748
+ const top = [...new Set(texts)].sort((a, b) => b.length - a.length).slice(0, 12);
749
+ let max = 0;
750
+ for (const text of top) {
751
+ el.textContent = text;
752
+ max = Math.max(max, el.getBoundingClientRect().width);
753
+ }
754
+ el.textContent = "";
755
+ return Math.ceil(max);
756
+ }
757
+ /** Double-clicking a column's edge fits it to its numbers and its header; the corner's goes back to its width. */
758
+ function onResizeFit(event) {
759
+ const handle = event.target.closest("[data-resize]");
760
+ if (!handle) return;
761
+ event.stopPropagation();
762
+ fitWidth(Number(handle.dataset.resize));
763
+ }
764
+ function fitWidth(col) {
765
+ if (col < 0) {
766
+ rowHeadDragged.value = 0;
767
+ return;
768
+ }
769
+ const column = colLines.value[col];
770
+ const display = valueDisplay.value;
771
+ const res = result.value;
772
+ const lines = allLines.value;
773
+ const texts = [];
774
+ const limit = Math.min(lines.length, 5e3);
775
+ for (let i = 0; i < limit; i++) {
776
+ const line = lines[i];
777
+ const v = line.value >= 0 ? line.value : Math.max(0, column.value);
778
+ texts.push(display[v].write(rowBlank(line) ? NaN : res.get(line.node, column.node, v)));
779
+ }
780
+ const head = headCells.value.find((c) => c.resize === col);
781
+ const textWidth = widest(texts);
782
+ const headWidthNeeded = head ? widest([head.text]) + (head.sorted ? 16 : 0) : 0;
783
+ const next = new Map(dragged.value);
784
+ next.set(colKey(column), Math.min(MAX_COLUMN, Math.max(MIN_COLUMN, Math.max(textWidth, headWidthNeeded) + 26)));
785
+ dragged.value = next;
786
+ }
787
+ /** The keys on a focused edge: the arrows resize it (Shift for bigger steps), Enter fits it. */
788
+ function onResizeKeydown(event) {
789
+ const handle = event.target.closest("[data-resize]");
790
+ if (!handle || event.key === "Tab") return;
791
+ event.stopPropagation();
792
+ const col = Number(handle.dataset.resize);
793
+ const step = event.shiftKey ? 32 : 8;
794
+ if (event.key === "ArrowLeft") setWidth(col, widthOf(col) - step);
795
+ else if (event.key === "ArrowRight") setWidth(col, widthOf(col) + step);
796
+ else if (event.key === "Enter") fitWidth(col);
797
+ else return;
798
+ event.preventDefault();
799
+ }
800
+ /** One edge takes Tab: that of the active cell's column, or the row headers'. */
801
+ const resizeStop = computed(() => {
802
+ const col = active.value?.col ?? -1;
803
+ return col >= 0 && col < colLines.value.length ? col : -1;
804
+ });
805
+ const active = shallowRef(null);
806
+ const focused = ref(false);
807
+ /** Where a range of cells starts; the active cell is its other corner. */
808
+ const anchor = shallowRef(null);
809
+ /** The selected range, rows and columns from first to last (column -1 is the row headers); null for one cell. */
810
+ const selection = computed(() => {
811
+ const a = anchor.value;
812
+ const b = active.value;
813
+ if (!a || !b || a.row === b.row && a.col === b.col) return null;
814
+ return {
815
+ top: Math.min(a.row, b.row),
816
+ bottom: Math.max(a.row, b.row),
817
+ left: Math.min(a.col, b.col),
818
+ right: Math.max(a.col, b.col)
819
+ };
820
+ });
821
+ /** What a screen reader reads out: a range picked, cells copied, a group opened. */
822
+ const announcement = ref("");
823
+ let announceTimer;
824
+ function announce(text, delay = 0) {
825
+ clearTimeout(announceTimer);
826
+ announcement.value = "";
827
+ announceTimer = setTimeout(() => announcement.value = text, delay || 50);
828
+ }
829
+ watch(selection, (sel) => {
830
+ if (!sel) return;
831
+ announce(format(messages.value.selected, {
832
+ rows: sel.bottom - sel.top + 1,
833
+ columns: sel.right - sel.left + 1
834
+ }), 400);
835
+ });
836
+ onBeforeUnmount(() => clearTimeout(announceTimer));
837
+ /** The selected columns of a row, as `left:right`; empty when the row is not in the range. */
838
+ const selectedIn = (row) => {
839
+ const sel = selection.value;
840
+ return sel && row >= sel.top && row <= sel.bottom ? `${sel.left}:${sel.right}` : "";
841
+ };
842
+ const allLines = computed(() => [...bodyLines.value, ...footLines.value]);
843
+ /** The highlighted groups' nodes, -1 when there is none (or its path matches no group). */
844
+ const highlightRow = computed(() => {
845
+ const path = props.highlight?.row;
846
+ return path?.length ? result.value.rows.find(path) || -1 : -1;
847
+ });
848
+ const highlightCol = computed(() => {
849
+ const path = props.highlight?.column;
850
+ return path?.length ? result.value.columns.find(path) || -1 : -1;
851
+ });
852
+ const cellId = (row, col) => `${uid}-${row}-${col}`;
853
+ const activeId = computed(() => {
854
+ const a = active.value;
855
+ if (!a || !focused.value) return void 0;
856
+ return cellId(a.row, a.col);
857
+ });
858
+ function cellEvent(row, col) {
859
+ const res = result.value;
860
+ const src = source.value;
861
+ const valueIndex = row.value >= 0 ? row.value : Math.max(0, col.value);
862
+ const indices = () => res.indices(row.node, col.node);
863
+ return {
864
+ row: row.kind === "total" ? [] : res.rows.path(row.node),
865
+ column: col.kind === "total" ? [] : res.columns.path(col.node),
866
+ labels: {
867
+ row: row.kind === "total" ? [] : pathTexts(res.rows, rowWriters.value, row.node),
868
+ column: col.kind === "total" ? [] : pathTexts(res.columns, colWriters.value, col.node)
869
+ },
870
+ valueIndex,
871
+ value: rowBlank(row) ? NaN : res.shown(row.node, col.node, valueIndex, valueDisplay.value[valueIndex]?.show ?? null),
872
+ records: () => indices().map((i) => src.record(i)),
873
+ indices
874
+ };
875
+ }
876
+ /** Set while a drag (or a Shift+click) picks a range, so the click that ends it opens nothing. */
877
+ let swept = false;
878
+ /** Pressing on a cell and dragging picks a range; Shift+click grows it. Touch scrolls instead. */
879
+ function onPointerDown(event) {
880
+ if (event.button !== 0 || event.pointerType === "touch") return;
881
+ const target = event.target;
882
+ if (target.closest("[data-resize], [data-head], [data-toggle-node], [data-sort-field]")) return;
883
+ const el = target.closest("[data-row]");
884
+ const view = viewport.value;
885
+ if (!el || !view) return;
886
+ event.preventDefault();
887
+ view.focus({ preventScroll: true });
888
+ const start = {
889
+ row: Number(el.dataset.row),
890
+ col: Number(el.dataset.col)
891
+ };
892
+ if (event.shiftKey) {
893
+ anchor.value ??= active.value ?? start;
894
+ active.value = start;
895
+ swept = true;
896
+ setTimeout(() => swept = false);
897
+ return;
898
+ }
899
+ anchor.value = null;
900
+ const onMove = (e) => {
901
+ const box = view.getBoundingClientRect();
902
+ if (e.clientY > box.bottom - 24) view.scrollTop += 16;
903
+ else if (e.clientY < box.top + headHeight.value + 8) view.scrollTop -= 16;
904
+ if (e.clientX > box.right - 24) view.scrollLeft += 16;
905
+ else if (e.clientX < box.left + headWidth.value + 8 && start.col >= 0) view.scrollLeft -= 16;
906
+ const hit = document.elementFromPoint(e.clientX, e.clientY)?.closest("[data-row]");
907
+ if (!hit || !view.contains(hit)) return;
908
+ const at = {
909
+ row: Number(hit.dataset.row),
910
+ col: Number(hit.dataset.col)
911
+ };
912
+ if (!swept && at.row === start.row && at.col === start.col) return;
913
+ swept = true;
914
+ anchor.value = start;
915
+ if (active.value?.row !== at.row || active.value?.col !== at.col) active.value = at;
916
+ };
917
+ const onUp = () => {
918
+ window.removeEventListener("pointermove", onMove);
919
+ window.removeEventListener("pointerup", onUp);
920
+ window.removeEventListener("pointercancel", onUp);
921
+ setTimeout(() => swept = false);
922
+ };
923
+ window.addEventListener("pointermove", onMove);
924
+ window.addEventListener("pointerup", onUp);
925
+ window.addEventListener("pointercancel", onUp);
926
+ }
927
+ function onClick(event) {
928
+ const target = event.target;
929
+ if (target.closest("[data-resize]") || justResized || swept) return;
930
+ const sortField = target.closest("[data-sort-field]");
931
+ if (sortField) return sortByField(Number(sortField.dataset.sortField));
932
+ const head = target.closest("[data-head]");
933
+ if (head) {
934
+ const cell = headCells.value.find((c) => c.key === head.dataset.head);
935
+ if (!cell) return;
936
+ if (cell.toggle && (target.closest("[data-toggle]") || !cell.sortable)) return toggleNode("columns", cell.span.node, event.altKey);
937
+ if (cell.sortable) sortByColumn(cell.span);
938
+ return;
939
+ }
940
+ const el = target.closest("[data-row]");
941
+ if (!el) return;
942
+ const row = Number(el.dataset.row);
943
+ const col = Number(el.dataset.col);
944
+ active.value = {
945
+ row,
946
+ col
947
+ };
948
+ anchor.value = null;
949
+ const line = allLines.value[row];
950
+ if (!line) return;
951
+ if (col < 0) {
952
+ const part = target.closest("[data-toggle-node]");
953
+ if (part) toggleNode("rows", Number(part.dataset.toggleNode), event.altKey);
954
+ else if (lineToggles(line)) toggleNode("rows", line.node, event.altKey);
955
+ return;
956
+ }
957
+ const column = colLines.value[col];
958
+ if (column) emit("cellClick", cellEvent(line, column));
959
+ }
960
+ function ensureVisible(row, col) {
961
+ const el = viewport.value;
962
+ if (!el) return;
963
+ const h = rowHeight.value;
964
+ if (row < bodyLines.value.length) {
965
+ const space = viewHeight.value - headHeight.value - footHeight.value;
966
+ if (row * h < el.scrollTop) el.scrollTop = row * h;
967
+ else if ((row + 1) * h > el.scrollTop + space) el.scrollTop = (row + 1) * h - space;
968
+ }
969
+ if (col >= 0) {
970
+ const starts = colStarts.value;
971
+ const space = viewWidth.value - headWidth.value;
972
+ if (starts[col] < el.scrollLeft) el.scrollLeft = starts[col];
973
+ else if (starts[col + 1] > el.scrollLeft + space) el.scrollLeft = starts[col + 1] - space;
974
+ }
975
+ onScroll();
976
+ }
977
+ /** Moves the active cell; `extend` (Shift) grows the range from where it started instead. */
978
+ function moveTo(row, col, extend = false) {
979
+ const rows = allLines.value.length;
980
+ const cols = colLines.value.length;
981
+ if (rows === 0) return;
982
+ const next = {
983
+ row: Math.max(0, Math.min(rows - 1, row)),
984
+ col: Math.max(-1, Math.min(cols - 1, col))
985
+ };
986
+ if (extend) anchor.value ??= active.value ?? next;
987
+ else anchor.value = null;
988
+ active.value = next;
989
+ ensureVisible(next.row, next.col);
990
+ }
991
+ /** The index of the first line of `node`'s parent above `row`. */
992
+ function parentRow(row) {
993
+ const lines = allLines.value;
994
+ const parent = rowTree.value.parent[lines[row].node];
995
+ if (parent <= 0) return row;
996
+ for (let i = row - 1; i >= 0; i--) if (lines[i].node === parent) {
997
+ let first = i;
998
+ while (first > 0 && lines[first - 1].node === parent) first--;
999
+ return first;
1000
+ }
1001
+ for (let i = row + 1; i < lines.length; i++) if (lines[i].node === parent) return i;
1002
+ return row;
1003
+ }
1004
+ function onKeydown(event) {
1005
+ if (event.isComposing) return;
1006
+ const a = active.value ?? {
1007
+ row: 0,
1008
+ col: -1
1009
+ };
1010
+ const line = allLines.value[a.row];
1011
+ const isGroup = !!line && lineToggles(line);
1012
+ const open = isGroup && !rowCollapsed.value.has(line.node);
1013
+ const page = Math.max(1, Math.floor((viewHeight.value - headHeight.value) / rowHeight.value) - 1);
1014
+ const mod = event.ctrlKey || event.metaKey;
1015
+ const shift = event.shiftKey;
1016
+ if (mod && !shift && !event.altKey && event.key.toLowerCase() === "a") {
1017
+ selectAll();
1018
+ event.preventDefault();
1019
+ return;
1020
+ }
1021
+ switch (event.key) {
1022
+ case "ArrowDown":
1023
+ moveTo(a.row + 1, a.col, shift);
1024
+ break;
1025
+ case "ArrowUp":
1026
+ moveTo(a.row - 1, a.col, shift);
1027
+ break;
1028
+ case "ArrowRight":
1029
+ if (a.col < 0 && isGroup && !open && !shift) toggleNode("rows", line.node);
1030
+ else moveTo(a.row, a.col + 1, shift);
1031
+ break;
1032
+ case "ArrowLeft":
1033
+ if (shift) moveTo(a.row, a.col - 1, true);
1034
+ else if (a.col < 0 && open) toggleNode("rows", line.node);
1035
+ else if (a.col < 0 && line) moveTo(parentRow(a.row), -1);
1036
+ else moveTo(a.row, a.col - 1);
1037
+ break;
1038
+ case "PageDown":
1039
+ moveTo(a.row + page, a.col, shift);
1040
+ break;
1041
+ case "PageUp":
1042
+ moveTo(a.row - page, a.col, shift);
1043
+ break;
1044
+ case "Home":
1045
+ moveTo(mod ? 0 : a.row, -1, shift);
1046
+ break;
1047
+ case "End":
1048
+ moveTo(mod ? allLines.value.length - 1 : a.row, colLines.value.length - 1, shift);
1049
+ break;
1050
+ case "Escape":
1051
+ if (!anchor.value) return;
1052
+ anchor.value = null;
1053
+ break;
1054
+ case "Enter":
1055
+ case " ":
1056
+ if (!line) return;
1057
+ if (a.col < 0) {
1058
+ if (isGroup) toggleNode("rows", line.node, event.altKey);
1059
+ } else if (event.key === "Enter") {
1060
+ const column = colLines.value[a.col];
1061
+ if (column) emit("cellClick", cellEvent(line, column));
1062
+ }
1063
+ break;
1064
+ default: return;
1065
+ }
1066
+ event.preventDefault();
1067
+ }
1068
+ function onFocus() {
1069
+ focused.value = true;
1070
+ if (!active.value && allLines.value.length) active.value = {
1071
+ row: 0,
1072
+ col: -1
1073
+ };
1074
+ }
1075
+ const rowKey = (line) => line ? `${line.kind}|${line.value}|${line.kind === "total" ? "" : JSON.stringify(result.value.rows.path(line.node))}` : "";
1076
+ let activeKeys = null;
1077
+ watch(active, (a) => {
1078
+ activeKeys = a ? {
1079
+ row: rowKey(allLines.value[a.row]),
1080
+ col: a.col < 0 ? "" : colKey(colLines.value[a.col])
1081
+ } : null;
1082
+ }, { flush: "sync" });
1083
+ watch([allLines, colLines], ([rows, cols]) => {
1084
+ const a = active.value;
1085
+ const keys = activeKeys;
1086
+ if (!a || !keys) return;
1087
+ let row = rowKey(rows[a.row]) === keys.row ? a.row : rows.findIndex((line) => rowKey(line) === keys.row);
1088
+ if (row < 0) row = Math.min(a.row, rows.length - 1);
1089
+ let col = !keys.col ? -1 : a.col >= 0 && cols[a.col] && colKey(cols[a.col]) === keys.col ? a.col : cols.findIndex((line) => colKey(line) === keys.col);
1090
+ if (col < 0 && keys.col) col = Math.min(a.col, cols.length - 1);
1091
+ if (row < 0) active.value = null;
1092
+ else if (row !== a.row || col !== a.col) {
1093
+ active.value = {
1094
+ row,
1095
+ col
1096
+ };
1097
+ anchor.value = null;
1098
+ nextTick(() => ensureVisible(row, col));
1099
+ }
1100
+ });
1101
+ watch(engineConfig, () => {
1102
+ nextTick(() => {
1103
+ const el = viewport.value;
1104
+ if (!el) return;
1105
+ el.scrollTop = 0;
1106
+ el.scrollLeft = 0;
1107
+ onScroll();
1108
+ });
1109
+ });
1110
+ /** The column headers for export, which always name the values when they run along the columns. */
1111
+ const exportSpans = computed(() => headerSpans(result.value.columns, colLines.value, result.value.columns.levels.length === 0 || !valuesOnRows.value));
1112
+ function rowLabels(line, index, count) {
1113
+ const texts = Array.from({ length: count }, () => "");
1114
+ if (line.kind === "total") texts[0] = messages.value.total;
1115
+ else if (isFirstOfNode(line)) {
1116
+ const head = rowHead(line, index);
1117
+ if (tabular.value) head.parts.forEach((part, level) => part && (texts[level] = part.text));
1118
+ else texts[head.depth] = head.text;
1119
+ }
1120
+ return texts;
1121
+ }
1122
+ /**
1123
+ * Cells of the table: rows `top` to `bottom`, columns `left` to `right`
1124
+ * (`-1` takes the row headers), with the column headers when `heads`.
1125
+ */
1126
+ function gridOf(range, heads) {
1127
+ const res = result.value;
1128
+ const display = valueDisplay.value;
1129
+ const levels = Math.max(1, res.rows.levels.length);
1130
+ const withLeft = range.left < 0;
1131
+ const headCols = withLeft ? levels + (valuesOnRows.value ? 1 : 0) : 0;
1132
+ const from = Math.max(0, range.left);
1133
+ const to = range.right;
1134
+ const width = headCols + Math.max(0, to - from + 1);
1135
+ const cells = [];
1136
+ const headRowCount = heads ? res.columns.levels.length + 1 : 0;
1137
+ for (let r = 0; r < headRowCount; r++) cells.push(Array.from({ length: width }, () => ({
1138
+ text: "",
1139
+ kind: "head"
1140
+ })));
1141
+ if (heads && withLeft) {
1142
+ columnFields.value.forEach((label, level) => cells[level][0] = {
1143
+ text: label,
1144
+ kind: "head"
1145
+ });
1146
+ const last = cells[headRowCount - 1];
1147
+ for (const field of rowFields.value) last[field.index] = {
1148
+ text: field.label,
1149
+ kind: "head"
1150
+ };
1151
+ if (valuesOnRows.value) last[levels] = {
1152
+ text: messages.value.values,
1153
+ kind: "head"
1154
+ };
1155
+ }
1156
+ if (heads) for (const list of exportSpans.value) for (const span of list) {
1157
+ const a = Math.max(span.from, from);
1158
+ const b = Math.min(span.to - 1, to);
1159
+ if (a > b) continue;
1160
+ const col = headCols + a - from;
1161
+ const cell = {
1162
+ text: spanText(span),
1163
+ kind: "head"
1164
+ };
1165
+ const line = colLines.value[span.from];
1166
+ if (span.kind === "total" || span.kind === "subtotal" || line.kind !== "group") cell.total = true;
1167
+ if (b > a) cell.colSpan = b - a + 1;
1168
+ if (span.rows > 1) cell.rowSpan = span.rows;
1169
+ for (let r = span.level; r < span.level + span.rows; r++) for (let c = col; c <= col + b - a; c++) cells[r][c] = null;
1170
+ cells[span.level][col] = cell;
1171
+ }
1172
+ const lines = allLines.value;
1173
+ for (let i = range.top; i <= range.bottom; i++) {
1174
+ const line = lines[i];
1175
+ if (!line) continue;
1176
+ const out = [];
1177
+ const lineTotal = line.kind !== "group" || line.node > 0 && res.rows.isGroup(line.node);
1178
+ const mark = lineTotal ? { total: true } : {};
1179
+ if (withLeft) {
1180
+ for (const text of rowLabels(line, i, levels)) out.push({
1181
+ text,
1182
+ kind: "label",
1183
+ ...mark
1184
+ });
1185
+ if (valuesOnRows.value) out.push({
1186
+ text: line.value >= 0 ? display[line.value].label : "",
1187
+ kind: "label",
1188
+ ...mark
1189
+ });
1190
+ }
1191
+ const blank = rowBlank(line);
1192
+ for (let c = from; c <= to; c++) {
1193
+ const col = colLines.value[c];
1194
+ const v = line.value >= 0 ? line.value : Math.max(0, col.value);
1195
+ const n = blank ? NaN : res.shown(line.node, col.node, v, display[v].show);
1196
+ const total = lineTotal || col.kind !== "group" || col.node > 0 && res.columns.isGroup(col.node);
1197
+ out.push({
1198
+ text: display[v].write(n),
1199
+ kind: "value",
1200
+ value: n,
1201
+ valueIndex: v,
1202
+ ...total ? { total: true } : {}
1203
+ });
1204
+ }
1205
+ cells.push(out);
1206
+ }
1207
+ const widths = [];
1208
+ if (withLeft) {
1209
+ const own = tabular.value ? levelWidth.value : levels === 1 ? rowHeadWidth.value : Math.max(80, Math.round(props.rowHeaderWidth * .6));
1210
+ for (let l = 0; l < levels; l++) widths.push(own);
1211
+ if (valuesOnRows.value) widths.push(VALUE_HEADER_WIDTH);
1212
+ }
1213
+ const starts = colStarts.value;
1214
+ for (let c = from; c <= to; c++) widths.push(starts[c + 1] - starts[c]);
1215
+ return {
1216
+ cells,
1217
+ headRows: headRowCount,
1218
+ headCols,
1219
+ formats: display.map((d) => d.sheetFormat),
1220
+ widths
1221
+ };
1222
+ }
1223
+ const wholeRange = () => ({
1224
+ top: 0,
1225
+ bottom: allLines.value.length - 1,
1226
+ left: -1,
1227
+ right: colLines.value.length - 1
1228
+ });
1229
+ /** The whole table, headers and all, as cells. */
1230
+ const toGrid = () => gridOf(wholeRange(), true);
1231
+ /**
1232
+ * What copying takes: the selected range, or the active cell. A range over
1233
+ * the whole table brings the column headers along.
1234
+ */
1235
+ function copyGrid(all = false) {
1236
+ if (all) return toGrid();
1237
+ const range = selection.value ?? (active.value ? {
1238
+ top: active.value.row,
1239
+ bottom: active.value.row,
1240
+ left: active.value.col,
1241
+ right: active.value.col
1242
+ } : null);
1243
+ if (!range) return null;
1244
+ const whole = wholeRange();
1245
+ return gridOf(range, range.top === 0 && range.bottom === whole.bottom && range.left === -1 && range.right === whole.right);
1246
+ }
1247
+ /** Selects every cell, the row headers too (Ctrl / ⌘ + A). */
1248
+ function selectAll() {
1249
+ const whole = wholeRange();
1250
+ if (whole.bottom < 0) return;
1251
+ anchor.value = {
1252
+ row: 0,
1253
+ col: -1
1254
+ };
1255
+ active.value = {
1256
+ row: whole.bottom,
1257
+ col: whole.right
1258
+ };
1259
+ }
1260
+ /** Copying puts tab-separated text and a table on the clipboard; pasted into DouSheet, numbers keep their formats. */
1261
+ function onCopy(event) {
1262
+ const grid = copyGrid();
1263
+ if (!grid || !event.clipboardData) return;
1264
+ event.preventDefault();
1265
+ event.clipboardData.setData("text/plain", gridToTSV(grid));
1266
+ event.clipboardData.setData("text/html", gridToHTML(grid, { sheet: true }));
1267
+ announceCopied(grid);
1268
+ }
1269
+ function announceCopied(grid) {
1270
+ let cells = 0;
1271
+ for (const row of grid.cells) for (const cell of row) if (cell) cells++;
1272
+ announce(format(messages.value.copied, { cells }));
1273
+ }
1274
+ async function copy(options = {}) {
1275
+ const grid = copyGrid(options.all);
1276
+ if (!grid) return;
1277
+ const text = gridToTSV(grid);
1278
+ const html = gridToHTML(grid, { sheet: true });
1279
+ const clipboard = navigator.clipboard;
1280
+ if (typeof ClipboardItem !== "undefined" && clipboard?.write) await clipboard.write([new ClipboardItem({
1281
+ "text/plain": new Blob([text], { type: "text/plain" }),
1282
+ "text/html": new Blob([html], { type: "text/html" })
1283
+ })]);
1284
+ else await clipboard.writeText(text);
1285
+ announceCopied(grid);
1286
+ }
1287
+ const toCSV = (options) => gridToCSV(toGrid(), options);
1288
+ function toSheet(options = {}) {
1289
+ const grid = toGrid();
1290
+ const colSizes = {};
1291
+ grid.widths.forEach((w, i) => colSizes[i] = w);
1292
+ return {
1293
+ version: 1,
1294
+ sheets: [{
1295
+ name: options.name ?? messages.value.pivot,
1296
+ cells: gridToSheetCells(grid),
1297
+ merges: gridMerges(grid),
1298
+ freeze: {
1299
+ rows: grid.headRows,
1300
+ cols: grid.headCols
1301
+ },
1302
+ colSizes
1303
+ }],
1304
+ styles: structuredClone(SHEET_STYLES)
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Rows for a chart: a row per group on show that is not open (its children
1309
+ * would be the rows instead), a series per column the same way; the
1310
+ * subtotals and totals are left out.
1311
+ */
1312
+ function toChartRows(options = {}) {
1313
+ const res = result.value;
1314
+ const display = valueDisplay.value;
1315
+ const { valueIndex } = options;
1316
+ const many = display.length > 1;
1317
+ const series = colLines.value.map((line, col) => ({
1318
+ line,
1319
+ col
1320
+ })).filter(({ line }) => res.columns.levels.length === 0 ? line.kind === "total" : line.kind === "group").filter(({ line }) => valueIndex === void 0 || line.value < 0 || line.value === valueIndex).map(({ line }, i) => {
1321
+ const v = line.value >= 0 ? line.value : valueIndex ?? 0;
1322
+ const group = line.kind === "total" ? "" : pathText(res.columns, colWriters.value, line.node);
1323
+ const value = display[v].label;
1324
+ const name = group ? many && !valuesOnRows.value ? join([group, value]) : group : value;
1325
+ return {
1326
+ key: `s${i}`,
1327
+ name,
1328
+ valueIndex: v,
1329
+ node: line.node,
1330
+ format: display[v].write
1331
+ };
1332
+ });
1333
+ const lines = res.rows.levels.length === 0 ? footLines.value : bodyLines.value;
1334
+ const rows = [];
1335
+ for (const line of lines) {
1336
+ if (res.rows.levels.length > 0) {
1337
+ if (line.kind !== "group") continue;
1338
+ if (res.rows.isGroup(line.node) && !rowCollapsed.value.has(line.node)) continue;
1339
+ }
1340
+ if (valuesOnRows.value && line.value !== (valueIndex ?? 0)) continue;
1341
+ const row = { label: line.kind === "total" ? messages.value.total : pathText(res.rows, rowWriters.value, line.node) };
1342
+ for (const s of series) {
1343
+ const v = valuesOnRows.value ? line.value : s.valueIndex;
1344
+ const n = res.shown(line.node, s.node, v, display[v].show);
1345
+ row[s.key] = n === n ? n : null;
1346
+ }
1347
+ rows.push(row);
1348
+ }
1349
+ return {
1350
+ x: "label",
1351
+ rows,
1352
+ series: series.map(({ key, name, valueIndex: v, format: write }) => {
1353
+ const at = valuesOnRows.value ? valueIndex ?? 0 : v;
1354
+ return {
1355
+ key,
1356
+ name,
1357
+ valueIndex: at,
1358
+ format: valuesOnRows.value ? display[at].write : write
1359
+ };
1360
+ })
1361
+ };
1362
+ }
1363
+ const join = (texts) => texts.join(" · ");
1364
+ /** A group's labels from the outermost in, as one text. */
1365
+ function pathTexts(tree, writers, node) {
1366
+ const texts = [];
1367
+ for (let at = node; at > 0; at = tree.parent[at]) texts.unshift(nodeText(tree, writers, at));
1368
+ return texts;
1369
+ }
1370
+ const pathText = (tree, writers, node) => join(pathTexts(tree, writers, node));
1371
+ const escape = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1372
+ /** A4 less 10mm margins, in px at 96 per inch. */
1373
+ const PAGE_WIDTH = {
1374
+ portrait: 718,
1375
+ landscape: 1047
1376
+ };
1377
+ function print(options = {}) {
1378
+ const grid = toGrid();
1379
+ const orientation = options.orientation ?? (grid.widths.reduce((a, b) => a + b, 0) > PAGE_WIDTH.portrait ? "landscape" : "portrait");
1380
+ const title = options.title ?? ariaLabel.value;
1381
+ const iframe = document.createElement("iframe");
1382
+ iframe.setAttribute("aria-hidden", "true");
1383
+ iframe.style.cssText = "position:fixed;width:0;height:0;border:0;visibility:hidden";
1384
+ document.body.append(iframe);
1385
+ const doc = iframe.contentDocument;
1386
+ const font = root.value ? getComputedStyle(root.value).fontFamily : "sans-serif";
1387
+ doc.open();
1388
+ doc.write(`<!doctype html><html><head><meta charset="utf-8"><title>${escape(title || document.title)}</title><style>@page{size:A4 ${orientation};margin:10mm}html,body{margin:0;color:#1f1f1f;font:10px/1.4 ${font};font-variant-numeric:tabular-nums}h1{margin:0 0 8px;font-size:13px;font-weight:600}table{border-collapse:collapse;width:100%}thead{display:table-header-group}tr{break-inside:avoid}th,td{border:1px solid #d6d6d6;padding:3px 6px;white-space:nowrap}th{background:#f5f5f5;font-weight:500;text-align:left}tbody th{background:none}.is-total{font-weight:600}thead th.is-end{text-align:right}*{print-color-adjust:exact;-webkit-print-color-adjust:exact}</style></head><body>${title ? `<h1>${escape(title)}</h1>` : ""}${gridToHTML(grid)}</body></html>`);
1389
+ doc.close();
1390
+ const win = iframe.contentWindow;
1391
+ const done = () => iframe.remove();
1392
+ win.addEventListener("afterprint", done);
1393
+ setTimeout(done, 6e4);
1394
+ win.focus();
1395
+ win.print();
1396
+ }
1397
+ const ariaLabel = computed(() => props.ariaLabel ?? messages.value.pivot);
1398
+ const rootStyle = computed(() => {
1399
+ return `height:min(${typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight}, ${headHeight.value + bodyHeight.value + footHeight.value + gutter.value + 2}px);--dou-pivot-row-height:${rowHeight.value}px;--dou-pivot-head-width:${headWidth.value}px`;
1400
+ });
1401
+ __expose({
1402
+ get result() {
1403
+ return result.value;
1404
+ },
1405
+ expandAll,
1406
+ collapseAll,
1407
+ toggle,
1408
+ toGrid,
1409
+ toCSV,
1410
+ toSheet,
1411
+ toChartRows,
1412
+ copy,
1413
+ print,
1414
+ selectAll
1415
+ });
1416
+ return (_ctx, _cache) => {
1417
+ return openBlock(), createElementBlock("div", {
1418
+ ref_key: "root",
1419
+ ref: root,
1420
+ class: normalizeClass(["dou-pivot", [`dou-pivot--${__props.size}`, {
1421
+ "is-focused": focused.value,
1422
+ "is-scrolled-x": scroll.value.left > 0,
1423
+ "is-scrolled-y": scroll.value.top > 0,
1424
+ "has-values-column": valuesOnRows.value,
1425
+ "is-tabular": tabular.value,
1426
+ "is-busy": busy.value
1427
+ }]]),
1428
+ style: normalizeStyle(rootStyle.value)
1429
+ }, [
1430
+ createElementVNode("div", {
1431
+ ref_key: "viewport",
1432
+ ref: viewport,
1433
+ class: "dou-pivot__viewport",
1434
+ role: "treegrid",
1435
+ tabindex: "0",
1436
+ "aria-label": ariaLabel.value,
1437
+ "aria-rowcount": headRows.value + allLines.value.length,
1438
+ "aria-colcount": colLines.value.length + (valuesOnRows.value ? 2 : 1),
1439
+ "aria-multiselectable": "true",
1440
+ "aria-activedescendant": activeId.value,
1441
+ "aria-busy": busy.value || void 0,
1442
+ onScroll,
1443
+ onPointerdown: onPointerDown,
1444
+ onClick,
1445
+ onCopy,
1446
+ onKeydown,
1447
+ onFocus,
1448
+ onBlur: _cache[2] || (_cache[2] = ($event) => focused.value = false)
1449
+ }, [
1450
+ createElementVNode("div", {
1451
+ class: "dou-pivot__sheet",
1452
+ style: normalizeStyle(`width:${totalWidth.value}px`)
1453
+ }, [
1454
+ createElementVNode("div", {
1455
+ class: "dou-pivot__head",
1456
+ role: "rowgroup",
1457
+ style: normalizeStyle(`height:${headHeight.value}px`)
1458
+ }, [createElementVNode("div", {
1459
+ class: "dou-pivot__corner",
1460
+ style: normalizeStyle(`width:${headWidth.value}px;height:${headHeight.value}px`)
1461
+ }, [
1462
+ (openBlock(true), createElementBlock(Fragment, null, renderList(columnFields.value, (label, level) => {
1463
+ return openBlock(), createElementBlock("div", {
1464
+ key: `c${level}`,
1465
+ class: "dou-pivot__corner-line is-column",
1466
+ style: normalizeStyle(`top:${level * rowHeight.value}px`)
1467
+ }, [createElementVNode("span", _hoisted_2, toDisplayString(label), 1)], 4);
1468
+ }), 128)),
1469
+ createElementVNode("div", {
1470
+ class: "dou-pivot__corner-line is-rows",
1471
+ style: normalizeStyle(`top:${(headRows.value - 1) * rowHeight.value}px`)
1472
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(rowFields.value, (field) => {
1473
+ return openBlock(), createElementBlock(Fragment, { key: field.index }, [field.index > 0 && !tabular.value ? (openBlock(), createElementBlock("span", _hoisted_3, "/")) : createCommentVNode("", true), createElementVNode("span", {
1474
+ class: normalizeClass(tabular.value ? ["dou-pivot__corner-field", { "is-last": field.index === rowFields.value.length - 1 }] : "dou-pivot__corner-inline"),
1475
+ style: normalizeStyle(tabular.value ? `width:${levelWidth.value}px` : void 0)
1476
+ }, [createElementVNode("button", {
1477
+ type: "button",
1478
+ class: normalizeClass(["dou-pivot__field", { "is-sorted": !config.value.sort && field.sort === "desc" }]),
1479
+ tabindex: "-1",
1480
+ "data-sort-field": field.index,
1481
+ "aria-label": unref(format)(unref(messages).sortField, { field: field.label })
1482
+ }, [createTextVNode(toDisplayString(field.label) + " ", 1), !config.value.sort && field.sort === "desc" ? (openBlock(), createBlock(unref(ArrowDownIcon), {
1483
+ key: 0,
1484
+ "aria-hidden": "true"
1485
+ })) : createCommentVNode("", true)], 10, _hoisted_4)], 6)], 64);
1486
+ }), 128)), valuesOnRows.value ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(unref(messages).values), 1)) : !valueRow.value ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(valueDisplay.value[0].label), 1)) : createCommentVNode("", true)], 4),
1487
+ __props.resizable ? (openBlock(), createElementBlock("span", {
1488
+ key: 0,
1489
+ class: "dou-pivot__resize",
1490
+ "data-resize": "-1",
1491
+ role: "separator",
1492
+ "aria-orientation": "vertical",
1493
+ "aria-label": unref(format)(unref(messages).columnWidth, { column: rowFieldNames.value }),
1494
+ "aria-valuenow": widthOf(-1),
1495
+ "aria-valuemin": minWidthOf(-1),
1496
+ "aria-valuemax": maxWidthOf(-1),
1497
+ tabindex: resizeStop.value === -1 ? 0 : -1,
1498
+ title: unref(messages).resizeColumn,
1499
+ onPointerdown: onResizeDown,
1500
+ onDblclick: onResizeFit,
1501
+ onKeydown: onResizeKeydown
1502
+ }, null, 40, _hoisted_7)) : createCommentVNode("", true)
1503
+ ], 4), createElementVNode("div", {
1504
+ class: "dou-pivot__col-heads",
1505
+ style: normalizeStyle(`width:${columnsWidth.value}px;height:${headHeight.value}px`)
1506
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(headCells.value, (cell) => {
1507
+ return openBlock(), createElementBlock("div", {
1508
+ key: cell.key,
1509
+ class: normalizeClass(cell.cls),
1510
+ role: "columnheader",
1511
+ "aria-sort": cell.sorted ? cell.sorted === "asc" ? "ascending" : "descending" : void 0,
1512
+ "aria-expanded": cell.toggle ? cell.open : void 0,
1513
+ "data-head": cell.key,
1514
+ title: cell.sortable ? unref(messages).sortValue : void 0,
1515
+ style: normalizeStyle(cell.style)
1516
+ }, [createElementVNode("span", _hoisted_9, [
1517
+ cell.toggle ? (openBlock(), createElementBlock("span", {
1518
+ key: 0,
1519
+ class: normalizeClass(["dou-pivot__toggle", { "is-open": cell.open }]),
1520
+ "data-toggle": "",
1521
+ "aria-label": cell.open ? unref(messages).collapse : unref(messages).expand
1522
+ }, [createVNode(unref(ChevronRightIcon), { "aria-hidden": "true" })], 10, _hoisted_10)) : createCommentVNode("", true),
1523
+ createElementVNode("span", _hoisted_11, toDisplayString(cell.text), 1),
1524
+ cell.sorted === "asc" ? (openBlock(), createBlock(unref(ArrowUpIcon), {
1525
+ key: 1,
1526
+ class: "dou-pivot__sort",
1527
+ "aria-hidden": "true"
1528
+ })) : cell.sorted === "desc" ? (openBlock(), createBlock(unref(ArrowDownIcon), {
1529
+ key: 2,
1530
+ class: "dou-pivot__sort",
1531
+ "aria-hidden": "true"
1532
+ })) : createCommentVNode("", true)
1533
+ ]), cell.resize >= 0 ? (openBlock(), createElementBlock("span", {
1534
+ key: 0,
1535
+ class: "dou-pivot__resize",
1536
+ "data-resize": cell.resize,
1537
+ role: "separator",
1538
+ "aria-orientation": "vertical",
1539
+ "aria-label": unref(format)(unref(messages).columnWidth, { column: cell.text }),
1540
+ "aria-valuenow": widthOf(cell.resize),
1541
+ "aria-valuemin": MIN_COLUMN,
1542
+ "aria-valuemax": MAX_COLUMN,
1543
+ tabindex: resizeStop.value === cell.resize ? 0 : -1,
1544
+ title: unref(messages).resizeColumn,
1545
+ onPointerdown: onResizeDown,
1546
+ onDblclick: onResizeFit,
1547
+ onKeydown: onResizeKeydown
1548
+ }, null, 40, _hoisted_12)) : createCommentVNode("", true)], 14, _hoisted_8);
1549
+ }), 128))], 4)], 4),
1550
+ createElementVNode("div", {
1551
+ class: "dou-pivot__body",
1552
+ role: "rowgroup",
1553
+ style: normalizeStyle(`height:${bodyHeight.value}px`)
1554
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleRows.value, ({ line, index }, __, ___, _cached) => {
1555
+ const _memo = [
1556
+ line,
1557
+ colWindow.value,
1558
+ colLines.value,
1559
+ colStarts.value,
1560
+ rowHeadWidth.value,
1561
+ tabular.value,
1562
+ valueDisplay.value,
1563
+ rowWriters.value,
1564
+ active.value?.row === index ? active.value.col : -9,
1565
+ selectedIn(index),
1566
+ highlightCol.value,
1567
+ line.node === highlightRow.value
1568
+ ];
1569
+ if (_cached && _cached.el && _cached.key === index && isMemoSame(_cached, _memo)) return _cached;
1570
+ const _item = (openBlock(), createElementBlock("div", {
1571
+ key: index,
1572
+ class: normalizeClass(["dou-pivot__row", {
1573
+ "is-group": lineToggles(line),
1574
+ "is-subtotal": line.kind === "subtotal",
1575
+ "is-node-start": line.value <= 0,
1576
+ "is-highlighted": line.node === highlightRow.value
1577
+ }]),
1578
+ role: "row",
1579
+ "aria-rowindex": headRows.value + index + 1,
1580
+ "aria-level": rowTree.value.depth[line.node],
1581
+ "aria-expanded": lineToggles(line) ? !rowCollapsed.value.has(line.node) : void 0,
1582
+ style: normalizeStyle(`top:${index * rowHeight.value}px;width:${totalWidth.value}px`)
1583
+ }, [
1584
+ (openBlock(true), createElementBlock(Fragment, null, renderList([rowHead(line, index)], (head) => {
1585
+ return openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("div", {
1586
+ id: cellId(index, -1),
1587
+ class: normalizeClass(["dou-pivot__row-head", {
1588
+ "is-active": active.value?.row === index && active.value.col === -1,
1589
+ "is-clickable": head.group,
1590
+ "is-selected": selectedIn(index).startsWith("-1:")
1591
+ }]),
1592
+ role: "rowheader",
1593
+ "aria-selected": selectedIn(index).startsWith("-1:") || void 0,
1594
+ "data-row": index,
1595
+ "data-col": "-1",
1596
+ style: normalizeStyle(tabular.value ? `width:${rowHeadWidth.value}px;padding-left:0` : `width:${rowHeadWidth.value}px;padding-left:${12 + head.depth * 16}px`)
1597
+ }, [tabular.value ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(head.parts, (part, level) => {
1598
+ return openBlock(), createElementBlock("span", {
1599
+ key: level,
1600
+ class: "dou-pivot__level",
1601
+ style: normalizeStyle(`width:${levelWidth.value}px`)
1602
+ }, [part ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [part.toggle > 0 ? (openBlock(), createElementBlock("span", {
1603
+ key: 0,
1604
+ class: normalizeClass(["dou-pivot__toggle", { "is-open": part.open }]),
1605
+ "data-toggle-node": part.toggle,
1606
+ "aria-hidden": "true"
1607
+ }, [createVNode(unref(ChevronRightIcon))], 10, _hoisted_14)) : level < head.parts.length - 1 ? (openBlock(), createElementBlock("span", _hoisted_15)) : createCommentVNode("", true), createElementVNode("span", _hoisted_16, toDisplayString(part.text), 1)], 64)) : createCommentVNode("", true)], 4);
1608
+ }), 128)) : head.firstOfNode ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [head.group ? (openBlock(), createElementBlock("span", {
1609
+ key: 0,
1610
+ class: normalizeClass(["dou-pivot__toggle", { "is-open": head.open }]),
1611
+ "aria-hidden": "true"
1612
+ }, [createVNode(unref(ChevronRightIcon))], 2)) : rowTree.value.levels.length > 1 ? (openBlock(), createElementBlock("span", _hoisted_17)) : createCommentVNode("", true), createElementVNode("span", _hoisted_18, toDisplayString(head.text), 1)], 64)) : createCommentVNode("", true)], 14, _hoisted_13), valuesOnRows.value ? (openBlock(), createElementBlock("div", {
1613
+ key: 0,
1614
+ class: "dou-pivot__value-head",
1615
+ role: "rowheader",
1616
+ style: normalizeStyle(`width:${VALUE_HEADER_WIDTH}px;left:${rowHeadWidth.value}px`)
1617
+ }, [createElementVNode("span", _hoisted_19, toDisplayString(head.valueText), 1)], 4)) : createCommentVNode("", true)], 64);
1618
+ }), 128)),
1619
+ createElementVNode("div", {
1620
+ class: "dou-pivot__spacer",
1621
+ style: normalizeStyle(`width:${colStarts.value[colWindow.value.from]}px`)
1622
+ }, null, 4),
1623
+ (openBlock(true), createElementBlock(Fragment, null, renderList(rowCells(line, index), (cell) => {
1624
+ return openBlock(), createElementBlock("div", {
1625
+ id: cellId(index, cell.col),
1626
+ key: cell.key,
1627
+ class: normalizeClass([cell.cls, { "is-active": active.value?.row === index && active.value.col === cell.col }]),
1628
+ role: "gridcell",
1629
+ "aria-selected": cell.selected || void 0,
1630
+ "aria-colindex": cell.col + (valuesOnRows.value ? 3 : 2),
1631
+ "data-row": index,
1632
+ "data-col": cell.col,
1633
+ style: normalizeStyle(cell.style)
1634
+ }, [createElementVNode("span", _hoisted_21, toDisplayString(cell.text), 1)], 14, _hoisted_20);
1635
+ }), 128))
1636
+ ], 14, [
1637
+ "aria-rowindex",
1638
+ "aria-level",
1639
+ "aria-expanded"
1640
+ ]));
1641
+ _item.memo = _memo;
1642
+ return _item;
1643
+ }, _cache, 0), 128))], 4),
1644
+ footLines.value.length ? (openBlock(), createElementBlock("div", {
1645
+ key: 0,
1646
+ class: "dou-pivot__foot",
1647
+ role: "rowgroup",
1648
+ style: normalizeStyle(`height:${footHeight.value}px`)
1649
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(visibleFoot.value, ({ line, index }, i) => {
1650
+ return openBlock(), createElementBlock("div", {
1651
+ key: index,
1652
+ class: "dou-pivot__row is-total",
1653
+ role: "row",
1654
+ "aria-rowindex": headRows.value + index + 1,
1655
+ style: normalizeStyle(`top:${i * rowHeight.value}px;width:${totalWidth.value}px`)
1656
+ }, [
1657
+ createElementVNode("div", {
1658
+ id: cellId(index, -1),
1659
+ class: normalizeClass(["dou-pivot__row-head", {
1660
+ "is-active": active.value?.row === index && active.value.col === -1,
1661
+ "is-selected": selectedIn(index).startsWith("-1:")
1662
+ }]),
1663
+ role: "rowheader",
1664
+ "aria-selected": selectedIn(index).startsWith("-1:") || void 0,
1665
+ "data-row": index,
1666
+ "data-col": "-1",
1667
+ style: normalizeStyle(`width:${rowHeadWidth.value}px`)
1668
+ }, [line.value <= 0 ? (openBlock(), createElementBlock("span", _hoisted_24, toDisplayString(unref(messages).total), 1)) : createCommentVNode("", true)], 14, _hoisted_23),
1669
+ valuesOnRows.value ? (openBlock(), createElementBlock("div", {
1670
+ key: 0,
1671
+ class: "dou-pivot__value-head",
1672
+ role: "rowheader",
1673
+ style: normalizeStyle(`width:${VALUE_HEADER_WIDTH}px;left:${rowHeadWidth.value}px`)
1674
+ }, [createElementVNode("span", _hoisted_25, toDisplayString(valueDisplay.value[line.value]?.label), 1)], 4)) : createCommentVNode("", true),
1675
+ createElementVNode("div", {
1676
+ class: "dou-pivot__spacer",
1677
+ style: normalizeStyle(`width:${colStarts.value[colWindow.value.from]}px`)
1678
+ }, null, 4),
1679
+ (openBlock(true), createElementBlock(Fragment, null, renderList(rowCells(line, index), (cell) => {
1680
+ return openBlock(), createElementBlock("div", {
1681
+ id: cellId(index, cell.col),
1682
+ key: cell.key,
1683
+ class: normalizeClass([cell.cls, { "is-active": active.value?.row === index && active.value.col === cell.col }]),
1684
+ role: "gridcell",
1685
+ "aria-selected": cell.selected || void 0,
1686
+ "aria-colindex": cell.col + (valuesOnRows.value ? 3 : 2),
1687
+ "data-row": index,
1688
+ "data-col": cell.col,
1689
+ style: normalizeStyle(cell.style)
1690
+ }, [createElementVNode("span", _hoisted_27, toDisplayString(cell.text), 1)], 14, _hoisted_26);
1691
+ }), 128))
1692
+ ], 12, _hoisted_22);
1693
+ }), 128))], 4)) : createCommentVNode("", true)
1694
+ ], 4),
1695
+ createElementVNode("span", {
1696
+ ref_key: "measurer",
1697
+ ref: measurer,
1698
+ class: "dou-pivot__measure",
1699
+ "aria-hidden": "true"
1700
+ }, null, 512),
1701
+ result.value.matched === 0 && !busy.value && result.value !== shapeOnly.value ? (openBlock(), createElementBlock("div", _hoisted_28, toDisplayString(unref(messages).noData), 1)) : createCommentVNode("", true)
1702
+ ], 40, _hoisted_1),
1703
+ createElementVNode("span", _hoisted_29, toDisplayString(announcement.value), 1),
1704
+ busy.value ? (openBlock(), createElementBlock("div", _hoisted_30, [createElementVNode("span", _hoisted_31, toDisplayString(unref(messages).busy), 1)])) : createCommentVNode("", true)
1705
+ ], 6);
1706
+ };
1707
+ }
1708
+ });
1709
+ export { pivot_vue_vue_type_script_setup_true_lang_default as default };
1710
+
1711
+ //# sourceMappingURL=pivot.vue_vue_type_script_setup_true_lang.js.map