@qnc/qnc_data_tables 1.1.0 → 1.3.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.
Files changed (66) hide show
  1. package/intermediate/qnc_data_tables_inline_css.js +2 -2
  2. package/package.json +5 -3
  3. package/dist/bound_stored_value.d.ts +0 -39
  4. package/dist/bound_stored_value.js +0 -134
  5. package/dist/bound_stored_value.ts +0 -158
  6. package/dist/column_manager.d.ts +0 -45
  7. package/dist/column_manager.js +0 -124
  8. package/dist/column_manager.ts +0 -160
  9. package/dist/column_resizing.d.ts +0 -3
  10. package/dist/column_resizing.js +0 -30
  11. package/dist/column_resizing.ts +0 -52
  12. package/dist/column_sorting.d.ts +0 -11
  13. package/dist/column_sorting.js +0 -53
  14. package/dist/column_sorting.ts +0 -63
  15. package/dist/conditionally_wrapped_element.d.ts +0 -5
  16. package/dist/conditionally_wrapped_element.js +0 -14
  17. package/dist/conditionally_wrapped_element.ts +0 -17
  18. package/dist/create_mithril_app.d.ts +0 -3
  19. package/dist/create_mithril_app.js +0 -25
  20. package/dist/create_mithril_app.ts +0 -35
  21. package/dist/create_style.d.ts +0 -4
  22. package/dist/create_style.js +0 -9
  23. package/dist/create_style.ts +0 -10
  24. package/dist/custom_element.d.ts +0 -23
  25. package/dist/custom_element.js +0 -63
  26. package/dist/custom_element.ts +0 -71
  27. package/dist/event_names.d.ts +0 -1
  28. package/dist/event_names.js +0 -1
  29. package/dist/event_names.ts +0 -1
  30. package/dist/index.d.ts +0 -5
  31. package/dist/index.js +0 -4
  32. package/dist/index.ts +0 -5
  33. package/dist/mithril_view.d.ts +0 -16
  34. package/dist/mithril_view.js +0 -493
  35. package/dist/mithril_view.ts +0 -1029
  36. package/dist/optional_storage.d.ts +0 -6
  37. package/dist/optional_storage.js +0 -18
  38. package/dist/optional_storage.ts +0 -23
  39. package/dist/overflow_class_manager.d.ts +0 -20
  40. package/dist/overflow_class_manager.js +0 -30
  41. package/dist/overflow_class_manager.ts +0 -40
  42. package/dist/prepare_cell_html.d.ts +0 -5
  43. package/dist/prepare_cell_html.js +0 -38
  44. package/dist/prepare_cell_html.ts +0 -48
  45. package/dist/renderer.d.ts +0 -16
  46. package/dist/renderer.js +0 -51
  47. package/dist/renderer.ts +0 -86
  48. package/dist/selection_fieldset_controller.d.ts +0 -9
  49. package/dist/selection_fieldset_controller.js +0 -85
  50. package/dist/selection_fieldset_controller.ts +0 -104
  51. package/dist/state_machine.d.ts +0 -68
  52. package/dist/state_machine.js +0 -334
  53. package/dist/state_machine.ts +0 -458
  54. package/dist/state_types.d.ts +0 -62
  55. package/dist/state_types.js +0 -1
  56. package/dist/state_types.ts +0 -84
  57. package/dist/table_manager.d.ts +0 -10
  58. package/dist/table_manager.js +0 -19
  59. package/dist/table_manager.ts +0 -32
  60. package/dist/table_options.d.ts +0 -177
  61. package/dist/table_options.js +0 -109
  62. package/dist/table_options.ts +0 -150
  63. package/dist/tsconfig.tsbuildinfo +0 -1
  64. package/dist/watched_mutable_value.d.ts +0 -15
  65. package/dist/watched_mutable_value.js +0 -23
  66. package/dist/watched_mutable_value.ts +0 -23
