@dou-ui/pivot 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/assets/default-PXde2wzm.js.map +1 -0
- package/dist/core/format.js +116 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/labels.js +28 -0
- package/dist/core/labels.js.map +1 -0
- package/dist/core/layout.js +190 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/source.js +46 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/table.js +154 -0
- package/dist/core/table.js.map +1 -0
- package/dist/engine/aggregate.js +51 -0
- package/dist/engine/aggregate.js.map +1 -0
- package/dist/engine/compute.js +735 -0
- package/dist/engine/compute.js.map +1 -0
- package/dist/engine/engine.js +46 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/formula.js +225 -0
- package/dist/engine/formula.js.map +1 -0
- package/dist/engine/group.js +107 -0
- package/dist/engine/group.js.map +1 -0
- package/dist/engine/hash.js +108 -0
- package/dist/engine/hash.js.map +1 -0
- package/dist/engine/index.js +8 -0
- package/dist/engine/show.js +132 -0
- package/dist/engine/show.js.map +1 -0
- package/dist/engine/source.js +339 -0
- package/dist/engine/source.js.map +1 -0
- package/dist/engine/tree.js +226 -0
- package/dist/engine/tree.js.map +1 -0
- package/dist/fields/fields.js +6 -0
- package/dist/fields/fields.js.map +1 -0
- package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js +881 -0
- package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/filter-list.js +6 -0
- package/dist/fields/filter-list.js.map +1 -0
- package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js +184 -0
- package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/formula-dialog.js +6 -0
- package/dist/fields/formula-dialog.js.map +1 -0
- package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js +165 -0
- package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/model.js +200 -0
- package/dist/fields/model.js.map +1 -0
- package/dist/fields/top-dialog.js +6 -0
- package/dist/fields/top-dialog.js.map +1 -0
- package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js +174 -0
- package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +508 -0
- package/dist/locale.js.map +1 -0
- package/dist/pivot.js +6 -0
- package/dist/pivot.js.map +1 -0
- package/dist/pivot.vue_vue_type_script_setup_true_lang.js +1711 -0
- package/dist/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +13 -0
- package/dist/types/core/format.d.ts +26 -0
- package/dist/types/core/labels.d.ts +14 -0
- package/dist/types/core/layout.d.ts +72 -0
- package/dist/types/core/source.d.ts +7 -0
- package/dist/types/core/table.d.ts +73 -0
- package/dist/types/engine/aggregate.d.ts +35 -0
- package/dist/types/engine/compute.d.ts +173 -0
- package/dist/types/engine/engine.d.ts +29 -0
- package/dist/types/engine/formula.d.ts +44 -0
- package/dist/types/engine/group.d.ts +34 -0
- package/dist/types/engine/hash.d.ts +36 -0
- package/dist/types/engine/index.d.ts +15 -0
- package/dist/types/engine/show.d.ts +27 -0
- package/dist/types/engine/source.d.ts +108 -0
- package/dist/types/engine/tree.d.ts +87 -0
- package/dist/types/engine/types.d.ts +148 -0
- package/dist/types/fields/fields.vue.d.ts +26 -0
- package/dist/types/fields/filter-list.vue.d.ts +20 -0
- package/dist/types/fields/formula-dialog.vue.d.ts +30 -0
- package/dist/types/fields/model.d.ts +52 -0
- package/dist/types/fields/top-dialog.vue.d.ts +25 -0
- package/dist/types/index.d.ts +458 -0
- package/dist/types/locale.d.ts +184 -0
- package/dist/types/pivot.vue.d.ts +29 -0
- package/dist/types/types.d.ts +199 -0
- package/dist/types/worker/client.d.ts +19 -0
- package/dist/types/worker/default.d.ts +1 -0
- package/dist/types/worker/index.d.ts +5 -0
- package/dist/types/worker/protocol.d.ts +60 -0
- package/dist/types/worker/serve.d.ts +22 -0
- package/dist/worker/client.js +191 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/default.js +20 -0
- package/dist/worker/default.js.map +1 -0
- package/dist/worker/index.js +3 -0
- package/dist/worker/serve.js +101 -0
- package/dist/worker/serve.js.map +1 -0
- 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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|