@internetarchive/ads-table 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table-storybook.d.ts +6 -0
  2. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table-storybook.js +88 -0
  3. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table-storybook.js.map +1 -0
  4. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table.d.ts +53 -0
  5. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table.js +425 -0
  6. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/ads-table.js.map +1 -0
  7. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/index.d.ts +2 -0
  8. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/index.js +3 -0
  9. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/index.js.map +1 -0
  10. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/types.d.ts +27 -0
  11. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/types.js +88 -0
  12. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/dist/types.js.map +1 -0
  13. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/ads-table/tsconfig.tsbuildinfo +1 -0
  14. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/index.d.ts +1 -0
  15. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/index.js +2 -0
  16. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/index.js.map +1 -0
  17. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/sample-component.d.ts +7 -0
  18. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/sample-component.js +43 -0
  19. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/dist/sample-component.js.map +1 -0
  20. package/.rollup.cache/Users/jeffwklein/work/archive/ads-common/packages/sample-package/tsconfig.tsbuildinfo +1 -0
  21. package/dist/ads-table-storybook.d.ts +6 -0
  22. package/dist/ads-table.d.ts +53 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +659 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/types.d.ts +27 -0
  27. package/package.json +38 -0
  28. package/rollup.config.cjs +17 -0
  29. package/src/ads-table-storybook.ts +104 -0
  30. package/src/ads-table.ts +473 -0
  31. package/src/index.ts +14 -0
  32. package/src/types.ts +129 -0
  33. package/tsconfig.json +31 -0
  34. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,473 @@
