@excom/data-table 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/data-table.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/data-table.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/data-table.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/data-table.ts +396 -0
- package/data-th.ts +49 -0
- package/index.css +5 -0
- package/index.ts +23 -0
- package/package.json +44 -0
- package/rush-logs/data-table.apply-exports.cache.log +1 -0
- package/rush-logs/data-table.apply-exports.log +1 -0
- package/rush-logs/data-table.build_docs.cache.log +1 -0
- package/rush-logs/data-table.build_docs.log +1 -0
- package/rush-logs/data-table.build_package-metas.cache.log +1 -0
- package/rush-logs/data-table.build_package-metas.log +1 -0
- package/src/data-table.css +124 -0
- package/support/custom-elements.json +366 -0
- package/support/demos/filter.html +67 -0
- package/support/demos/simple.html +14 -0
- package/support/dist-docs/data-table.md +245 -0
- package/support/dist-docs/data-th.md +29 -0
- package/support/docs/INTERNAL.md +7 -0
- package/support/docs/README.md +74 -0
- package/support/package-meta.json +280 -0
- package/support/tests/data-table.test.ts +704 -0
- package/support/tests/filter.view.test.ts +33 -0
- package/support/tests/simple.view.test.ts +31 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/data-table.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type { DataTh } from "./data-th";
|
|
2
|
+
import type { DataThSortEvent } from "./data-th";
|
|
3
|
+
import { getChildren } from "@excom/kit-utils";
|
|
4
|
+
import { ConstructorType, Neutron, TEvent } from "@excom/neutron";
|
|
5
|
+
|
|
6
|
+
export type { DataThSortEvent };
|
|
7
|
+
|
|
8
|
+
type TDataThElement = typeof DataTh.CustomElement;
|
|
9
|
+
|
|
10
|
+
const determineSortNumber = (input: number) =>
|
|
11
|
+
input === 0 ? 0 : input > 0 ? 1 : -1;
|
|
12
|
+
|
|
13
|
+
const CELL_SORTER_MAP = {
|
|
14
|
+
asc: {
|
|
15
|
+
string: (a: string, b: string) => a.localeCompare(b),
|
|
16
|
+
number: (a: string, b: string) =>
|
|
17
|
+
determineSortNumber(parseFloat(a) - parseFloat(b)),
|
|
18
|
+
date: (a: string, b: string) =>
|
|
19
|
+
determineSortNumber(new Date(b).getTime() - new Date(a).getTime()),
|
|
20
|
+
},
|
|
21
|
+
desc: {
|
|
22
|
+
string: (a: string, b: string) => b.localeCompare(a),
|
|
23
|
+
number: (a: string, b: string) =>
|
|
24
|
+
determineSortNumber(parseFloat(b) - parseFloat(a)),
|
|
25
|
+
date: (a: string, b: string) =>
|
|
26
|
+
determineSortNumber(new Date(a).getTime() - new Date(b).getTime()),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Directions = keyof typeof CELL_SORTER_MAP;
|
|
31
|
+
type SortableTypes = keyof typeof CELL_SORTER_MAP.asc;
|
|
32
|
+
|
|
33
|
+
export type DataTableSortDetail = {
|
|
34
|
+
sortDirection: "asc" | "desc";
|
|
35
|
+
columnType: "string" | "number" | "date";
|
|
36
|
+
columnIndex: number;
|
|
37
|
+
sortFn: (a: string, b: string) => number;
|
|
38
|
+
rows: HTMLElement[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options the `--export` invoker carries as `data-*` attributes
|
|
43
|
+
* (`data-file-type="json"`, `data-file-name="people"`, `data-full`).
|
|
44
|
+
*/
|
|
45
|
+
export type DataTableExportOptions = {
|
|
46
|
+
full?: boolean;
|
|
47
|
+
fileType?: "csv" | "json";
|
|
48
|
+
fileName?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type DataTableSortEvent = TEvent & {
|
|
52
|
+
type: "data-table-sort";
|
|
53
|
+
detail: DataTableSortDetail;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Row counts and the active sort / filter of a `<data-table>`. */
|
|
57
|
+
export interface DataTableProvision {
|
|
58
|
+
/** `data-tbody > data-tr` rows in the DOM. */
|
|
59
|
+
totalRows: number;
|
|
60
|
+
/** Rows the filter leaves visible (`--data-tr-display` is not `none`). */
|
|
61
|
+
visibleRows: number;
|
|
62
|
+
/** Index of the active `data-th` among its siblings; `null` when unsorted. */
|
|
63
|
+
sortColumnIndex: number | null;
|
|
64
|
+
/** Direction of the active sort; `null` when unsorted. */
|
|
65
|
+
sortDirection: "asc" | "desc" | null;
|
|
66
|
+
/** The current `filter-value`; `null` when no filter is applied. */
|
|
67
|
+
filterValue: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isSameProvision = (
|
|
71
|
+
a: DataTableProvision | null | undefined,
|
|
72
|
+
b: DataTableProvision
|
|
73
|
+
) =>
|
|
74
|
+
!!a &&
|
|
75
|
+
a.totalRows === b.totalRows &&
|
|
76
|
+
a.visibleRows === b.visibleRows &&
|
|
77
|
+
a.sortColumnIndex === b.sortColumnIndex &&
|
|
78
|
+
a.sortDirection === b.sortDirection &&
|
|
79
|
+
a.filterValue === b.filterValue;
|
|
80
|
+
|
|
81
|
+
const ROW_CONVERSION_MAP = {
|
|
82
|
+
csv: (rows: HTMLElement[]): string => {
|
|
83
|
+
const csv: string[] = [];
|
|
84
|
+
for (let i = 0; i < rows.length; i++) {
|
|
85
|
+
const row: string[] = [];
|
|
86
|
+
const cols = rows[i].querySelectorAll(
|
|
87
|
+
"data-td, data-th"
|
|
88
|
+
) as NodeListOf<HTMLElement> | null;
|
|
89
|
+
if (cols) {
|
|
90
|
+
for (let j = 0; j < cols.length; j++) {
|
|
91
|
+
// Strip newlines / doubled spaces so CSV cells stay one line
|
|
92
|
+
let data = cols[j].innerText
|
|
93
|
+
.replace(/(\r\n|\n|\r)/gm, "")
|
|
94
|
+
.replace(/(\s\s)/gm, " ");
|
|
95
|
+
// CSV: escape `"` as `""`
|
|
96
|
+
data = data.replace(/"/g, '""');
|
|
97
|
+
// Quoted cell
|
|
98
|
+
row.push('"' + data + '"');
|
|
99
|
+
}
|
|
100
|
+
csv.push(row.join(","));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return csv.join("\n");
|
|
104
|
+
},
|
|
105
|
+
json: (rows: HTMLElement[]): string => {
|
|
106
|
+
const json: any[] = [];
|
|
107
|
+
const fields = [...rows[0].querySelectorAll("data-th")].map(
|
|
108
|
+
(th) => th.innerText
|
|
109
|
+
);
|
|
110
|
+
for (let i = 1; i < rows.length; i++) {
|
|
111
|
+
const row: any = {};
|
|
112
|
+
const cols = rows[i].querySelectorAll(
|
|
113
|
+
"data-td, data-th"
|
|
114
|
+
) as NodeListOf<HTMLElement> | null;
|
|
115
|
+
if (cols) {
|
|
116
|
+
for (let j = 0; j < cols.length; j++) {
|
|
117
|
+
row[fields[j]] = cols[j].innerText;
|
|
118
|
+
}
|
|
119
|
+
json.push(row);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return JSON.stringify(json, null, 2);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sortable, filterable table built from plain custom tags. `<data-table>`
|
|
128
|
+
* and `<data-th>` are the only registered elements — `<data-thead>`,
|
|
129
|
+
* `<data-tbody>`, `<data-tr>`, `<data-td>`, `<data-tfoot>`, and `<data-tf>`
|
|
130
|
+
* are inert structural tags styled by this package's CSS (or the
|
|
131
|
+
* equivalent `.tag-data-*` classes); they carry no behavior of their own.
|
|
132
|
+
* Sort / filter are visual only (CSS `order` / `display`) so row nodes
|
|
133
|
+
* stay put — Quark bindings and `iterate()` tables keep working.
|
|
134
|
+
*
|
|
135
|
+
* `.provision` holds the row counts and the active sort / filter
|
|
136
|
+
* (`{ totalRows, visibleRows, sortColumnIndex, sortDirection, filterValue }`)
|
|
137
|
+
* so a "12 of 40 rows" readout binds with `prop("provision")`.
|
|
138
|
+
*
|
|
139
|
+
* @summary Sortable / filterable data table — plain tags, one behavior owner.
|
|
140
|
+
*
|
|
141
|
+
* @descendant data-th - Sortable column header. Clicking toggles its
|
|
142
|
+
* `sort-direction` and fires `data-th-sort`, which becomes the active
|
|
143
|
+
* column.
|
|
144
|
+
* @descendant data-tbody - Required row container. Sorting and filtering
|
|
145
|
+
* both operate on its `data-tr` children.
|
|
146
|
+
* @descendant data-tr - Row, direct child of `data-tbody`. Gets
|
|
147
|
+
* `--data-tr-order` on sort and `--data-tr-display` on filter. DOM
|
|
148
|
+
* order is unchanged.
|
|
149
|
+
* @descendant data-td - Cell within a `data-tr`, read as sortable /
|
|
150
|
+
* export cell content.
|
|
151
|
+
*
|
|
152
|
+
* @listens data-th-sort - Bubbled up from a descendant `data-th` when its
|
|
153
|
+
* `sort-direction` changes. Sets that header as the active sort column
|
|
154
|
+
* (clearing `sort-direction` from the previously active one) and emits
|
|
155
|
+
* `data-table-sort`.
|
|
156
|
+
* @type DataThSortEvent
|
|
157
|
+
* @command --export - Builds a file from the table's `data-tr` / `data-td`
|
|
158
|
+
* / `data-th` text content and downloads it. Options are `data-*` on the
|
|
159
|
+
* invoker: `data-file-type` (`csv`, the default, or `json`),
|
|
160
|
+
* `data-file-name` (default `export_table_<locale-date>`), and `data-full`
|
|
161
|
+
* to download every row in DOM order instead of only the visible rows in
|
|
162
|
+
* visual sort order. Filtering needs no command: write `filter-value` /
|
|
163
|
+
* `filter-casing`.
|
|
164
|
+
*
|
|
165
|
+
* @fires data-table-sort - Cancelable. Dispatched when the active sort
|
|
166
|
+
* column / direction changes: on connect (if a `data-th` already has
|
|
167
|
+
* `sort-direction`) and after every `data-th-sort`. Call
|
|
168
|
+
* `preventDefault()` to take over sorting yourself.
|
|
169
|
+
* @type DataTableSortEvent
|
|
170
|
+
*
|
|
171
|
+
* @default-action data-table-sort - Sorts `event.detail.rows` by
|
|
172
|
+
* `columnIndex` with `sortFn` and sets `--data-tr-order` on each row
|
|
173
|
+
* (visual CSS `order` — DOM order is unchanged).
|
|
174
|
+
*/
|
|
175
|
+
export const DataTable = Neutron({
|
|
176
|
+
tag: "data-table",
|
|
177
|
+
props: {
|
|
178
|
+
// options
|
|
179
|
+
/**
|
|
180
|
+
* @option
|
|
181
|
+
* Hides `data-tr` rows (via `--data-tr-display: none`) whose text
|
|
182
|
+
* content doesn't include this value. Case-insensitive unless
|
|
183
|
+
* `filter-casing` is set. Unset / empty clears the filter. Rows stay
|
|
184
|
+
* in the DOM so Quark bindings survive.
|
|
185
|
+
*/
|
|
186
|
+
filterValue: String,
|
|
187
|
+
/**
|
|
188
|
+
* @option
|
|
189
|
+
* Match `filter-value` case-sensitively instead of the default
|
|
190
|
+
* case-insensitive comparison.
|
|
191
|
+
*/
|
|
192
|
+
filterCasing: Boolean,
|
|
193
|
+
/**
|
|
194
|
+
* @provision
|
|
195
|
+
* `{ totalRows, visibleRows, sortColumnIndex, sortDirection,
|
|
196
|
+
* filterValue }` — recomputed after connect, after the
|
|
197
|
+
* `data-table-sort` default action, and after every filter change.
|
|
198
|
+
* Not reflected as an attribute.
|
|
199
|
+
* @type DataTableProvision
|
|
200
|
+
*/
|
|
201
|
+
provision: Object as unknown as ConstructorType<DataTableProvision>,
|
|
202
|
+
// private state, not part of the public API
|
|
203
|
+
activeDataTh: {
|
|
204
|
+
type: HTMLElement,
|
|
205
|
+
store: "weak",
|
|
206
|
+
},
|
|
207
|
+
dataTbody: {
|
|
208
|
+
type: HTMLElement,
|
|
209
|
+
store: "weak",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
.defineMethods({
|
|
214
|
+
/**
|
|
215
|
+
* Rebuild `provision` from the DOM. New object only when a field
|
|
216
|
+
* changed, so Quark / `neutron-provision` listeners skip no-ops.
|
|
217
|
+
*/
|
|
218
|
+
_syncProvision: ({ dataTbody, activeDataTh, filterValue, provision }) => {
|
|
219
|
+
const rows = dataTbody
|
|
220
|
+
? ([...dataTbody.children] as HTMLElement[]).filter(
|
|
221
|
+
(child) => child.localName === "data-tr"
|
|
222
|
+
)
|
|
223
|
+
: [];
|
|
224
|
+
const next: DataTableProvision = {
|
|
225
|
+
totalRows: rows.length,
|
|
226
|
+
visibleRows: rows.filter(
|
|
227
|
+
(tr) => tr.style.getPropertyValue("--data-tr-display") !== "none"
|
|
228
|
+
).length,
|
|
229
|
+
sortColumnIndex: activeDataTh?.parentElement
|
|
230
|
+
? [...activeDataTh.parentElement.children].indexOf(activeDataTh)
|
|
231
|
+
: null,
|
|
232
|
+
// Same default as `fireSortEvent` when the header carries none
|
|
233
|
+
sortDirection: activeDataTh
|
|
234
|
+
? ((activeDataTh.getAttribute("sort-direction") ||
|
|
235
|
+
"desc") as Directions)
|
|
236
|
+
: null,
|
|
237
|
+
filterValue: filterValue || null,
|
|
238
|
+
};
|
|
239
|
+
return isSameProvision(provision, next) ? {} : { provision: next };
|
|
240
|
+
},
|
|
241
|
+
fireSortEvent: ({ activeDataTh, dataTbody }) => {
|
|
242
|
+
if (!activeDataTh || !dataTbody) return {};
|
|
243
|
+
const rows = getChildren(dataTbody).otherChildren;
|
|
244
|
+
if (!rows?.length || rows.length < 2) return {};
|
|
245
|
+
const sortDirection = (activeDataTh.getAttribute("sort-direction") ||
|
|
246
|
+
"desc") as Directions;
|
|
247
|
+
const columnType = (activeDataTh.getAttribute("column-type") ||
|
|
248
|
+
"string") as SortableTypes;
|
|
249
|
+
const sortFn = CELL_SORTER_MAP[sortDirection][columnType];
|
|
250
|
+
const columnIndex = [...activeDataTh.parentElement!.children].indexOf(
|
|
251
|
+
activeDataTh
|
|
252
|
+
);
|
|
253
|
+
return {
|
|
254
|
+
emit: [
|
|
255
|
+
"data-table-sort",
|
|
256
|
+
{
|
|
257
|
+
detail: {
|
|
258
|
+
// Attrs, not props: THs may not be connected yet when `onConnected` sorts
|
|
259
|
+
sortDirection,
|
|
260
|
+
// Nested tables would need `> data-thead > data-th` instead of this match
|
|
261
|
+
columnType,
|
|
262
|
+
columnIndex,
|
|
263
|
+
sortFn,
|
|
264
|
+
rows,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
.onConnected((element) => {
|
|
272
|
+
const activeDataTh = element.querySelector(
|
|
273
|
+
"data-th[sort-direction]"
|
|
274
|
+
) as unknown as TDataThElement;
|
|
275
|
+
const dataTbody = element.querySelector("data-tbody") as HTMLElement;
|
|
276
|
+
|
|
277
|
+
return [
|
|
278
|
+
{
|
|
279
|
+
...(dataTbody && { dataTbody }),
|
|
280
|
+
...(activeDataTh && { activeDataTh }),
|
|
281
|
+
},
|
|
282
|
+
...(activeDataTh ? [{ fireSortEvent: [] }] : []),
|
|
283
|
+
{ _syncProvision: [] },
|
|
284
|
+
];
|
|
285
|
+
})
|
|
286
|
+
.onEvent("data-th-sort", (_, e) => {
|
|
287
|
+
return [
|
|
288
|
+
{ activeDataTh: e.target as TDataThElement },
|
|
289
|
+
{ fireSortEvent: [] },
|
|
290
|
+
// Column / direction are known now, even if `data-table-sort` is later prevented
|
|
291
|
+
{ _syncProvision: [] },
|
|
292
|
+
];
|
|
293
|
+
})
|
|
294
|
+
.onPropChanged("activeDataTh", (_, previous) => {
|
|
295
|
+
previous?.activeDataTh?.removeAttribute("sort-direction");
|
|
296
|
+
})
|
|
297
|
+
.onEventDefault("data-table-sort", (_, e) => {
|
|
298
|
+
const { columnIndex, sortFn, rows } = e.detail;
|
|
299
|
+
|
|
300
|
+
if (rows?.length) {
|
|
301
|
+
([...rows] as HTMLElement[])
|
|
302
|
+
.toSorted((a, b) => {
|
|
303
|
+
return sortFn(
|
|
304
|
+
a?.children?.[columnIndex]?.textContent?.trim() ?? "",
|
|
305
|
+
b?.children?.[columnIndex]?.textContent?.trim() ?? ""
|
|
306
|
+
);
|
|
307
|
+
})
|
|
308
|
+
.forEach((tr, index) => {
|
|
309
|
+
tr.style.setProperty("--data-tr-order", (index + 1).toString());
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return { _syncProvision: [] };
|
|
313
|
+
})
|
|
314
|
+
.onCommand("--export", (element, { source }) => {
|
|
315
|
+
const options = exportOptions(source);
|
|
316
|
+
const { full } = options;
|
|
317
|
+
// Rows from this table
|
|
318
|
+
const fileType = options.fileType?.trim()?.toLowerCase() || "csv";
|
|
319
|
+
if (!(fileType in ROW_CONVERSION_MAP)) {
|
|
320
|
+
// no kit-logger dependency here; a bad option is an authoring error
|
|
321
|
+
console.error(
|
|
322
|
+
`data-table --export: invalid data-file-type "${fileType}" (csv | json)`
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
let rows = [
|
|
327
|
+
...(element.querySelectorAll("data-tr") as NodeListOf<HTMLElement>),
|
|
328
|
+
];
|
|
329
|
+
if ([null, undefined, false].includes(full)) {
|
|
330
|
+
rows = rows
|
|
331
|
+
.filter(
|
|
332
|
+
(row) => row.style.getPropertyValue("--data-tr-display") !== "none"
|
|
333
|
+
)
|
|
334
|
+
.toSorted((a, b) => {
|
|
335
|
+
const aVal = parseInt(
|
|
336
|
+
a.style.getPropertyValue("--data-tr-order") || "0"
|
|
337
|
+
);
|
|
338
|
+
const bVal = parseInt(
|
|
339
|
+
b.style.getPropertyValue("--data-tr-order") || "0"
|
|
340
|
+
);
|
|
341
|
+
return determineSortNumber(aVal - bVal);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Build CSV
|
|
345
|
+
const str = ROW_CONVERSION_MAP[fileType](rows);
|
|
346
|
+
// Download
|
|
347
|
+
const fileName =
|
|
348
|
+
options.fileName?.trim() ||
|
|
349
|
+
`export_table_${new Date().toLocaleDateString()}`;
|
|
350
|
+
const link = document.createElement("a");
|
|
351
|
+
link.style.display = "none";
|
|
352
|
+
link.setAttribute("target", "_blank");
|
|
353
|
+
link.setAttribute(
|
|
354
|
+
"href",
|
|
355
|
+
`data:text/${fileType};charset=utf-8,${encodeURIComponent(str)}`
|
|
356
|
+
);
|
|
357
|
+
link.setAttribute("download", `${fileName}.${fileType}`);
|
|
358
|
+
document.body.appendChild(link);
|
|
359
|
+
link.click();
|
|
360
|
+
document.body.removeChild(link);
|
|
361
|
+
})
|
|
362
|
+
.onPropChanged(["filterValue", "filterCasing"], (element) => {
|
|
363
|
+
const { filterValue, filterCasing } = element;
|
|
364
|
+
const trs = [
|
|
365
|
+
...element.querySelectorAll("data-tbody > data-tr"),
|
|
366
|
+
] as HTMLElement[];
|
|
367
|
+
if (filterValue) {
|
|
368
|
+
trs.forEach((tr) => {
|
|
369
|
+
const isHidden = !(filterCasing
|
|
370
|
+
? tr.innerText.replace(/\t/g, " ").includes(filterValue)
|
|
371
|
+
: tr.innerText
|
|
372
|
+
.toLowerCase()
|
|
373
|
+
.replace(/\t/g, " ")
|
|
374
|
+
.includes(filterValue.toLowerCase()));
|
|
375
|
+
if (isHidden) {
|
|
376
|
+
tr.style.setProperty("--data-tr-display", "none");
|
|
377
|
+
} else {
|
|
378
|
+
tr.style.removeProperty("--data-tr-display");
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
} else {
|
|
382
|
+
trs.forEach((tr) => tr.style.removeProperty("--data-tr-display"));
|
|
383
|
+
}
|
|
384
|
+
return { _syncProvision: [] };
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
/** `--export` options from the invoker's `data-*` (`data-full` = present and not `"false"`). */
|
|
388
|
+
function exportOptions(source: Element | null): DataTableExportOptions {
|
|
389
|
+
const data: DOMStringMap =
|
|
390
|
+
source instanceof HTMLElement ? source.dataset : {};
|
|
391
|
+
return {
|
|
392
|
+
fileType: data.fileType as DataTableExportOptions["fileType"],
|
|
393
|
+
fileName: data.fileName,
|
|
394
|
+
full: "full" in data && data.full !== "false",
|
|
395
|
+
};
|
|
396
|
+
}
|
package/data-th.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Neutron, TEvent } from "@excom/neutron";
|
|
2
|
+
|
|
3
|
+
export type DataThSortEvent = TEvent & {
|
|
4
|
+
type: "data-th-sort";
|
|
5
|
+
detail: void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sortable column header. Clicking toggles `sort-direction` between `asc`
|
|
10
|
+
* / `desc`; the parent `<data-table>` listens for the resulting
|
|
11
|
+
* `data-th-sort` event to drive sorting and to track which header is
|
|
12
|
+
* active. Renders no chrome of its own — style via `.tag-data-th` or the
|
|
13
|
+
* sort-direction indicators in `data-table`'s stylesheet.
|
|
14
|
+
*
|
|
15
|
+
* @summary Sortable column header — click toggles sort direction.
|
|
16
|
+
*
|
|
17
|
+
* @fires data-th-sort - Dispatched whenever `sort-direction` is set,
|
|
18
|
+
* whether by a click or programmatically. Bubbles to the parent
|
|
19
|
+
* `<data-table>`.
|
|
20
|
+
* @type DataThSortEvent
|
|
21
|
+
*/
|
|
22
|
+
export const DataTh = Neutron({
|
|
23
|
+
tag: "data-th",
|
|
24
|
+
props: {
|
|
25
|
+
/**
|
|
26
|
+
* @option
|
|
27
|
+
* Sort comparator to use for this column's cell values.
|
|
28
|
+
* `data-table` treats `null` as "string".
|
|
29
|
+
* @values string | number | date
|
|
30
|
+
*/
|
|
31
|
+
columnType: String,
|
|
32
|
+
/**
|
|
33
|
+
* @option
|
|
34
|
+
* @state
|
|
35
|
+
* Current sort direction. A click toggles between `asc` / `desc`;
|
|
36
|
+
* setting it (by any means) fires `data-th-sort`. Only one `data-th`
|
|
37
|
+
* per table should carry this at a time — the parent `<data-table>`
|
|
38
|
+
* clears the previously active header when a new one is set.
|
|
39
|
+
* @values asc | desc
|
|
40
|
+
*/
|
|
41
|
+
sortDirection: String,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
.onEvent("click", ({ sortDirection }) => ({
|
|
45
|
+
sortDirection: sortDirection === "asc" ? "desc" : "asc",
|
|
46
|
+
}))
|
|
47
|
+
.onPropSet("sortDirection", () => ({
|
|
48
|
+
emit: ["data-th-sort"],
|
|
49
|
+
}));
|
package/index.css
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { DataTable } from "./data-table";
|
|
2
|
+
import { DataTh } from "./data-th";
|
|
3
|
+
|
|
4
|
+
DataTable.define();
|
|
5
|
+
DataTh.define();
|
|
6
|
+
|
|
7
|
+
export { DataTable, DataTh };
|
|
8
|
+
|
|
9
|
+
type T_HTMLDataTableElement = typeof DataTable.CustomElement;
|
|
10
|
+
type T_HTMLDataThElement = typeof DataTh.CustomElement;
|
|
11
|
+
declare global {
|
|
12
|
+
interface HTMLDataTableElement extends T_HTMLDataTableElement {}
|
|
13
|
+
interface HTMLDataThElement extends T_HTMLDataThElement {}
|
|
14
|
+
interface Window {
|
|
15
|
+
HTMLDataTableElement: HTMLDataTableElement;
|
|
16
|
+
HTMLDataThElement: HTMLDataThElement;
|
|
17
|
+
}
|
|
18
|
+
interface HTMLElementTagNameMap {
|
|
19
|
+
"data-table": HTMLDataTableElement;
|
|
20
|
+
"data-th": HTMLDataThElement;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export type { HTMLDataTableElement, HTMLDataThElement };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/data-table",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "<data-table> custom element",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@excom/neutron": "^0.1.0",
|
|
12
|
+
"@excom/valence": "^0.1.0",
|
|
13
|
+
"@excom/kit-utils": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@excom/heft-rig": "^0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"url": "excom-dev/nucleus",
|
|
21
|
+
"directory": "packages/data-table"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/data-table/support/docs/README.md",
|
|
24
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"data-table",
|
|
27
|
+
"neutron",
|
|
28
|
+
"custom-elements"
|
|
29
|
+
],
|
|
30
|
+
"excom": {
|
|
31
|
+
"packageType": "kit-element"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
35
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
36
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
37
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
38
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
|
|
39
|
+
"dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
|
|
40
|
+
"preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
|
|
41
|
+
"build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
|
|
42
|
+
"build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|