@vueye-table/headless 3.0.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.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/index.d.ts +641 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +734 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid } from "@vueye-table/vue";
|
|
2
|
+
import { defineComponent, h, inject, nextTick, onMounted, onScopeDispose, provide, ref, useId, watch } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/shared.ts
|
|
5
|
+
/** The element a component renders. Every headless component accepts `as`. */
|
|
6
|
+
const asProp = (tag) => ({
|
|
7
|
+
type: String,
|
|
8
|
+
default: tag
|
|
9
|
+
});
|
|
10
|
+
const columnProp = {
|
|
11
|
+
type: Object,
|
|
12
|
+
required: true
|
|
13
|
+
};
|
|
14
|
+
const rowProp = {
|
|
15
|
+
type: Object,
|
|
16
|
+
required: true
|
|
17
|
+
};
|
|
18
|
+
/** `data-*` attributes take strings; `undefined` leaves the attribute off. */
|
|
19
|
+
function flag(value) {
|
|
20
|
+
return value ? "" : void 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/controls.ts
|
|
25
|
+
function checkbox(props) {
|
|
26
|
+
return h("input", {
|
|
27
|
+
type: "checkbox",
|
|
28
|
+
checked: props.checked,
|
|
29
|
+
indeterminate: props.indeterminate,
|
|
30
|
+
disabled: props.disabled,
|
|
31
|
+
"aria-label": props.label,
|
|
32
|
+
"data-state": props.indeterminate ? "indeterminate" : props.checked ? "checked" : "unchecked",
|
|
33
|
+
onChange: props.onChange
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Selects or clears every row in the table's select-all scope. It shows the indeterminate state
|
|
38
|
+
* when only some are selected. The slot receives the state to draw a custom control.
|
|
39
|
+
*/
|
|
40
|
+
const DataTableSelectAll = defineComponent({
|
|
41
|
+
name: "DataTableSelectAll",
|
|
42
|
+
slots: Object,
|
|
43
|
+
props: { label: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: "Select all rows"
|
|
46
|
+
} },
|
|
47
|
+
setup(props, { slots }) {
|
|
48
|
+
const table = injectDataTable("<DataTableSelectAll>");
|
|
49
|
+
return () => {
|
|
50
|
+
const state = table.allSelection;
|
|
51
|
+
const toggle = () => table.toggleAll();
|
|
52
|
+
return slots.default?.({
|
|
53
|
+
state,
|
|
54
|
+
toggle
|
|
55
|
+
}) ?? checkbox({
|
|
56
|
+
checked: state === "all",
|
|
57
|
+
indeterminate: state === "some",
|
|
58
|
+
label: props.label,
|
|
59
|
+
disabled: table.selectionMode !== "multiple" || table.rows.length === 0,
|
|
60
|
+
onChange: toggle
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
/** Selects or clears one row. */
|
|
66
|
+
const DataTableSelectRow = defineComponent({
|
|
67
|
+
name: "DataTableSelectRow",
|
|
68
|
+
slots: Object,
|
|
69
|
+
props: {
|
|
70
|
+
row: rowProp,
|
|
71
|
+
label: {
|
|
72
|
+
type: String,
|
|
73
|
+
default: "Select row"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
setup(props, { slots }) {
|
|
77
|
+
const table = injectDataTable("<DataTableSelectRow>");
|
|
78
|
+
return () => {
|
|
79
|
+
const selected = table.isSelected(props.row.key);
|
|
80
|
+
const toggle = () => table.toggleRow(props.row.key);
|
|
81
|
+
return slots.default?.({
|
|
82
|
+
selected,
|
|
83
|
+
toggle
|
|
84
|
+
}) ?? checkbox({
|
|
85
|
+
checked: selected,
|
|
86
|
+
indeterminate: false,
|
|
87
|
+
label: props.label,
|
|
88
|
+
disabled: table.selectionMode === "none",
|
|
89
|
+
onChange: toggle
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* A search box bound to the table's free-text search. `debounce` waits that many milliseconds
|
|
96
|
+
* after the last keystroke before searching.
|
|
97
|
+
*/
|
|
98
|
+
const DataTableSearch = defineComponent({
|
|
99
|
+
name: "DataTableSearch",
|
|
100
|
+
props: {
|
|
101
|
+
debounce: {
|
|
102
|
+
type: Number,
|
|
103
|
+
default: 0
|
|
104
|
+
},
|
|
105
|
+
label: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: "Search"
|
|
108
|
+
},
|
|
109
|
+
placeholder: {
|
|
110
|
+
type: String,
|
|
111
|
+
default: void 0
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
setup(props) {
|
|
115
|
+
const table = injectDataTable("<DataTableSearch>");
|
|
116
|
+
let timer;
|
|
117
|
+
onScopeDispose(() => clearTimeout(timer));
|
|
118
|
+
return () => h("input", {
|
|
119
|
+
type: "search",
|
|
120
|
+
value: table.state.search,
|
|
121
|
+
placeholder: props.placeholder,
|
|
122
|
+
"aria-label": props.label,
|
|
123
|
+
onInput: (event) => {
|
|
124
|
+
const { value } = event.target;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
if (props.debounce > 0) timer = setTimeout(() => table.search(value), props.debounce);
|
|
127
|
+
else table.search(value);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* Page navigation. The slot receives everything needed to draw custom controls; without one it
|
|
134
|
+
* renders previous, numbered, and next buttons with `aria-current` on the current page.
|
|
135
|
+
*/
|
|
136
|
+
const DataTablePagination = defineComponent({
|
|
137
|
+
name: "DataTablePagination",
|
|
138
|
+
slots: Object,
|
|
139
|
+
props: {
|
|
140
|
+
as: asProp("nav"),
|
|
141
|
+
label: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: "Pagination"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
setup(props, { slots }) {
|
|
147
|
+
const table = injectDataTable("<DataTablePagination>");
|
|
148
|
+
return () => {
|
|
149
|
+
const slotProps = {
|
|
150
|
+
page: table.page,
|
|
151
|
+
pageCount: table.pageCount,
|
|
152
|
+
items: table.pageItems,
|
|
153
|
+
canPrevious: table.canPreviousPage,
|
|
154
|
+
canNext: table.canNextPage,
|
|
155
|
+
goToPage: table.goToPage,
|
|
156
|
+
next: table.nextPage,
|
|
157
|
+
previous: table.previousPage
|
|
158
|
+
};
|
|
159
|
+
const button = (label, content, disabled, onClick, current = false) => h("button", {
|
|
160
|
+
type: "button",
|
|
161
|
+
disabled,
|
|
162
|
+
"aria-label": label,
|
|
163
|
+
"aria-current": current ? "page" : void 0,
|
|
164
|
+
"data-current": flag(current),
|
|
165
|
+
onClick
|
|
166
|
+
}, content);
|
|
167
|
+
return h(props.as, { "aria-label": props.label }, slots.default?.(slotProps) ?? [
|
|
168
|
+
button("Previous page", "‹", !table.canPreviousPage, table.previousPage),
|
|
169
|
+
...table.pageItems.map((item) => item.type === "gap" ? h("span", {
|
|
170
|
+
key: item.key,
|
|
171
|
+
"aria-hidden": "true",
|
|
172
|
+
"data-gap": ""
|
|
173
|
+
}, "…") : button(`Page ${item.page}`, String(item.page), false, () => table.goToPage(item.page), item.current)),
|
|
174
|
+
button("Next page", "›", !table.canNextPage, table.nextPage)
|
|
175
|
+
]);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
/** A page size picker. */
|
|
180
|
+
const DataTablePageSize = defineComponent({
|
|
181
|
+
name: "DataTablePageSize",
|
|
182
|
+
props: {
|
|
183
|
+
options: {
|
|
184
|
+
type: Array,
|
|
185
|
+
default: () => [
|
|
186
|
+
5,
|
|
187
|
+
10,
|
|
188
|
+
20,
|
|
189
|
+
50
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
label: {
|
|
193
|
+
type: String,
|
|
194
|
+
default: "Rows per page"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
setup(props) {
|
|
198
|
+
const table = injectDataTable("<DataTablePageSize>");
|
|
199
|
+
return () => {
|
|
200
|
+
const sizes = props.options.includes(table.pageSize) ? props.options : [...props.options, table.pageSize].toSorted((left, right) => left - right);
|
|
201
|
+
return h("select", {
|
|
202
|
+
value: table.pageSize,
|
|
203
|
+
"aria-label": props.label,
|
|
204
|
+
onChange: (event) => table.setPageSize(Number(event.target.value))
|
|
205
|
+
}, sizes.map((size) => h("option", {
|
|
206
|
+
key: size,
|
|
207
|
+
value: size,
|
|
208
|
+
selected: size === table.pageSize
|
|
209
|
+
}, String(size))));
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
/** A checkbox per column that shows or hides it. The slot receives the columns and `toggle`. */
|
|
214
|
+
const DataTableColumnVisibility = defineComponent({
|
|
215
|
+
name: "DataTableColumnVisibility",
|
|
216
|
+
slots: Object,
|
|
217
|
+
props: {
|
|
218
|
+
as: asProp("fieldset"),
|
|
219
|
+
legend: {
|
|
220
|
+
type: String,
|
|
221
|
+
default: "Columns"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
setup(props, { slots }) {
|
|
225
|
+
const table = injectDataTable("<DataTableColumnVisibility>");
|
|
226
|
+
return () => {
|
|
227
|
+
const hidden = new Set(table.state.hiddenColumns);
|
|
228
|
+
const columns = table.allColumns.map((column) => ({
|
|
229
|
+
column,
|
|
230
|
+
visible: !hidden.has(column.id)
|
|
231
|
+
}));
|
|
232
|
+
return h(props.as, slots.default?.({
|
|
233
|
+
columns,
|
|
234
|
+
toggle: table.toggleColumn
|
|
235
|
+
}) ?? [props.as === "fieldset" ? h("legend", props.legend) : null, ...columns.map(({ column, visible }) => h("label", {
|
|
236
|
+
key: column.id,
|
|
237
|
+
"data-column": column.id
|
|
238
|
+
}, [h("input", {
|
|
239
|
+
type: "checkbox",
|
|
240
|
+
checked: visible,
|
|
241
|
+
disabled: visible && table.columns.length === 1,
|
|
242
|
+
onChange: () => table.toggleColumn(column.id)
|
|
243
|
+
}), column.header]))]);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* A polite live region describing what is shown, such as "1–10 of 57 rows". Screen readers
|
|
249
|
+
* announce it after a search, filter, or page change.
|
|
250
|
+
*/
|
|
251
|
+
const DataTableStatus = defineComponent({
|
|
252
|
+
name: "DataTableStatus",
|
|
253
|
+
slots: Object,
|
|
254
|
+
props: { as: asProp("p") },
|
|
255
|
+
setup(props, { slots }) {
|
|
256
|
+
const table = injectDataTable("<DataTableStatus>");
|
|
257
|
+
return () => {
|
|
258
|
+
const slotProps = {
|
|
259
|
+
start: table.pageStart,
|
|
260
|
+
end: table.pageEnd,
|
|
261
|
+
rowCount: table.rowCount,
|
|
262
|
+
totalRowCount: table.totalRowCount,
|
|
263
|
+
selectedCount: table.selectedCount
|
|
264
|
+
};
|
|
265
|
+
const text = table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
|
|
266
|
+
return h(props.as, {
|
|
267
|
+
role: "status",
|
|
268
|
+
"aria-live": "polite"
|
|
269
|
+
}, slots.default?.(slotProps) ?? text);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/table.ts
|
|
276
|
+
/**
|
|
277
|
+
* Provides a table to the components inside it and renders the table element. Without a default
|
|
278
|
+
* slot it renders a complete header and body.
|
|
279
|
+
*/
|
|
280
|
+
const DataTableRoot = defineComponent({
|
|
281
|
+
name: "DataTableRoot",
|
|
282
|
+
slots: Object,
|
|
283
|
+
props: {
|
|
284
|
+
table: {
|
|
285
|
+
type: Object,
|
|
286
|
+
required: true
|
|
287
|
+
},
|
|
288
|
+
as: asProp("table")
|
|
289
|
+
},
|
|
290
|
+
setup(props, { slots }) {
|
|
291
|
+
provideDataTable(props.table);
|
|
292
|
+
return () => h(props.as, {
|
|
293
|
+
"aria-rowcount": props.table.rowCount + 1,
|
|
294
|
+
"aria-colcount": props.table.columns.length,
|
|
295
|
+
"data-empty": flag(props.table.rowCount === 0)
|
|
296
|
+
}, slots.default?.({ table: props.table }) ?? [h(DataTableHeader), h(DataTableBody)]);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const DataTableCaption = defineComponent({
|
|
300
|
+
name: "DataTableCaption",
|
|
301
|
+
slots: Object,
|
|
302
|
+
props: { as: asProp("caption") },
|
|
303
|
+
setup(props, { slots }) {
|
|
304
|
+
return () => h(props.as, slots.default?.());
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
/** The header section. Without a slot it renders one header cell per visible column. */
|
|
308
|
+
const DataTableHeader = defineComponent({
|
|
309
|
+
name: "DataTableHeader",
|
|
310
|
+
slots: Object,
|
|
311
|
+
props: { as: asProp("thead") },
|
|
312
|
+
setup(props, { slots }) {
|
|
313
|
+
const table = injectDataTable("<DataTableHeader>");
|
|
314
|
+
return () => h(props.as, slots.default?.({ columns: table.columns }) ?? h(DataTableHeaderRow, () => table.columns.map((column) => h(DataTableHeaderCell, {
|
|
315
|
+
key: column.id,
|
|
316
|
+
column
|
|
317
|
+
}))));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
const DataTableHeaderRow = defineComponent({
|
|
321
|
+
name: "DataTableHeaderRow",
|
|
322
|
+
slots: Object,
|
|
323
|
+
props: { as: asProp("tr") },
|
|
324
|
+
setup(props, { slots }) {
|
|
325
|
+
return () => h(props.as, { "aria-rowindex": 1 }, slots.default?.());
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
/**
|
|
329
|
+
* A column header with `aria-sort`. Without a slot it shows the header text inside a sort button
|
|
330
|
+
* for sortable columns.
|
|
331
|
+
*/
|
|
332
|
+
const DataTableHeaderCell = defineComponent({
|
|
333
|
+
name: "DataTableHeaderCell",
|
|
334
|
+
slots: Object,
|
|
335
|
+
props: {
|
|
336
|
+
column: columnProp,
|
|
337
|
+
as: asProp("th")
|
|
338
|
+
},
|
|
339
|
+
setup(props, { slots }) {
|
|
340
|
+
const table = injectDataTable("<DataTableHeaderCell>");
|
|
341
|
+
return () => {
|
|
342
|
+
const { column } = props;
|
|
343
|
+
const sort = table.getSort(column.id);
|
|
344
|
+
const ariaSort = sort ? sort.direction === "asc" ? "ascending" : "descending" : column.sortable ? "none" : void 0;
|
|
345
|
+
const content = slots.default?.({
|
|
346
|
+
column,
|
|
347
|
+
sort,
|
|
348
|
+
toggleSort: (multi = false) => table.toggleSort(column.id, { multi })
|
|
349
|
+
}) ?? (column.sortable ? h(DataTableSortButton, { column }, () => column.header) : column.header);
|
|
350
|
+
return h(props.as, {
|
|
351
|
+
scope: "col",
|
|
352
|
+
"aria-sort": ariaSort,
|
|
353
|
+
"data-column": column.id,
|
|
354
|
+
"data-align": column.align,
|
|
355
|
+
"data-sortable": flag(column.sortable),
|
|
356
|
+
"data-sort": sort?.direction,
|
|
357
|
+
style: columnStyle(column)
|
|
358
|
+
}, content);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
function columnStyle(column) {
|
|
363
|
+
const style = {};
|
|
364
|
+
if (column.width !== void 0) style["width"] = `${column.width}px`;
|
|
365
|
+
if (column.minWidth !== void 0) style["minWidth"] = `${column.minWidth}px`;
|
|
366
|
+
return Object.keys(style).length > 0 ? style : void 0;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Toggles a column's sort. Shift-click adds the column to the existing sort instead of replacing
|
|
370
|
+
* it. The slot receives the current sort so it can draw an indicator.
|
|
371
|
+
*/
|
|
372
|
+
const DataTableSortButton = defineComponent({
|
|
373
|
+
name: "DataTableSortButton",
|
|
374
|
+
slots: Object,
|
|
375
|
+
props: {
|
|
376
|
+
column: columnProp,
|
|
377
|
+
as: asProp("button")
|
|
378
|
+
},
|
|
379
|
+
setup(props, { slots }) {
|
|
380
|
+
const table = injectDataTable("<DataTableSortButton>");
|
|
381
|
+
return () => {
|
|
382
|
+
const sort = table.getSort(props.column.id);
|
|
383
|
+
const next = !sort ? "ascending" : sort.direction === "asc" ? "descending" : "unsorted";
|
|
384
|
+
return h(props.as, {
|
|
385
|
+
type: props.as === "button" ? "button" : void 0,
|
|
386
|
+
"data-sort": sort?.direction,
|
|
387
|
+
"aria-label": `${props.column.header}, sort ${next}`,
|
|
388
|
+
onClick: (event) => table.toggleSort(props.column.id, { multi: event.shiftKey })
|
|
389
|
+
}, [slots.default?.({ sort }) ?? props.column.header, slots.indicator?.({ sort })]);
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
/**
|
|
394
|
+
* The body section. Without a slot it renders a row per row on the page, and the `empty` slot in
|
|
395
|
+
* a full-width row when there are none.
|
|
396
|
+
*/
|
|
397
|
+
const DataTableBody = defineComponent({
|
|
398
|
+
name: "DataTableBody",
|
|
399
|
+
slots: Object,
|
|
400
|
+
props: { as: asProp("tbody") },
|
|
401
|
+
setup(props, { slots }) {
|
|
402
|
+
const table = injectDataTable("<DataTableBody>");
|
|
403
|
+
return () => {
|
|
404
|
+
let content = slots.default?.({ rows: table.rows });
|
|
405
|
+
if (!content) content = table.rows.length === 0 && slots.empty ? h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, table.columns.length) }, slots.empty())]) : table.rows.map((row) => h(DataTableRow, {
|
|
406
|
+
key: row.key,
|
|
407
|
+
row
|
|
408
|
+
}));
|
|
409
|
+
return h(props.as, content);
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
/** A body row; `aria-selected` and `data-selected` follow the selection. */
|
|
414
|
+
const DataTableRow = defineComponent({
|
|
415
|
+
name: "DataTableRow",
|
|
416
|
+
slots: Object,
|
|
417
|
+
props: {
|
|
418
|
+
row: rowProp,
|
|
419
|
+
as: asProp("tr")
|
|
420
|
+
},
|
|
421
|
+
setup(props, { slots }) {
|
|
422
|
+
const table = injectDataTable("<DataTableRow>");
|
|
423
|
+
return () => {
|
|
424
|
+
const { row } = props;
|
|
425
|
+
const selected = table.isSelected(row.key);
|
|
426
|
+
const position = table.rows.indexOf(row);
|
|
427
|
+
return h(props.as, {
|
|
428
|
+
"aria-rowindex": position === -1 ? void 0 : table.pageStart + position + 1,
|
|
429
|
+
"aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
|
|
430
|
+
"data-selected": flag(selected),
|
|
431
|
+
"data-key": String(row.key)
|
|
432
|
+
}, slots.default?.({
|
|
433
|
+
row,
|
|
434
|
+
selected,
|
|
435
|
+
columns: table.columns
|
|
436
|
+
}) ?? table.columns.map((column) => h(DataTableCell, {
|
|
437
|
+
key: column.id,
|
|
438
|
+
row,
|
|
439
|
+
column
|
|
440
|
+
})));
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
/** A body cell. The slot receives the raw value and the formatted text. */
|
|
445
|
+
const DataTableCell = defineComponent({
|
|
446
|
+
name: "DataTableCell",
|
|
447
|
+
slots: Object,
|
|
448
|
+
props: {
|
|
449
|
+
row: rowProp,
|
|
450
|
+
column: columnProp,
|
|
451
|
+
as: asProp("td")
|
|
452
|
+
},
|
|
453
|
+
setup(props, { slots }) {
|
|
454
|
+
return () => {
|
|
455
|
+
const { row, column } = props;
|
|
456
|
+
const value = row.getValue(column.id);
|
|
457
|
+
const display = row.getDisplay(column.id);
|
|
458
|
+
return h(props.as, {
|
|
459
|
+
"data-column": column.id,
|
|
460
|
+
"data-align": column.align
|
|
461
|
+
}, slots.default?.({
|
|
462
|
+
row,
|
|
463
|
+
column,
|
|
464
|
+
value,
|
|
465
|
+
display
|
|
466
|
+
}) ?? display);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
/** Renders its slot only when no row passes the search and filters. */
|
|
471
|
+
const DataTableEmpty = defineComponent({
|
|
472
|
+
name: "DataTableEmpty",
|
|
473
|
+
slots: Object,
|
|
474
|
+
props: { as: asProp("div") },
|
|
475
|
+
setup(props, { slots }) {
|
|
476
|
+
const table = injectDataTable("<DataTableEmpty>");
|
|
477
|
+
return () => table.rowCount === 0 ? h(props.as, { role: "status" }, slots.default?.()) : null;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/grid.ts
|
|
483
|
+
const gridContextKey = Symbol("vueye-table-grid-context");
|
|
484
|
+
function useGrid(consumer) {
|
|
485
|
+
const grid = injectDataGrid();
|
|
486
|
+
const context = inject(gridContextKey, void 0);
|
|
487
|
+
if (!grid || !context) throw new Error(`${consumer} must be rendered inside <DataGridRoot>.`);
|
|
488
|
+
return {
|
|
489
|
+
grid,
|
|
490
|
+
context
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function cellId(gridId, row, column) {
|
|
494
|
+
return `${gridId}-r${row}-c${column}`;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* A spreadsheet grid over a table. The grid element keeps keyboard focus and points at the active
|
|
498
|
+
* cell with `aria-activedescendant`; arrows, Tab, Home, End, Enter, F2, Delete, Escape, typing,
|
|
499
|
+
* undo, redo, and clipboard copy, cut, and paste all work on the selected range.
|
|
500
|
+
*/
|
|
501
|
+
const DataGridRoot = defineComponent({
|
|
502
|
+
name: "DataGridRoot",
|
|
503
|
+
slots: Object,
|
|
504
|
+
props: {
|
|
505
|
+
table: {
|
|
506
|
+
type: Object,
|
|
507
|
+
required: true
|
|
508
|
+
},
|
|
509
|
+
as: asProp("table"),
|
|
510
|
+
label: {
|
|
511
|
+
type: String,
|
|
512
|
+
default: void 0
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
setup(props, { slots, expose }) {
|
|
516
|
+
provideDataTable(props.table);
|
|
517
|
+
const grid = useDataGrid(props.table);
|
|
518
|
+
provideDataGrid(grid);
|
|
519
|
+
const element = ref();
|
|
520
|
+
const id = `vt-grid-${useId()}`;
|
|
521
|
+
const focus = () => {
|
|
522
|
+
nextTick(() => element.value?.focus());
|
|
523
|
+
};
|
|
524
|
+
provide(gridContextKey, {
|
|
525
|
+
id,
|
|
526
|
+
focus
|
|
527
|
+
});
|
|
528
|
+
expose({
|
|
529
|
+
grid,
|
|
530
|
+
focus
|
|
531
|
+
});
|
|
532
|
+
const onKeydown = (event) => {
|
|
533
|
+
if (event.target !== element.value) return;
|
|
534
|
+
if (grid.handleKey(event)) event.preventDefault();
|
|
535
|
+
};
|
|
536
|
+
const onCopy = (event, cut = false) => {
|
|
537
|
+
if (grid.editor || !grid.range || !event.clipboardData) return;
|
|
538
|
+
event.clipboardData.setData("text/plain", grid.copy());
|
|
539
|
+
event.preventDefault();
|
|
540
|
+
if (cut) grid.clear();
|
|
541
|
+
};
|
|
542
|
+
const onPaste = (event) => {
|
|
543
|
+
const text = event.clipboardData?.getData("text/plain");
|
|
544
|
+
if (grid.editor || text === void 0 || text === "") return;
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
grid.paste(text);
|
|
547
|
+
};
|
|
548
|
+
return () => {
|
|
549
|
+
const focusPosition = grid.selection?.focus;
|
|
550
|
+
return h(props.as, {
|
|
551
|
+
ref: element,
|
|
552
|
+
id,
|
|
553
|
+
role: "grid",
|
|
554
|
+
tabindex: 0,
|
|
555
|
+
"aria-label": props.label,
|
|
556
|
+
"aria-multiselectable": "true",
|
|
557
|
+
"aria-rowcount": props.table.rows.length + 1,
|
|
558
|
+
"aria-colcount": props.table.columns.length,
|
|
559
|
+
"aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
|
|
560
|
+
onKeydown,
|
|
561
|
+
onCopy: (event) => onCopy(event),
|
|
562
|
+
onCut: (event) => onCopy(event, true),
|
|
563
|
+
onPaste
|
|
564
|
+
}, slots.default?.({
|
|
565
|
+
table: props.table,
|
|
566
|
+
grid
|
|
567
|
+
}) ?? [h(DataTableHeader), h(DataGridBody)]);
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
/**
|
|
572
|
+
* The grid body: a row of cells per row on the page. The `cell` slot draws the content of every
|
|
573
|
+
* cell that is not being edited; the `rowHeader` slot adds a leading cell to each row.
|
|
574
|
+
*/
|
|
575
|
+
const DataGridBody = defineComponent({
|
|
576
|
+
name: "DataGridBody",
|
|
577
|
+
slots: Object,
|
|
578
|
+
props: { as: asProp("tbody") },
|
|
579
|
+
setup(props, { slots }) {
|
|
580
|
+
const table = injectDataTable("<DataGridBody>");
|
|
581
|
+
return () => h(props.as, slots.default?.({ rows: table.rows }) ?? table.rows.map((row, rowIndex) => h("tr", {
|
|
582
|
+
key: row.key,
|
|
583
|
+
role: "row",
|
|
584
|
+
"aria-rowindex": rowIndex + 2,
|
|
585
|
+
"data-key": String(row.key)
|
|
586
|
+
}, [slots.rowHeader?.({
|
|
587
|
+
row,
|
|
588
|
+
index: rowIndex
|
|
589
|
+
}), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
|
|
590
|
+
key: column.id,
|
|
591
|
+
row,
|
|
592
|
+
column,
|
|
593
|
+
rowIndex,
|
|
594
|
+
columnIndex
|
|
595
|
+
}, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))])));
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
/** The text box shown in a cell while it is edited. */
|
|
599
|
+
const DataGridEditor = defineComponent({
|
|
600
|
+
name: "DataGridEditor",
|
|
601
|
+
props: { label: {
|
|
602
|
+
type: String,
|
|
603
|
+
required: true
|
|
604
|
+
} },
|
|
605
|
+
setup(props) {
|
|
606
|
+
const { grid, context } = useGrid("<DataGridEditor>");
|
|
607
|
+
const input = ref();
|
|
608
|
+
onMounted(() => {
|
|
609
|
+
const element = input.value;
|
|
610
|
+
if (element) {
|
|
611
|
+
element.focus();
|
|
612
|
+
element.setSelectionRange(element.value.length, element.value.length);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
const finish = (then) => {
|
|
616
|
+
grid.commitEdit(then);
|
|
617
|
+
context.focus();
|
|
618
|
+
};
|
|
619
|
+
return () => h("input", {
|
|
620
|
+
ref: input,
|
|
621
|
+
value: grid.editor?.draft ?? "",
|
|
622
|
+
"aria-label": props.label,
|
|
623
|
+
"data-editor": "",
|
|
624
|
+
onInput: (event) => grid.updateDraft(event.target.value),
|
|
625
|
+
onBlur: () => {
|
|
626
|
+
if (grid.editor) grid.commitEdit();
|
|
627
|
+
},
|
|
628
|
+
onKeydown: (event) => {
|
|
629
|
+
if (event.key === "Enter") {
|
|
630
|
+
event.preventDefault();
|
|
631
|
+
finish(event.shiftKey ? "up" : "down");
|
|
632
|
+
} else if (event.key === "Tab") {
|
|
633
|
+
event.preventDefault();
|
|
634
|
+
finish(event.shiftKey ? "left" : "right");
|
|
635
|
+
} else if (event.key === "Escape") {
|
|
636
|
+
event.preventDefault();
|
|
637
|
+
grid.cancelEdit();
|
|
638
|
+
context.focus();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
/**
|
|
645
|
+
* A grid cell. A click selects it, Shift-click or dragging extends the range, and a double click
|
|
646
|
+
* edits it. The `editor` slot replaces the default text box.
|
|
647
|
+
*/
|
|
648
|
+
const DataGridCell = defineComponent({
|
|
649
|
+
name: "DataGridCell",
|
|
650
|
+
slots: Object,
|
|
651
|
+
props: {
|
|
652
|
+
row: rowProp,
|
|
653
|
+
column: columnProp,
|
|
654
|
+
as: asProp("td"),
|
|
655
|
+
/** The row's position on the page. Found by searching the rows when left out. */
|
|
656
|
+
rowIndex: {
|
|
657
|
+
type: Number,
|
|
658
|
+
default: void 0
|
|
659
|
+
},
|
|
660
|
+
/** The column's position among the visible columns. Found by searching when left out. */
|
|
661
|
+
columnIndex: {
|
|
662
|
+
type: Number,
|
|
663
|
+
default: void 0
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
setup(props, { slots }) {
|
|
667
|
+
const table = injectDataTable("<DataGridCell>");
|
|
668
|
+
const { grid, context } = useGrid("<DataGridCell>");
|
|
669
|
+
const element = ref();
|
|
670
|
+
const position = () => ({
|
|
671
|
+
row: props.rowIndex ?? table.rows.indexOf(props.row),
|
|
672
|
+
column: props.columnIndex ?? table.columns.indexOf(props.column)
|
|
673
|
+
});
|
|
674
|
+
watch(() => grid.isFocused(position()), (focused) => {
|
|
675
|
+
if (focused) element.value?.scrollIntoView?.({
|
|
676
|
+
block: "nearest",
|
|
677
|
+
inline: "nearest"
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
return () => {
|
|
681
|
+
const at = position();
|
|
682
|
+
const { row, column } = props;
|
|
683
|
+
const editing = grid.isEditing(at);
|
|
684
|
+
const editable = column.isEditable(row.original);
|
|
685
|
+
const value = row.getValue(column.id);
|
|
686
|
+
const display = row.getDisplay(column.id);
|
|
687
|
+
let content;
|
|
688
|
+
if (editing) content = slots.editor?.({
|
|
689
|
+
draft: grid.editor?.draft ?? "",
|
|
690
|
+
update: (draft) => grid.updateDraft(draft),
|
|
691
|
+
commit: (then) => grid.commitEdit(then),
|
|
692
|
+
cancel: () => grid.cancelEdit()
|
|
693
|
+
}) ?? h(DataGridEditor, { label: `Edit ${column.header}` });
|
|
694
|
+
else content = slots.default?.({
|
|
695
|
+
row,
|
|
696
|
+
column,
|
|
697
|
+
value,
|
|
698
|
+
display,
|
|
699
|
+
editable
|
|
700
|
+
}) ?? display;
|
|
701
|
+
return h(props.as, {
|
|
702
|
+
ref: element,
|
|
703
|
+
id: cellId(context.id, at.row, at.column),
|
|
704
|
+
role: "gridcell",
|
|
705
|
+
"aria-selected": String(grid.isSelected(at)),
|
|
706
|
+
"aria-readonly": editable ? void 0 : "true",
|
|
707
|
+
"aria-colindex": at.column + 1,
|
|
708
|
+
"data-column": column.id,
|
|
709
|
+
"data-align": column.align,
|
|
710
|
+
"data-focused": flag(grid.isFocused(at)),
|
|
711
|
+
"data-selected": flag(grid.isSelected(at)),
|
|
712
|
+
"data-editing": flag(editing),
|
|
713
|
+
"data-readonly": flag(!editable),
|
|
714
|
+
style: columnStyle(column),
|
|
715
|
+
onMousedown: (event) => {
|
|
716
|
+
if (editing || event.button !== 0) return;
|
|
717
|
+
grid.focusCell(at, { extend: event.shiftKey });
|
|
718
|
+
context.focus();
|
|
719
|
+
},
|
|
720
|
+
onMouseenter: (event) => {
|
|
721
|
+
if (event.buttons === 1 && !grid.editor) grid.focusCell(at, { extend: true });
|
|
722
|
+
},
|
|
723
|
+
onDblclick: () => {
|
|
724
|
+
grid.focusCell(at);
|
|
725
|
+
grid.startEdit();
|
|
726
|
+
}
|
|
727
|
+
}, content);
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
//#endregion
|
|
733
|
+
export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableEmpty, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, cellId, columnStyle };
|
|
734
|
+
//# sourceMappingURL=index.js.map
|