1
+ // interface class for a Lit component class that contains table data
2
+ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
3
+ import {
4
+ SortComparisonFunction,
5
+ TableColumn,
6
+ TableDataType,
7
+ TableRow,
8
+ } from "./types";
9
+ import { property, state, query, customElement } from "lit/decorators.js";
10
+ import { EventHelpers } from "@jeffklein/ads-library";
11
+ import { getUserOS, UserOperatingSystem } from "@jeffklein/ads-library";
12
+
13
+ export abstract class AdsTable<T> extends LitElement {
14
+ @query("#main-table") tableElement: HTMLTableElement | undefined;
15
+
16
+ // base list of data that this class sorts
17
+ @property({ type: Array }) rows: TableRow<T>[] = [];
18
+
19
+ // abstract members to override
20
+ protected abstract columns: TableColumn<T>[];
21
+ protected abstract sortKey: keyof T | undefined;
22
+ protected abstract secondarySortKey: keyof T | undefined;
23
+
24
+ protected noDataText: string = "No items found";
25
+
26
+ protected readonly defaultSortDirection: "ascending" | "descending" =
27
+ "ascending";
28
+
29
+ @state() protected sortDirection: "ascending" | "descending" =
30
+ this.defaultSortDirection;
31
+
32
+ // list of selected rows in order of least to most recently added
33
+ @state() protected selectedRowIds: string[] = [];
34
+
35
+ @state() isLoading: boolean = false;
36
+
37
+ protected override updated(changedProperties: PropertyValues) {
38
+ super.updated(changedProperties);
39
+ if (changedProperties.has("selectedRowIds")) {
40
+ // report selected rows to parent via an event
41
+ this.emitEvent("row-select", { selectedRows: this.selectedRows });
42
+ }
43
+
44
+ // if rows change, re-ensure selected rows exist in table
45
+ if (this.rowsDidUpdate(changedProperties)) {
46
+ this.filterSelectedRowsToExistingRows();
47
+ }
48
+ }
49
+
50
+ // rather than compare references, determine if 'this.rows' has updated. an update occurs if:
51
+ // - row list is a different size
52
+ // - any elements in the sorted list may have changed place
53
+ protected rowsDidUpdate(changedProperties: PropertyValues): boolean {
54
+ // if the sort properties change, assume the elements have rearranged
55
+ if (
56
+ changedProperties.has("sortKey") ||
57
+ changedProperties.has("sortDirection")
58
+ ) {
59
+ return true;
60
+ }
61
+ if (!changedProperties.has("rows") || !changedProperties.get("rows")) {
62
+ return false;
63
+ }
64
+ const oldRowIds: string[] = (
65
+ changedProperties.get("rows") as TableRow<T>[]
66
+ ).map((row: TableRow<T>) => row.id);
67
+ // first check length. if list length changes, rows have changed.
68
+ if (oldRowIds.length !== this.rows.length) {
69
+ return true;
70
+ }
71
+ // otherwise, if they're the same size, compare individual items and ensure they're the same set of items
72
+ const newRowsIdsSet: Set<string> = new Set(this.rows.map((row) => row.id));
73
+ return !oldRowIds.every((id) => newRowsIdsSet.has(id));
74
+ }
75
+
76
+ protected emitEvent(eventName: string, detail = {}) {
77
+ this.dispatchEvent(
78
+ EventHelpers.createEvent(eventName, detail ? { detail } : {}),
79
+ );
80
+ }
81
+
82
+ protected get visibleColumns(): TableColumn<T>[] {
83
+ return this.columns.filter(({ isHidden }) => !isHidden);
84
+ }
85
+
86
+ protected get rowsById(): { [key: string]: TableRow<T> } {
87
+ return Object.fromEntries<TableRow<T>>(
88
+ this.rows.map((row) => [row.id, row]),
89
+ );
90
+ }
91
+
92
+ protected get sortedRows(): TableRow<T>[] {
93
+ const sortByPrimaryKey = this.getSortFunction(
94
+ this.sortColumnDataType,
95
+ this.sortDirection,
96
+ );
97
+ const sortBySecondaryKey = this.getSortFunction(
98
+ this.secondarySortDataType,
99
+ this.secondarySortDataType?.defaultSortDirection || this.sortDirection,
100
+ );
101
+ return this.rows.sort((rowA, rowB) => {
102
+ // sort by primary key: the selected column and direction
103
+ const sortResult = sortByPrimaryKey?.(rowA.data, rowB.data) || 0;
104
+ // if sort result on primary sort key is inconclusive, use secondary sort data type
105
+ if (sortResult === 0) {
106
+ return sortBySecondaryKey?.(rowA.data, rowB.data) || 0;
107
+ } else {
108
+ return sortResult;
109
+ }
110
+ });
111
+ }
112
+
113
+ protected get selectedRows(): TableRow<T>[] {
114
+ return this.selectedRowIds.map((id) => this.rowsById[id]).filter((x) => x);
115
+ }
116
+
117
+ // handler for when a user clicks on a column header
118
+ protected onColumnClick(
119
+ column: TableColumn<T>,
120
+ defaultSortDirection: "ascending" | "descending" = this
121
+ .defaultSortDirection,
122
+ ): void {
123
+ if (this.sortKey === column.key) {
124
+ // toggle the sort direction if the column you are clicking is already sorted
125
+ this.sortDirection =
126
+ this.sortDirection === "ascending" ? "descending" : "ascending";
127
+ } else {
128
+ // otherwise, sort by the column you clicked on, and in the default sort direction
129
+ this.sortKey = column.key;
130
+ this.sortDirection =
131
+ column.dataType.defaultSortDirection || defaultSortDirection;
132
+ }
133
+ }
134
+
135
+ // a map of column keys to their respective data types so we don't have to call .find everywhere
136
+ protected get columnKeyToDataType(): {
137
+ [columnKey: string]: TableDataType<T>;
138
+ } {
139
+ return Object.fromEntries<TableDataType<T>>(
140
+ this.columns.map((col) => [col.key, col.dataType]),
141
+ );
142
+ }
143
+
144
+ // data type of the currently sorted column
145
+ protected get sortColumnDataType(): TableDataType<T> | undefined {
146
+ if (this.sortKey) {
147
+ return this.columnKeyToDataType[this.sortKey as string];
148
+ } else {
149
+ return undefined;
150
+ }
151
+ }
152
+
153
+ // data type of the secondary sort column
154
+ protected get secondarySortDataType(): TableDataType<T> | undefined {
155
+ if (this.secondarySortKey) {
156
+ return this.columnKeyToDataType[this.secondarySortKey as string];
157
+ } else {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ // swaps the parameters of the sort function to reverse the sort direction
163
+ protected getSortFunction(
164
+ dataType: TableDataType<T> | undefined,
165
+ sortDirection: "ascending" | "descending",
166
+ ): SortComparisonFunction<T> | undefined {
167
+ if (!dataType) {
168
+ return undefined;
169
+ }
170
+ if (sortDirection === "ascending") {
171
+ // return the original comparison function
172
+ return dataType.compare;
173
+ } else if (dataType.compare !== undefined) {
174
+ // swap compare function parameters for descending
175
+ return (a, b) => dataType.compare?.(b, a) || 0;
176
+ } else {
177
+ return undefined;
178
+ }
179
+ }
180
+
181
+ protected renderArrowIcon(columnKey: keyof T): TemplateResult {
182
+ if (columnKey !== this.sortKey) {
183
+ return html``;
184
+ } else if (this.sortDirection === "ascending") {
185
+ return html`▲`;
186
+ } else {
187
+ return html`▼`;
188
+ }
189
+ }
190
+
191
+ protected get selectedRowIdsSet(): Set<string> {
192
+ return new Set(this.selectedRowIds);
193
+ }
194
+
195
+ protected get rowIdsSet(): Set<string> {
196
+ return new Set(this.rows.map((row) => row.id));
197
+ }
198
+
199
+ protected isSelected(row: TableRow<T>): boolean {
200
+ return this.selectedRowIdsSet.has(row.id);
201
+ }
202
+
203
+ protected toggleRowSelected(rowId: string): void {
204
+ if (this.selectedRowIdsSet.has(rowId)) {
205
+ // row is already selected: filter out clicked id
206
+ this.selectedRowIds = this.selectedRowIds.filter((id) => id !== rowId);
207
+ } else {
208
+ // row is not yet selected: append clicked id
209
+ this.selectedRowIds = [...this.selectedRowIds, rowId];
210
+ }
211
+ }
212
+
213
+ protected get lastSelectedRowId(): string {
214
+ return this.selectedRowIds[this.selectedRowIds.length - 1];
215
+ }
216
+
217
+ protected get indexOfLastSelectedRow(): number {
218
+ return this.rows.findIndex((row) => row.id === this.lastSelectedRowId);
219
+ }
220
+
221
+ // select rows in order from the last selected element up til the given index
222
+ protected groupRowSelect(rowIndex: number): void {
223
+ const getRowsToAdd = (): TableRow<T>[] => {
224
+ // get rows between index of last selected until index of selected row
225
+ if (rowIndex > this.indexOfLastSelectedRow) {
226
+ return this.rows.filter(
227
+ (_, i) => rowIndex >= i && i >= this.indexOfLastSelectedRow,
228
+ );
229
+ } else {
230
+ // reverse order of adding rows since clicked index is lower than last selected
231
+ return this.rows
232
+ .filter((_, i) => this.indexOfLastSelectedRow >= i && i >= rowIndex)
233
+ .reverse();
234
+ }
235
+ };
236
+ const rowIdsToAdd: string[] = getRowsToAdd().map((row) => row.id);
237
+ const rowIdsSet: Set<string> = new Set(rowIdsToAdd);
238
+ // first remove ids you are about to re-add, so they'll be in renewed order
239
+ this.selectedRowIds = this.selectedRowIds.filter(
240
+ (id) => !rowIdsSet.has(id),
241
+ );
242
+ // finally, add the new rows
243
+ this.selectedRowIds = [...this.selectedRowIds, ...rowIdsToAdd];
244
+ }
245
+
246
+ protected onRowClick(
247
+ event: MouseEvent,
248
+ clickedRow: TableRow<T>,
249
+ rowIndex: number,
250
+ ): void {
251
+ const userOs = getUserOS();
252
+ // meta (cmd) key for mac, control key for non-mac
253
+ const macHoldingHotKey =
254
+ userOs === UserOperatingSystem.MAC && event.metaKey;
255
+ const nonMacHoldingHotKey =
256
+ userOs !== UserOperatingSystem.MAC && event.ctrlKey;
257
+ // if holding meta on mac or ctrl on windows/linux, toggle individual row selection
258
+ if (macHoldingHotKey || nonMacHoldingHotKey) {
259
+ this.toggleRowSelected(clickedRow.id);
260
+ } else if (event.shiftKey) {
261
+ // if holding shift, select rows between this selection and the last selection
262
+ this.groupRowSelect(rowIndex);
263
+ } else {
264
+ // clicking a row will select just that row.
265
+ this.selectedRowIds = [clickedRow.id];
266
+ }
267
+ this.filterSelectedRowsToExistingRows();
268
+ // emit event so users can hook into this action
269
+ this.emitEvent("row-click", { row: clickedRow });
270
+ }
271
+
272
+ // filter selected down to rows that actually exist in the table
273
+ protected filterSelectedRowsToExistingRows() {
274
+ this.selectedRowIds = this.selectedRowIds.filter((id) =>
275
+ this.rowIdsSet.has(id),
276
+ );
277
+ }
278
+
279
+ protected onRowDoubleClick(clickedRow: TableRow<T>): void {
280
+ // emit event so users can hook into this event
281
+ this.emitEvent("row-double-click", { row: clickedRow });
282
+ }
283
+
284
+ // All keyboard events implemented through this method.
285
+ protected onKeyDown(event: KeyboardEvent): void {
286
+ switch (event.key) {
287
+ case "ArrowUp":
288
+ case "ArrowDown":
289
+ event.preventDefault();
290
+ // shift focus to table element when navigating with arrow keys
291
+ this.tableElement?.focus();
292
+ return this.onUpDownArrowKey(event.key);
293
+ }
294
+ }
295
+
296
+ protected onUpDownArrowKey(key: "ArrowUp" | "ArrowDown"): void {
297
+ if (this.selectedRowIds.length === 0) {
298
+ // select the first row if none are selected
299
+ this.selectedRowIds = [this.rows[0].id];
300
+ return;
301
+ }
302
+ // offset in the proper direction of the arrow
303
+ const indexOffset = key === "ArrowUp" ? -1 : 1;
304
+ const newSelectedRowIndex = this.constrainIndex(
305
+ this.indexOfLastSelectedRow + indexOffset,
306
+ );
307
+ const newSelectedRowId = this.rows[newSelectedRowIndex].id;
308
+ this.selectedRowIds = [newSelectedRowId];
309
+ }
310
+
311
+ // ensures index values are kept to the closest in-bounds index
312
+ protected constrainIndex(index: number): number {
313
+ return Math.max(Math.min(index, this.rows.length - 1), 0);
314
+ }
315
+
316
+ connectedCallback() {
317
+ super.connectedCallback();
318
+ // attach keyboard listener
319
+ window.addEventListener("keydown", (e: KeyboardEvent) => this.onKeyDown(e));
320
+ }
321
+
322
+ render() {
323
+ return html`
324
+ <table id="main-table" tabindex="0">
325
+ <thead>
326
+ <tr>
327
+ ${this.visibleColumns.map(
328
+ (column) => html`
329
+ <th
330
+ @click=${() => this.onColumnClick(column)}
331
+ class=${column.dataType.compare ? "sortable" : ""}
332
+ style=${`flex: ${column.flexRatio}`}
333
+ >
334
+ ${column.label}
335
+ ${column.dataType.compare
336
+ ? html`<span>${this.renderArrowIcon(column.key)}</span>`
337
+ : ""}
338
+ </th>
339
+ `,
340
+ )}
341
+ </tr>
342
+ </thead>
343
+ <tbody>
344
+ ${!this.isLoading
345
+ ? this.sortedRows.map(
346
+ (row, index) => html`
347
+ <tr
348
+ @click=${(e: MouseEvent) => this.onRowClick(e, row, index)}
349
+ @dblclick=${() => this.onRowDoubleClick(row)}
350
+ class=${this.isSelected(row) ? "row-selected" : ""}
351
+ data-row-selected=${this.isSelected(row)}
352
+ data-id=${row.id}
353
+ >
354
+ ${this.visibleColumns.map(
355
+ (column) => html`
356
+ <td style=${`flex: ${column.flexRatio}`}>
357
+ ${column.dataType.format(row.data)}
358
+ </td>
359
+ `,
360
+ )}
361
+ </tr>
362
+ `,
363
+ )
364
+ : html`
365
+ <tr>
366
+ <td class="no-data">Loading...</td>
367
+ </tr>
368
+ `}
369
+ ${!this.isLoading && this.sortedRows.length === 0
370
+ ? html`
371
+ <tr>
372
+ <td class="no-data">${this.noDataText}</td>
373
+ </tr>
374
+ `
375
+ : null}
376
+ </tbody>
377
+ </table>
378
+ `;
379
+ }
380
+
381
+ static styles = css`
382
+ table {
383
+ display: flex;
384
+ flex-direction: column;
385
+ user-select: none;
386
+ border-collapse: collapse;
387
+ }
388
+
389
+ thead,
390
+ tbody {
391
+ display: flex;
392
+ flex-direction: column;
393
+ }
394
+
395
+ thead {
396
+ background: #2a282c;
397
+ }
398
+
399
+ tbody {
400
+ scrollbar-gutter: stable;
401
+ }
402
+
403
+ tr {
404
+ display: flex;
405
+ width: 100%;
406
+ min-height: 40px;
407
+ flex-shrink: 0;
408
+ }
409
+
410
+ tr.row-selected {
411
+ background: #ccddf2;
412
+ }
413
+
414
+ td.no-data {
415
+ font-style: italic;
416
+ justify-content: center;
417
+ }
418
+
419
+ td,
420
+ th {
421
+ display: flex;
422
+ flex: 1;
423
+ justify-content: flex-start;
424
+ align-items: center;
425
+ padding: 0 12px;
426
+ height: 40px;
427
+
428
+ overflow: hidden;
429
+ text-overflow: ellipsis;
430
+ white-space: nowrap;
431
+ min-width: 0;
432
+ }
433
+
434
+ th {
435
+ color: white;
436
+ }
437
+
438
+ th.sortable {
439
+ cursor: pointer;
440
+ }
441
+
442
+ th.sortable:hover {
443
+ background: #525252;
444
+ }
445
+
446
+ th span {
447
+ padding-left: 5px;
448
+ }
449
+
450
+ td {
451
+ border-bottom: 1px solid #bbbbbb;
452
+ }
453
+ `;
454
+ }
455
+
456
+ // a simple, generic, out-of-the box implementation of AdsTable
457
+ @customElement("ads-simple-table")
458
+ export class AdsSimpleTable<T> extends AdsTable<T> {
459
+ // columns are exposed as a parameter like rows, not a class member to implement
460
+ @property({ type: Array }) columns: TableColumn<T>[] = [];
461
+
462
+ @property({ type: String }) secondarySortKey: keyof T | undefined = undefined;
463
+
464
+ // sort key is automatically held in state
465
+ @state() sortKey: keyof T | undefined = this.columns[0]?.key;
466
+
467
+ protected override updated(changedProperties: PropertyValues) {
468
+ super.updated(changedProperties);
469
+ if (changedProperties.has("columns")) {
470
+ this.sortKey = this.columns[0]?.key;
471
+ }
472
+ }
473
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { AdsTable, AdsSimpleTable } from "./ads-table";
2
+ export {
3
+ TableDataType,
4
+ TableColumn,
5
+ TableRow,
6
+ StringDataType,
7
+ DateDataType,
8
+ BytesDataType,
9
+ NumberDataType,
10
+ SortComparisonFunction,
11
+ SortComparisonResult,
12
+ NonSortable,
13
+ UndefinedHelpers,
14
+ } from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,129 @@
1
+ // T is the type you will be comparing and formatting
2
+ import { TemplateResult } from "lit";
3
+ import { formatDate, humanBytes } from "@jeffklein/ads-library";
4
+
5
+ export type SortComparisonResult = -1 | 0 | 1;
6
+ export type SortComparisonFunction<T> = (a: T, b: T) => SortComparisonResult;
7
+
8
+ export interface TableDataType<T> {
9
+ compare: SortComparisonFunction<T> | undefined;
10
+ format: (value: T) => string | TemplateResult;
11
+ // will override default sort direction if provided
12
+ defaultSortDirection?: "ascending" | "descending";
13
+ }
14
+
15
+ // Each table column extends a data type associated with it
16
+ export interface TableColumn<T> {
17
+ key: keyof T;
18
+ label: string;
19
+ dataType: TableDataType<T>;
20
+ // default is 1
21
+ flexRatio?: number;
22
+ // shown by default
23
+ isHidden?: boolean;
24
+ }
25
+
26
+ // for basic string comparison
27
+ export const StringDataType: TableDataType<string> = {
28
+ compare: (a: string, b: string): SortComparisonResult => {
29
+ if (a.toLowerCase() === b.toLowerCase()) {
30
+ return 0;
31
+ }
32
+ return a.toLowerCase() > b.toLowerCase() ? 1 : -1;
33
+ },
34
+ format(value: string): string | TemplateResult {
35
+ return value;
36
+ },
37
+ };
38
+
39
+ // for basic boolean comparison
40
+ export const BooleanDataType: TableDataType<boolean> = {
41
+ compare: (a: boolean, b: boolean): SortComparisonResult => {
42
+ if (a !== b) {
43
+ return a ? 1 : -1;
44
+ }
45
+ return 0;
46
+ },
47
+ format(value: boolean): string | TemplateResult {
48
+ return value.toString();
49
+ },
50
+ defaultSortDirection: "descending",
51
+ };
52
+
53
+ // for basic number comparison
54
+ export const NumberDataType: TableDataType<number> = {
55
+ compare: (a: number, b: number): SortComparisonResult => {
56
+ if (a === b) {
57
+ return 0;
58
+ }
59
+ return a > b ? 1 : -1;
60
+ },
61
+ format(value: number): string | TemplateResult {
62
+ return value.toFixed(0);
63
+ },
64
+ defaultSortDirection: "descending",
65
+ };
66
+
67
+ // for comparing and formatting dates
68
+ export const DateDataType: TableDataType<Date> = {
69
+ compare: (a: Date, b: Date): SortComparisonResult => {
70
+ return NumberDataType.compare?.(a.getTime(), b.getTime()) || 0;
71
+ },
72
+ format: formatDate,
73
+ defaultSortDirection: "descending",
74
+ };
75
+
76
+ // for comparing and formatting numbers that represent bytes
77
+ export const BytesDataType: TableDataType<number> = {
78
+ compare: NumberDataType.compare,
79
+ format: humanBytes,
80
+ };
81
+
82
+ // will return a DataType that extends the given DataType to handle undefined values
83
+ export const UndefinedHelpers = <T>(
84
+ dataType: TableDataType<T>,
85
+ ): TableDataType<T | undefined> => {
86
+ // if a compare function exists, extend it to handle potentially undefined inputs
87
+ return {
88
+ compare: dataType.compare
89
+ ? (a: T | undefined, b: T | undefined) => {
90
+ if (a && b) {
91
+ return dataType.compare?.(a, b) || 0;
92
+ } else if (a === b) {
93
+ // both are undefined
94
+ return 0;
95
+ }
96
+ // if a is defined, it's first, if not, b is defined and first
97
+ return a ? 1 : -1;
98
+ }
99
+ : undefined,
100
+ format(value: T | undefined): string | TemplateResult {
101
+ return value ? dataType.format(value) : "–";
102
+ },
103
+ };
104
+ };
105
+
106
+ // quickly define a wrapper around an interface key, applying sorting / formatting rules from a given type
107
+ export const FromKey = <S, T>(
108
+ targetDataType: TableDataType<S>,
109
+ key: keyof T,
110
+ ): TableDataType<T> => ({
111
+ ...targetDataType,
112
+ compare: (a: T, b: T) =>
113
+ targetDataType.compare?.(a[key] as S, b[key] as S) || 0,
114
+ format: (item: T) => targetDataType.format(item[key] as S),
115
+ });
116
+
117
+ // will return a DataType to have an undefined comparison function, turning sorting off for this data type
118
+ export const NonSortable = <T>(
119
+ dataType: TableDataType<T>,
120
+ ): TableDataType<T> => ({
121
+ ...dataType,
122
+ compare: undefined,
123
+ });
124
+
125
+ export interface TableRow<T> {
126
+ data: T;
127
+ // row-specific unique identifier
128
+ id: string;
129
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "esnext",
5
+ "moduleResolution": "node",
6
+ "noEmitOnError": true,
7
+ "lib": ["es2021", "dom"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "experimentalDecorators": true,
12
+ "importHelpers": true,
13
+ "outDir": "dist",
14
+ "incremental": true,
15
+ "sourceMap": true,
16
+ "inlineSources": true,
17
+ "rootDir": "./src",
18
+ "baseUrl": ".",
19
+ "skipLibCheck": true,
20
+ "declaration": true,
21
+ "allowJs": true,
22
+ "noImplicitAny": true,
23
+ "plugins": [
24
+ {
25
+ "name": "ts-lit-plugin"
26
+ }
27
+ ]
28
+ },
29
+ "include": ["src/**/*.ts"],
30
+ "exclude": ["node_modules", "build", "dist", "*.md", "rollup.config.js"]
31
+ }