@@ -1,1029 +0,0 @@
1
- import m, { Children, redraw, Vnode, VnodeDOM, trust } from "mithril";
2
- import { AppState, SortState, SortStateInterpreter } from "./state_types.js";
3
- import { Renderer } from "./renderer.js";
4
- import { ExtraSortFunction } from "./table_options.js";
5
- import { ConfiguredColumns, ConfiguredColumn } from "./column_manager.js";
6
- import { enable } from "@qnc/drag_sorter2";
7
- import { begin_header_drag } from "./column_resizing.js";
8
- import { WatchedMutableValue } from "./watched_mutable_value.js";
9
-
10
- function classes(...values: (string | false | null)[]) {
11
- return values.filter(Boolean).join(" ");
12
- }
13
-
14
- type Updaters = {
15
- set_page_size: (page_size: number) => void;
16
- set_page_number: (page_number: number) => void;
17
- set_column_enabled: (column_key: string, enabled: boolean) => void;
18
- sort_by_function: (sort_function_key: string) => void;
19
- sort_by_column: (
20
- column_key: string,
21
- direction: "forward" | "reverse",
22
- ) => void;
23
- set_column_width: (column_key: string, width: number) => void;
24
- set_column_order: (keys: string[]) => void;
25
- set_selected: (pk: string, selected: boolean) => void;
26
- bulk_set_selected: (pks: Iterable<string>, selected: boolean) => void;
27
- };
28
-
29
- export function app_view(
30
- state: AppState<string>,
31
- renderer: Renderer,
32
- updaters: Updaters,
33
- ) {
34
- return [
35
- m("style", state.row_width_style_string.get()),
36
- top_control_bar({
37
- current_page: state.page_number,
38
- set_page: updaters.set_page_number,
39
- total_pages:
40
- state.accessible_result_count !== null
41
- ? Math.ceil(state.accessible_result_count / state.page_size)
42
- : null,
43
- page_size: state.page_size,
44
- set_page_size: updaters.set_page_size,
45
- result_count: state.result_count,
46
- result_count_loading: state.fetching_result_count,
47
- accessible_result_count: state.accessible_result_count,
48
- extra_sort_functions: state.extra_sort_functions,
49
- sort_by_function: updaters.sort_by_function,
50
- current_sort: state.current_sort,
51
- }),
52
- column_controls(state.columns, updaters, renderer),
53
- m("form", [
54
- render_with_loader(state.fetching_page_data, [
55
- table(
56
- state.columns,
57
- updaters,
58
- state.table_data,
59
- renderer,
60
- state.sort_state_interpreter,
61
- state.fill_last_page,
62
- state.page_size,
63
- state.table_key,
64
- state.include_selection_column,
65
- state.table_overflows_left,
66
- state.table_overflows_right,
67
- state.row_width_style_string,
68
- state.selected_pks,
69
- ),
70
- state.include_selection_column &&
71
- selection_controls(state, updaters),
72
- ]),
73
- ]),
74
- ];
75
- }
76
- type RowData = Readonly<
77
- [
78
- string, // row pk
79
- ReadonlyMap<string, string>, // column key => cell html
80
- ]
81
- >;
82
- function table(
83
- columns: ConfiguredColumns,
84
- updaters: Updaters,
85
- data: ReadonlyArray<RowData>,
86
- renderer: Renderer,
87
- sort_state_interpreter: SortStateInterpreter,
88
- fill_last_page: boolean,
89
- page_size: number,
90
- table_key: string,
91
- include_selection_column: boolean,
92
- table_overflows_left: WatchedMutableValue<boolean>,
93
- table_overflows_right: WatchedMutableValue<boolean>,
94
- row_width_style_string: WatchedMutableValue<string>,
95
- selected_pks: ReadonlySet<string>,
96
- ) {
97
- return m(
98
- "div",
99
- {
100
- class: classes(
101
- `qnc-data-table qnc-data-table-${CSS.escape(table_key)}`,
102
- table_overflows_left.get() && "qdt-overflows-left",
103
- table_overflows_right.get() && "qdt-overflows-right",
104
- ),
105
- role: "table",
106
- // style: all_columns().map(c=>`${column_width_property_name(c.key)}: ${c.width}px`).join(';')
107
- },
108
- m(
109
- "div",
110
- {
111
- class: "qnc-data-table-head",
112
- role: "rowgroup",
113
- },
114
- header_row({
115
- columns,
116
- updaters,
117
- sort_state_interpreter,
118
- table_key,
119
- include_selection_column,
120
- renderer,
121
- table_overflows_left,
122
- table_overflows_right,
123
- row_width_style_string,
124
- }),
125
- ),
126
- m(
127
- "div",
128
- {
129
- class: "qnc-data-table-body",
130
- role: "rowgroup",
131
- },
132
- table_rows(
133
- columns,
134
- data,
135
- fill_last_page,
136
- page_size,
137
- table_key,
138
- include_selection_column,
139
- updaters,
140
- selected_pks,
141
- ),
142
- ),
143
- );
144
- }
145
- type HeaderAttrs = {
146
- columns: ConfiguredColumns;
147
- updaters: Updaters;
148
- renderer: Renderer;
149
- sort_state_interpreter: SortStateInterpreter;
150
- table_key: string;
151
- include_selection_column: boolean;
152
- table_overflows_left: WatchedMutableValue<boolean>;
153
- table_overflows_right: WatchedMutableValue<boolean>;
154
- row_width_style_string: WatchedMutableValue<string>;
155
- };
156
-
157
- function HeaderComponent(initialVnode: Vnode) {
158
- let sticky_style_string = "";
159
-
160
- function setup_detector(
161
- detector: Element,
162
- watched_value: WatchedMutableValue<boolean>,
163
- ) {
164
- const table = detector.closest(".qnc-data-table");
165
- if (!table) throw new Error("could not find parent table element");
166
- new IntersectionObserver(
167
- function (entries, observer) {
168
- watched_value.set(!entries[0]?.isIntersecting);
169
- },
170
- {
171
- root: table,
172
- },
173
- ).observe(detector);
174
- }
175
-
176
- // Note: layout tightlty coupled to recompute_sticky_style_string
177
- function raw_header_row(
178
- columns: ConfiguredColumns,
179
- updaters: Updaters,
180
- renderer: Renderer,
181
- sort_state_interpreter: SortStateInterpreter,
182
- table_key: string,
183
- table_overflows_left: WatchedMutableValue<boolean>,
184
- table_overflows_right: WatchedMutableValue<boolean>,
185
- include_selection_column: boolean,
186
- ) {
187
- return m(
188
- "div",
189
- {
190
- role: "row",
191
- className: `qnc-data-table-row qnc-data-table-header-row qnc-data-table-row qnc-data-table-row-${CSS.escape(
192
- table_key,
193
- )}`,
194
- },
195
-
196
- // "overflow-left detector"
197
- m(
198
- "div",
199
- {
200
- role: "columnheader",
201
- style: "padding: 0; width: 0; border: none;",
202
- },
203
- m("div", {
204
- style: "width: 1px; height: 1px; background-color: transparent; pointerEvents: none;",
205
- oncreate: (vnode) => {
206
- setup_detector(vnode.dom, table_overflows_left);
207
- },
208
- }),
209
- ),
210
-
211
- include_selection_column && empty_checkbox_cell(table_key),
212
-
213
- columns.fixed_enabled_columns.map((c) =>
214
- header_cell(
215
- renderer,
216
- c,
217
- sort_state_interpreter,
218
- updaters,
219
- true,
220
- table_key,
221
- ),
222
- ),
223
- m("div", {
224
- role: "columnheader",
225
- class: `qdt-overflow-indicator-cell qdt-overflow-indicator-cell-left qnc-data-table-fixed-cell-${CSS.escape(
226
- table_key,
227
- )}`,
228
- }),
229
- columns.sortable_enabled_columns.map((c) =>
230
- header_cell(
231
- renderer,
232
- c,
233
- sort_state_interpreter,
234
- updaters,
235
- false,
236
- table_key,
237
- ),
238
- ),
239
- empty_expanding_cell("columnheader"),
240
- m("div", {
241
- role: "columnheader",
242
- class: `qdt-overflow-indicator-cell qdt-overflow-indicator-cell-right`,
243
- }),
244
-
245
- // "overflow-right detector"
246
- m(
247
- "div",
248
- {
249
- role: "columnheader",
250
- style: "padding: 0; width: 0; border: none; position: relative;",
251
- },
252
- m("div", {
253
- style: "position: absolute; right: 0; width: 1px; height: 1px; background-color: transparent; pointerEvents: none;",
254
- oncreate: (vnode) => {
255
- setup_detector(vnode.dom, table_overflows_right);
256
- },
257
- }),
258
- ),
259
- );
260
- }
261
- // Note: tightlty coupled to raw_header_row layout
262
- function recompute_sticky_style_string(
263
- header_element: Element,
264
- include_selection_column: boolean,
265
- columns: ConfiguredColumns,
266
- table_key: string,
267
- ) {
268
- const old_value = sticky_style_string;
269
- sticky_style_string = "";
270
- const fixed_column_count =
271
- (include_selection_column ? 1 : 0) +
272
- columns.fixed_enabled_columns.length +
273
- 1; // the "sticky divider" column, where box-shadow is rendered
274
- let previous_width = 0;
275
- const start_index = 1; // skip over the "scroll detector" column
276
- let i = start_index; // i will range over "element index" of all fixed column headers
277
- const end_index =
278
- start_index +
279
- fixed_column_count + // all fixed columns
280
- 1; // left overflow indicator cell
281
- while (i < end_index) {
282
- const cell = header_element.children[i];
283
- if (!cell) break;
284
-
285
- sticky_style_string += `.qnc-data-table-fixed-cell-${CSS.escape(
286
- table_key,
287
- )}:nth-child(${i + 1}) {
288
- position: sticky;
289
- left: ${previous_width}px;
290
- z-index: 2;
291
- }`;
292
- previous_width += cell.clientWidth;
293
- i++;
294
- }
295
- return sticky_style_string != old_value;
296
- }
297
-
298
- /*
299
- Rows need explicit min-width.
300
- Otherwise, they are only as wide as table, and cells overflow.
301
- This causes problems if:
302
- 1. there are any background colours/borders on rows
303
- 2. there are sticky columns, and row content is more than two table-widths wide (eventually, the sticky columns get pushed out view)
304
-
305
- #1. can be solved by putting all colours/borders on cells, rather than rows, but issue #2 still requires fixed row widths.
306
-
307
- While we _could_ compute the expected width based on columns.map(c => c.width), that approach would also require us to know the padding per cell (which might change based on media query). So let's just _measure_ the actual rendered width, instead.
308
-
309
- Note: this isn't foolproof, either. In hindsight, I'd have preferred to just calculate this based on column widths (perhaps setting `box-sizing: border-box` on them, so we wouldn't have to worry about padding).
310
- */
311
- function get_row_width_style(header: Element, table_key: string): string {
312
- let row_width = 0;
313
- let cell = header.firstElementChild;
314
-
315
- while (cell) {
316
- // skip the "empty expanding column"; otherwise, row width will never shrink, even when it should
317
- if (!cell.classList.contains("qdt-empty-expanding-cell")) {
318
- row_width += cell.clientWidth;
319
- }
320
- cell = cell.nextElementSibling;
321
- }
322
- return `.qnc-data-table-row-${CSS.escape(
323
- table_key,
324
- )} {min-width: ${row_width}px;}`;
325
- }
326
-
327
- return {
328
- oncreate(vnode: VnodeDOM) {
329
- const {
330
- columns,
331
- include_selection_column,
332
- table_key,
333
- row_width_style_string,
334
- } = vnode.attrs as HeaderAttrs;
335
- row_width_style_string.set(
336
- get_row_width_style(vnode.dom, table_key),
337
- );
338
- if (
339
- recompute_sticky_style_string(
340
- vnode.dom,
341
- include_selection_column,
342
- columns,
343
- table_key,
344
- )
345
- )
346
- redraw();
347
- },
348
- onupdate(vnode: VnodeDOM) {
349
- const {
350
- columns,
351
- include_selection_column,
352
- table_key,
353
- row_width_style_string,
354
- } = vnode.attrs as HeaderAttrs;
355
- row_width_style_string.set(
356
- get_row_width_style(vnode.dom, table_key),
357
- );
358
-
359
- // Optimization opportunity: we _could_ hash (column order, column widths, include_selection_count)
360
- // and return early whenever unchanged
361
- if (
362
- recompute_sticky_style_string(
363
- vnode.dom,
364
- include_selection_column,
365
- columns,
366
- table_key,
367
- )
368
- )
369
- redraw();
370
- },
371
- view(vnode: Vnode) {
372
- const {
373
- columns,
374
- updaters,
375
- renderer,
376
- sort_state_interpreter,
377
- table_key,
378
- table_overflows_left,
379
- table_overflows_right,
380
- include_selection_column,
381
- } = vnode.attrs as HeaderAttrs;
382
- return [
383
- raw_header_row(
384
- columns,
385
- updaters,
386
- renderer,
387
- sort_state_interpreter,
388
- table_key,
389
- table_overflows_left,
390
- table_overflows_right,
391
- include_selection_column,
392
- ),
393
- m("style", sticky_style_string),
394
- ];
395
- },
396
- };
397
- }
398
- function header_row(attrs: HeaderAttrs) {
399
- return (m as any)(HeaderComponent, attrs);
400
- }
401
-
402
- function header_cell(
403
- renderer: Renderer,
404
- column: ConfiguredColumn,
405
- sort_state_interpreter: SortStateInterpreter,
406
- updaters: Updaters,
407
- fixed: boolean,
408
- table_key: string,
409
- ) {
410
- const sort_direction = sort_state_interpreter(column.key);
411
- return m(
412
- "div",
413
- {
414
- role: "columnheader",
415
- style: {
416
- width: column.width + "px",
417
- },
418
- class: classes(
419
- "qdt-cell",
420
- "qdt-resizable-header",
421
- fixed && `qnc-data-table-fixed-cell-${CSS.escape(table_key)}`,
422
- ),
423
- },
424
-
425
- m(
426
- "div", // serves two purposes: it has "overflow: hidden;", while the parent element has "overflow: visible" (to allow resize handle to overflow); also allows us to use flexbox for alignment
427
- {
428
- style: {
429
- display: "flex",
430
- justifyContent: column.horizontal_alignment,
431
- alignItems: "center", // No strong opinion on default vertical alignment for headers; center seems reasonable
432
- },
433
- },
434
- renderer.render_header({
435
- label: renderer.render_with_optional_help_text(
436
- column.display,
437
- column.help_text,
438
- ),
439
- sortable: column.sortable,
440
- sorted: sort_direction || "no",
441
- set_sort: function (direction: "forward" | "reverse") {
442
- updaters.sort_by_column(column.key, direction);
443
- },
444
- }),
445
- ),
446
-
447
- m("span", {
448
- onmousedown: (e: MouseEvent) => {
449
- e.preventDefault(); // prevent text highlighting
450
-
451
- begin_header_drag(
452
- e,
453
- (width) => {
454
- updaters.set_column_width(column.key, width);
455
- m.redraw();
456
- },
457
- column.width,
458
- () => {
459
- m.redraw();
460
- },
461
- );
462
- },
463
- class: "qdt-column-resizer",
464
- }),
465
- );
466
- }
467
- function table_rows(
468
- columns: ConfiguredColumns,
469
- data: ReadonlyArray<RowData>,
470
- fill_last_page: boolean,
471
- page_size: number,
472
- table_key: string,
473
- include_selection_column: boolean,
474
- updaters: Updaters,
475
- selected_pks: ReadonlySet<string>,
476
- ) {
477
- return [
478
- data.map((row) =>
479
- table_row(
480
- row,
481
- columns,
482
- table_key,
483
- include_selection_column
484
- ? checkbox_cell(row[0], updaters, selected_pks, table_key)
485
- : null,
486
- ),
487
- ),
488
- fill_last_page &&
489
- Array.from(Array(Math.max(0, page_size - data.length))).map((x) =>
490
- raw_table_row(
491
- columns.fixed_enabled_columns.map((c) =>
492
- data_cell(c, null, true, table_key),
493
- ),
494
- columns.sortable_enabled_columns.map((c) =>
495
- data_cell(c, null, false, table_key),
496
- ),
497
- table_key,
498
- null,
499
- ),
500
- ),
501
- ];
502
- }
503
- function raw_table_row(
504
- fixed_cells: Children,
505
- sortable_cells: Children,
506
- table_key: string,
507
- checkbox_cell: Vnode | null,
508
- ) {
509
- return m(
510
- "div",
511
- {
512
- role: "row",
513
- className: `qnc-data-table-row qnc-data-table-body-row qnc-data-table-row qnc-data-table-row-${CSS.escape(
514
- table_key,
515
- )}`,
516
- },
517
-
518
- // Empty "scroll detector" column
519
- m("div", {
520
- role: "cell",
521
- style: "width: 0px; padding: 0; border: none;",
522
- }),
523
-
524
- checkbox_cell,
525
- // include_selection_column && checkbox_cell(pk, updaters, selected_pks),
526
-
527
- fixed_cells,
528
- m("div", {
529
- role: "cell",
530
- class: `qdt-overflow-indicator-cell qdt-overflow-indicator-cell-left qnc-data-table-fixed-cell-${CSS.escape(
531
- table_key,
532
- )}`,
533
- }),
534
- sortable_cells,
535
-
536
- empty_expanding_cell(),
537
-
538
- m("div", {
539
- role: "cell",
540
- class: `qdt-overflow-indicator-cell qdt-overflow-indicator-cell-right`,
541
- }),
542
-
543
- // Empty "overflow detector" column
544
- m("div", {
545
- role: "cell",
546
- style: "width: 0px; padding: 0; border: none;",
547
- }),
548
- );
549
- }
550
-
551
- // #coupled-cell-functions: these functions are tightly coupled. TODO: move to a separate module
552
- function data_cell(
553
- column: ConfiguredColumn,
554
- content: Children,
555
- fixed: boolean,
556
- table_key: string,
557
- ) {
558
- return m(
559
- "div",
560
- {
561
- role: "cell",
562
- style: {
563
- width: column.width + "px",
564
- "--qdt-cell-justify-content": column.horizontal_alignment,
565
- "--qdt-cell-align-items": column.data_vertical_alignment,
566
- },
567
- class: classes(
568
- "qdt-cell",
569
- fixed && `qnc-data-table-fixed-cell-${CSS.escape(table_key)}`,
570
- ),
571
- },
572
- content,
573
- );
574
- }
575
- // Note: layout tightly coupled to HeaderRow
576
- function table_row(
577
- row: readonly [string, ReadonlyMap<string, string>],
578
- columns: ConfiguredColumns,
579
- table_key: string,
580
- checkbox_cell: Vnode | null,
581
- ) {
582
- const [id, cell_map] = row;
583
-
584
- return raw_table_row(
585
- columns.fixed_enabled_columns.map((c) =>
586
- data_cell(c, trust(cell_map.get(c.key) || ""), true, table_key),
587
- ),
588
- columns.sortable_enabled_columns.map((c) =>
589
- data_cell(c, trust(cell_map.get(c.key) || ""), false, table_key),
590
- ),
591
- table_key,
592
- checkbox_cell,
593
- );
594
- }
595
-
596
- // #coupled-cell-functions: these functions are tightly coupled. TODO: move to a separate module
597
- function empty_checkbox_cell(table_key: string) {
598
- return m(
599
- "div",
600
- {
601
- role: "columnheader",
602
- style: { width: "auto" }, // This is the only auto-width column; it works because every row has the same content
603
- className: `qdt-cell qnc-data-table-fixed-cell-${CSS.escape(
604
- table_key,
605
- )}`,
606
- },
607
- // Render an invisible checkbox, so this header cell takes up the same width as the rest of the checkbox cells
608
- m(
609
- "label",
610
- { className: "qdt-row-checkbox-label qdt-flex-cell" },
611
- m("input", {
612
- type: "checkbox",
613
- style: {
614
- visibility: "hidden",
615
- pointerEvents: "none",
616
- },
617
- tabIndex: -1,
618
- }),
619
- ),
620
- );
621
- }
622
-
623
- // #coupled-cell-functions: these functions are tightly coupled. TODO: move to a separate module
624
- function checkbox_cell(
625
- pk: string,
626
- updaters: Updaters,
627
- selected_pks: ReadonlySet<string>,
628
- table_key: string,
629
- ) {
630
- return m(
631
- "div",
632
- {
633
- role: "cell",
634
- style: { width: "auto" }, // This is the only auto-width column; it works because every row has the same content
635
- className: `qdt-cell qnc-data-table-fixed-cell-${CSS.escape(
636
- table_key,
637
- )}`,
638
- },
639
- m(
640
- "label",
641
- { className: "qdt-row-checkbox-label qdt-flex-cell" },
642
- m("input", {
643
- type: "checkbox",
644
- checked: selected_pks.has(pk),
645
- onchange: (e: MouseEvent) => {
646
- updaters.set_selected(pk, !selected_pks.has(pk));
647
- // TODO: handle shift+clicking, like django_data_tables did
648
- },
649
- }),
650
- ),
651
- );
652
- }
653
- function empty_expanding_cell(role = "cell") {
654
- return m("div", {
655
- style: { flexGrow: "1", padding: "0", flexBasis: "0px" },
656
- class: "qdt-empty-expanding-cell",
657
- // Just in case users are using this in their selector for default cell background/border
658
- role: role,
659
- });
660
- }
661
-
662
- function extra_sort_function_widget(
663
- current_sort: SortState | null,
664
- extra_sort_functions: ExtraSortFunction[],
665
- sort_by_function: (function_key: string) => void,
666
- ) {
667
- // TODO: update checkbox-dropdown to work with radio buttons, publish on npm, document that it should always be included as a peer dependency
668
- // use that, so we can make help text smaller, and not show in "summary"
669
- const current_function_key =
670
- current_sort &&
671
- current_sort.type == "function" &&
672
- current_sort.function_key;
673
- return m(
674
- "label",
675
- "sort by: ",
676
- m(
677
- "select",
678
- {
679
- onchange: (e: Event) => {
680
- const key = (e.target as HTMLSelectElement).value;
681
- if (key) sort_by_function(key);
682
- },
683
- },
684
- current_sort == null && m("option", "-----"),
685
- current_sort &&
686
- current_sort.type == "column" &&
687
- m("option", "(selected column)"),
688
- extra_sort_functions.map((esf) =>
689
- m(
690
- "option",
691
- {
692
- selected: current_function_key == esf.key,
693
- value: esf.key,
694
- },
695
- esf.display,
696
- esf.help_text && ` (${esf.help_text})`,
697
- ),
698
- ),
699
- ),
700
- );
701
- }
702
-
703
- function top_control_bar(options: {
704
- current_page: number;
705
- set_page: (page: number) => void;
706
- total_pages: number | null;
707
- page_size: number;
708
- set_page_size: (size: number) => void;
709
- result_count: number | null;
710
- result_count_loading: boolean;
711
- accessible_result_count: number | null;
712
- sort_by_function: (function_key: string) => void;
713
- extra_sort_functions: ExtraSortFunction[];
714
- current_sort: SortState | null;
715
- }) {
716
- function space_horizontally(gap: string, items: m.Children[]) {
717
- return m(
718
- "div",
719
- { style: { overflow: "hidden" } },
720
- m(
721
- "div",
722
- { style: { display: "flex", marginRight: "-" + gap } },
723
- items.map((item) =>
724
- m("div", { style: { marginRight: gap } }, item, " "),
725
- ),
726
- ),
727
- );
728
- }
729
-
730
- const {
731
- current_page,
732
- set_page,
733
- total_pages,
734
- page_size,
735
- set_page_size,
736
- result_count,
737
- result_count_loading,
738
- accessible_result_count,
739
- } = options;
740
-
741
- return [
742
- space_horizontally("2em", [
743
- // Paginator
744
- [
745
- m(
746
- "button",
747
- {
748
- type: "button",
749
- disabled: current_page <= 1,
750
- onclick: (e: MouseEvent) => set_page(current_page - 1),
751
- },
752
- "<",
753
- ),
754
- " Page ",
755
- m("input", {
756
- type: "number",
757
- value: current_page,
758
- max: total_pages,
759
- style: { width: "3.5em" },
760
- onchange: function (e: Event) {
761
- set_page(
762
- parseInt((e.target as HTMLInputElement).value),
763
- );
764
- },
765
- }),
766
- " of ",
767
- total_pages ?? "?",
768
- " ",
769
- m(
770
- "button",
771
- {
772
- type: "button",
773
- disabled: current_page >= (total_pages ?? 1),
774
- onclick: (e: MouseEvent) => set_page(current_page + 1),
775
- },
776
- ">",
777
- ),
778
- ],
779
-
780
- // Results per page
781
- m(
782
- "label",
783
- m("input", {
784
- value: page_size,
785
- type: "number",
786
- style: { width: "3.5em" },
787
- onchange: function (e: Event) {
788
- set_page_size(
789
- parseInt((e.target as HTMLInputElement).value),
790
- );
791
- },
792
- }),
793
- " results per page",
794
- ),
795
-
796
- // Result count
797
- render_with_loader(
798
- result_count_loading,
799
- m("span", m("b", result_count), " result(s) total."),
800
- ),
801
-
802
- options.extra_sort_functions.length > 0 &&
803
- extra_sort_function_widget(
804
- options.current_sort,
805
- options.extra_sort_functions,
806
- options.sort_by_function,
807
- ),
808
- ]),
809
-
810
- m(
811
- "div",
812
- accessible_result_count !== null &&
813
- result_count !== null &&
814
- accessible_result_count < result_count &&
815
- m(
816
- "div",
817
- { style: { color: "red", fontWeight: "bold" } },
818
- m("sup", "*"),
819
- "The result table is limited to the first ",
820
- accessible_result_count,
821
- " results.",
822
- ),
823
- ),
824
- ];
825
- }
826
-
827
- function column_controls(
828
- columns: ConfiguredColumns,
829
- updaters: Updaters,
830
- renderer: Renderer,
831
- ) {
832
- return m(
833
- "details",
834
- { className: "qdt-column-details" },
835
- m("summary", "Columns"),
836
- m(
837
- "ol",
838
- {
839
- oncreate: (vnode) =>
840
- enable(vnode.dom as HTMLElement, {
841
- on_committed_move: (
842
- container: Element,
843
- dragged_element: Element,
844
- start_index: number,
845
- end_index: number,
846
- ) => {
847
- console.log("moved");
848
- updaters.set_column_order(
849
- Array.from(container.children).map(function (
850
- li: Element,
851
- ) {
852
- if (!(li instanceof HTMLElement))
853
- throw new Error(
854
- `unexpected child: ${li}`,
855
- );
856
- const value = li.dataset.key;
857
- if (typeof value == "undefined")
858
- throw new Error(
859
- "column li missing [data-key]",
860
- );
861
- return value;
862
- }),
863
- );
864
- },
865
- stationary_list_item_markers: true,
866
- handle_selector: ".handle",
867
- }),
868
- },
869
- columns.all_sortable_columns.map((c) =>
870
- m(
871
- "li",
872
- {
873
- "data-key": c.key,
874
- key: c.key,
875
- className: "qnc-data-table-column-item",
876
- },
877
- m(
878
- "label",
879
- m("input", {
880
- type: "checkbox",
881
- checked: c.enabled,
882
- onclick: (e: MouseEvent) =>
883
- updaters.set_column_enabled(c.key, !c.enabled),
884
- }),
885
- " ",
886
- c.display,
887
- " ",
888
- // don't wrap c.display in help text, because it's part of the drag handle
889
- c.help_text &&
890
- renderer.render_with_help_text("", c.help_text),
891
- " ",
892
- m(
893
- "span.handle",
894
- { style: { padding: "0 0.5em", cursor: "grab" } },
895
- "↕︎",
896
- ),
897
- ),
898
- ),
899
- ),
900
- ),
901
- );
902
- }
903
-
904
- // I'd like to replace this with some kind of <qdt-loading-container> (make a new package on npm) which is user-styleable
905
- // Alternatively (or as part of that work), we could make use of <qnc-spinner-uppercase>
906
- // https://tracker.quadrant.net/issues/271/
907
- function render_with_loader(loading: boolean, content: m.Children) {
908
- return m(
909
- "div",
910
- {
911
- style: {
912
- "--qdt-loading-spinner-size": "1em",
913
- minWidth: "var(--qdt-loading-spinner-size)",
914
- minHeight: "var(--qdt-loading-spinner-size)",
915
- position: "relative",
916
- isolation: "isolate", // force new stacking context
917
- },
918
- },
919
- // content wrapper
920
- m(
921
- "div",
922
- {
923
- style: {
924
- opacity: loading ? "50%" : "",
925
- },
926
- },
927
-
928
- content,
929
- ),
930
- // spinner
931
- m("div", {
932
- style: {
933
- display: loading ? "block" : "none",
934
- pointerEvents: "none",
935
- zIndex: 2147483647,
936
- position: "absolute",
937
- top: "calc(50% - calc(var(--qdt-loading-spinner-size)/2))",
938
- left: "calc(50% - calc(var(--qdt-loading-spinner-size)/2))",
939
- borderRadius: "1000px",
940
- border: "1px solid black",
941
- borderWidth: "calc(var(--qdt-loading-spinner-size)/2)",
942
- borderLeftColor: "white",
943
- borderRightColor: "white",
944
- opacity: "0.5",
945
- animation: "qdt-loader-spin 1.2s linear infinite",
946
- },
947
- }),
948
- );
949
- }
950
- function selection_controls(state: AppState<string>, updaters: Updaters) {
951
- const page_ids = state.table_data.map(([pk, cells]) => pk);
952
- const entire_page_selected = contains_all(
953
- state.selected_pks,
954
- new Set(page_ids),
955
- );
956
- const all_results_selected = contains_all(
957
- state.selected_pks,
958
- new Set(state.filtered_pks),
959
- );
960
-
961
- function clear_page() {
962
- updaters.bulk_set_selected(page_ids, false);
963
- }
964
- function select_page() {
965
- updaters.bulk_set_selected(page_ids, true);
966
- }
967
- function clear_table() {
968
- updaters.bulk_set_selected(state.filtered_pks, false);
969
- }
970
- function select_table() {
971
- updaters.bulk_set_selected(state.filtered_pks, true);
972
- }
973
- function clear_selection() {
974
- updaters.bulk_set_selected(state.selected_pks, false);
975
- }
976
-
977
- const selection_operators = [
978
- entire_page_selected
979
- ? m(
980
- "button",
981
- { onclick: clear_page, type: "button" },
982
- "Deselect Current Page",
983
- )
984
- : m(
985
- "button",
986
- { onclick: select_page, type: "button" },
987
- "Select Current Page",
988
- ),
989
- all_results_selected
990
- ? m(
991
- "button",
992
- { onclick: clear_table, type: "button" },
993
- "Deselect All Results",
994
- )
995
- : m(
996
- "button",
997
- { onclick: select_table, type: "button" },
998
- "Select All Results",
999
- ),
1000
- m(
1001
- "button",
1002
- {
1003
- onclick: clear_selection,
1004
- disabled: state.selected_pks.size == 0,
1005
- type: "button",
1006
- },
1007
- "Clear Selection",
1008
- ),
1009
- ];
1010
- return m("div.qdt-selection-controls", selection_operators);
1011
- }
1012
-
1013
- const contains_all = (Set.prototype as any).isSubsetOf
1014
- ? function <U>(
1015
- possible_superset: ReadonlySet<U>,
1016
- possible_subset: ReadonlySet<U>,
1017
- ): boolean {
1018
- // @ts-ignore
1019
- return possible_subset.isSubsetOf(possible_superset);
1020
- }
1021
- : function <U>(
1022
- possible_superset: ReadonlySet<U>,
1023
- possible_subset: ReadonlySet<U>,
1024
- ): boolean {
1025
- for (const value of possible_subset) {
1026
- if (!possible_superset.has(value)) return false;
1027
- }
1028
- return true;
1029
- };