@qnc/qnc_data_tables 1.0.4 → 1.0.7-a

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 (68) hide show
  1. package/README.md +36 -0
  2. package/dist/bound_stored_value.d.ts +39 -0
  3. package/dist/bound_stored_value.js +134 -0
  4. package/dist/bound_stored_value.ts +158 -0
  5. package/dist/column_manager.d.ts +43 -0
  6. package/dist/column_manager.js +124 -0
  7. package/dist/column_manager.ts +158 -0
  8. package/dist/column_resizing.d.ts +3 -0
  9. package/dist/column_resizing.js +30 -0
  10. package/dist/column_resizing.ts +52 -0
  11. package/dist/column_sorting.d.ts +11 -0
  12. package/dist/column_sorting.js +53 -0
  13. package/dist/column_sorting.ts +63 -0
  14. package/dist/conditionally_wrapped_element.d.ts +5 -0
  15. package/dist/conditionally_wrapped_element.js +14 -0
  16. package/dist/conditionally_wrapped_element.ts +17 -0
  17. package/dist/create_mithril_app.d.ts +3 -0
  18. package/dist/create_mithril_app.js +25 -0
  19. package/dist/create_mithril_app.ts +35 -0
  20. package/dist/create_style.d.ts +4 -0
  21. package/dist/create_style.js +9 -0
  22. package/dist/create_style.ts +10 -0
  23. package/dist/custom_element.d.ts +23 -0
  24. package/dist/custom_element.js +63 -0
  25. package/dist/custom_element.ts +71 -0
  26. package/dist/event_names.d.ts +1 -0
  27. package/dist/event_names.js +1 -0
  28. package/dist/event_names.ts +1 -0
  29. package/dist/index.d.ts +5 -0
  30. package/dist/index.js +4 -0
  31. package/dist/index.ts +5 -0
  32. package/dist/mithril_view.d.ts +16 -0
  33. package/dist/mithril_view.js +484 -0
  34. package/dist/mithril_view.ts +1014 -0
  35. package/dist/optional_storage.d.ts +6 -0
  36. package/dist/optional_storage.js +18 -0
  37. package/dist/optional_storage.ts +23 -0
  38. package/dist/overflow_class_manager.d.ts +20 -0
  39. package/dist/overflow_class_manager.js +30 -0
  40. package/dist/overflow_class_manager.ts +40 -0
  41. package/dist/renderer.d.ts +16 -0
  42. package/dist/renderer.js +51 -0
  43. package/dist/renderer.ts +86 -0
  44. package/dist/selection_fieldset_controller.d.ts +9 -0
  45. package/dist/selection_fieldset_controller.js +85 -0
  46. package/dist/selection_fieldset_controller.ts +104 -0
  47. package/dist/state_machine.d.ts +67 -0
  48. package/dist/state_machine.js +316 -0
  49. package/dist/state_machine.ts +434 -0
  50. package/dist/state_types.d.ts +62 -0
  51. package/dist/state_types.js +1 -0
  52. package/dist/state_types.ts +84 -0
  53. package/dist/table_manager.d.ts +9 -0
  54. package/dist/table_manager.js +16 -0
  55. package/dist/table_manager.ts +28 -0
  56. package/dist/table_options.d.ts +164 -0
  57. package/dist/table_options.js +97 -0
  58. package/dist/table_options.ts +132 -0
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/dist/watched_mutable_value.d.ts +15 -0
  61. package/dist/watched_mutable_value.js +23 -0
  62. package/dist/watched_mutable_value.ts +23 -0
  63. package/intermediate/qnc_data_tables_inline_css.d.ts +2 -0
  64. package/intermediate/qnc_data_tables_inline_css.js +8 -0
  65. package/package.json +14 -5
  66. package/dist/qnc_data_tables.d.ts +0 -57
  67. package/dist/qnc_data_tables.js +0 -1136
  68. package/dist/qnc_data_tables_inline_css.js +0 -8
@@ -0,0 +1,434 @@
1
+ import { SELECTION_CHANGE } from "./event_names.js";
2
+ import { AppState, SortState } from "./state_types.js";
3
+ import { TableOptions, get_storage } from "./table_options.js";
4
+ import * as tu from "@qnc/type_utils";
5
+ import { ColumnManager } from "./column_manager.js";
6
+ import * as column_sorting from "./column_sorting.js";
7
+ import {
8
+ BoundStoredValue,
9
+ BoundStoredStringSet,
10
+ NUMBER_CONVERTER,
11
+ STRING_CONVERTER,
12
+ } from "./bound_stored_value.js";
13
+ import { WatchedMutableValue } from "./watched_mutable_value.js";
14
+
15
+ /**
16
+ The StateMachine is responsible for aggregating state from 3 sources of information:
17
+ - table configuration (eg. coming from JS or HTML attributes)
18
+ - saved user preferences
19
+ - data fetched from the backend
20
+
21
+ It is also responsible for:
22
+ - providing methods to adjust state
23
+ - fetching data from the back end
24
+ - automatically saving any changes of user-configurable "settings" for the table.
25
+ - dispatching custom events
26
+ */
27
+ export class StateMachine<cell_data_type> {
28
+ /** This represents all of the "user settings", EXCEPT those dealing with columns. We delegate the management of those settings to column_manager. */
29
+ private stored_values: ReturnType<typeof make_bound_stored_values>;
30
+ private onchange: () => void;
31
+ private result_count_xhr = new XMLHttpRequest();
32
+ private id_list_xhr = new XMLHttpRequest();
33
+ private table_data_xhr = new XMLHttpRequest();
34
+ private result_count: number | null = null;
35
+ private accessible_result_count: number | null = null;
36
+ private id_list: string[] = [];
37
+ private table_data: [string, Map<string, cell_data_type>][] = [];
38
+ private column_manager: ColumnManager;
39
+ private fetched_columns = new Set<string>();
40
+ private valid_sort_keys: Set<string>;
41
+ public after_render: (() => void) | null = null;
42
+ private table_overflows_left: WatchedMutableValue<boolean>;
43
+ private table_overflows_right: WatchedMutableValue<boolean>;
44
+ private row_width_style_string: WatchedMutableValue<string>;
45
+ constructor(
46
+ private options: TableOptions,
47
+ /** This is used to "prepare" cell html from the backend for your UI library of choice */
48
+ private prepare_cell: (
49
+ html: string,
50
+ column_key: string,
51
+ ) => cell_data_type,
52
+ private event_target: EventTarget,
53
+ /** Will be called any time any of our state changes */
54
+ onchange: (state: AppState<cell_data_type>) => void,
55
+ ) {
56
+ this.column_manager = new ColumnManager(
57
+ options.columns,
58
+ get_storage(options.layout_storage),
59
+ options.key_prefix + "_columns",
60
+ this.on_column_manager_change,
61
+ );
62
+ this.stored_values = make_bound_stored_values(options);
63
+ this.onchange = () => onchange(this.get_state());
64
+
65
+ this.table_overflows_left = new WatchedMutableValue(
66
+ false,
67
+ this.onchange,
68
+ );
69
+ this.table_overflows_right = new WatchedMutableValue(
70
+ false,
71
+ this.onchange,
72
+ );
73
+ this.row_width_style_string = new WatchedMutableValue(
74
+ "",
75
+ this.onchange,
76
+ );
77
+
78
+ this.setup_result_count_xhr(this.result_count_xhr);
79
+ this.setup_id_list_xhr(this.id_list_xhr);
80
+ this.setup_page_xhr(this.table_data_xhr);
81
+
82
+ this.valid_sort_keys = new Set();
83
+ for (const sort_function of options.extra_sort_functions) {
84
+ this.valid_sort_keys.add(
85
+ column_sorting.make_function_sort_key(sort_function.key),
86
+ );
87
+ }
88
+ for (const column of options.columns) {
89
+ if (!column.sortable) continue;
90
+ this.valid_sort_keys.add(
91
+ column_sorting.make_column_sort_key(column.key, "forward"),
92
+ );
93
+ this.valid_sort_keys.add(
94
+ column_sorting.make_column_sort_key(column.key, "reverse"),
95
+ );
96
+ }
97
+
98
+ this.fetch_data();
99
+ }
100
+
101
+ on_column_manager_change() {}
102
+
103
+ /** Get current app state. This will not be needed often, but you might want it for your initial rendering. */
104
+ get_state(): AppState<cell_data_type> {
105
+ return {
106
+ page_number: this.stored_values.page_number.value,
107
+ selected_pks: this.stored_values.selected_pks.value,
108
+ filtered_pks: this.id_list,
109
+ result_count: this.result_count,
110
+ page_limit: this.options.page_limit,
111
+ table_limit: this.options.table_limit,
112
+ fetching_page_data:
113
+ is_loading(this.id_list_xhr) || is_loading(this.table_data_xhr),
114
+ fetching_result_count: is_loading(this.result_count_xhr),
115
+ columns: this.column_manager,
116
+
117
+ // Note: now that we've added current_sort, this could be replaced
118
+ sort_state_interpreter: (column_key: string) =>
119
+ column_sorting.current_sort_direction(
120
+ column_key,
121
+ this.stored_values.sort_key.value,
122
+ ),
123
+
124
+ table_data: this.table_data,
125
+ fill_last_page: this.options.fill_last_page,
126
+ page_size: this.stored_values.page_size.value,
127
+ accessible_result_count: this.accessible_result_count,
128
+ fetching_accessible_result_count: is_loading(this.id_list_xhr),
129
+ table_key: this.options.key_prefix,
130
+ include_selection_column: this.options.allow_selection,
131
+ table_overflows_left: this.table_overflows_left,
132
+ table_overflows_right: this.table_overflows_right,
133
+ row_width_style_string: this.row_width_style_string,
134
+ current_sort: this.get_sort_state(),
135
+ extra_sort_functions: this.options.extra_sort_functions,
136
+ };
137
+ }
138
+ get_sort_state(): SortState | null {
139
+ try {
140
+ return column_sorting.parse_sort_key(
141
+ this.stored_values.sort_key.value,
142
+ );
143
+ } catch (e) {
144
+ console.error(e);
145
+ return null;
146
+ }
147
+ }
148
+
149
+ get selected_ids(): ReadonlySet<string> {
150
+ return this.stored_values.selected_pks.value;
151
+ }
152
+
153
+ private setup_result_count_xhr(xhr: XMLHttpRequest) {
154
+ xhr.onload = (e: ProgressEvent) => {
155
+ if (xhr.status != 200) {
156
+ log_error(xhr);
157
+ this.onchange();
158
+ return;
159
+ }
160
+
161
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
162
+ this.result_count = tu.require_number(data.count);
163
+ this.onchange();
164
+ };
165
+ xhr.onerror = () => {
166
+ log_error(xhr);
167
+ this.onchange();
168
+ };
169
+ }
170
+ private setup_id_list_xhr(xhr: XMLHttpRequest) {
171
+ xhr.onload = () => {
172
+ if (xhr.status != 200) {
173
+ log_error(xhr);
174
+ this.onchange();
175
+ return;
176
+ }
177
+
178
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
179
+ this.id_list = tu.require_array(data.ids).map(tu.require_string);
180
+ this.accessible_result_count = this.id_list.length;
181
+
182
+ this.onchange();
183
+ this.fetch_page();
184
+ };
185
+ xhr.onerror = () => {
186
+ log_error(xhr);
187
+ this.onchange();
188
+ };
189
+ }
190
+ private setup_page_xhr(xhr: XMLHttpRequest) {
191
+ xhr.onload = () => {
192
+ if (xhr.status != 200) {
193
+ log_error(xhr);
194
+ this.onchange();
195
+ return;
196
+ }
197
+
198
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
199
+ this.table_data = tu.require_array(data.rows).map((row) => {
200
+ if (!tu.is_2_tuple(row))
201
+ throw new Error("row data not an array");
202
+ const pk = tu.require_string(row[0]);
203
+ const cell_data = tu.require_array(row[1]);
204
+
205
+ const cells = new Map<string, cell_data_type>();
206
+
207
+ for (const column of cell_data) {
208
+ // const array = tu.require_array(column)
209
+ // column
210
+
211
+ const [column_key, html] = tu.require_2_tuple(column);
212
+
213
+ tu.assert_string(column_key);
214
+ tu.assert_string(html);
215
+
216
+ cells.set(column_key, this.prepare_cell(html, column_key));
217
+ }
218
+
219
+ return [pk, cells];
220
+ });
221
+
222
+ this.onchange();
223
+ };
224
+ xhr.onerror = () => {
225
+ log_error(xhr);
226
+ this.onchange();
227
+ };
228
+ }
229
+
230
+ private fetch_result_count() {
231
+ const data = get_filter_form_data(this.options.filter_form_id);
232
+ data.append(`${this.options.key_prefix}_action`, "get_result_count");
233
+
234
+ this.result_count_xhr.open("POST", this.options.api_url);
235
+ this.result_count_xhr.send(data);
236
+
237
+ this.onchange();
238
+ }
239
+ private fetch_id_list() {
240
+ const data = get_filter_form_data(this.options.filter_form_id);
241
+ data.append(`${this.options.key_prefix}_action`, "get_ids");
242
+ if (this.stored_values.sort_key.value) {
243
+ data.append(
244
+ `${this.options.key_prefix}_sort_key`,
245
+ this.stored_values.sort_key.value,
246
+ );
247
+ }
248
+
249
+ this.id_list_xhr.open("POST", this.options.api_url);
250
+ this.id_list_xhr.send(data);
251
+
252
+ this.onchange();
253
+ }
254
+ private fetch_page() {
255
+ // Note - most pages won't need data from filter_form to return results
256
+ // However, some pages MAY include controls in the filter_form which impact how the columns/data are generated
257
+ // It's part of our documented contract that we always submit filter_form data with each post request
258
+ const data = get_filter_form_data(this.options.filter_form_id);
259
+ data.append(`${this.options.key_prefix}_action`, "get_table_data");
260
+
261
+ const column_keys = this.column_manager.all_enabled_columns.map(
262
+ (c) => c.key,
263
+ );
264
+ this.fetched_columns = new Set(column_keys);
265
+
266
+ data.append(
267
+ `${this.options.key_prefix}_columns`,
268
+ JSON.stringify(column_keys),
269
+ );
270
+ const page_size = this.stored_values.page_size.value;
271
+ const start_index =
272
+ (this.stored_values.page_number.value - 1) * page_size;
273
+ data.append(
274
+ `${this.options.key_prefix}_ids`,
275
+ JSON.stringify(
276
+ this.id_list.slice(start_index, start_index + page_size),
277
+ ),
278
+ );
279
+
280
+ this.table_data_xhr.open("POST", this.options.api_url);
281
+ this.table_data_xhr.send(data);
282
+ }
283
+
284
+ private get num_pages() {
285
+ return Math.ceil(
286
+ this.id_list.length / this.stored_values.page_size.value,
287
+ );
288
+ }
289
+
290
+ fetch_data() {
291
+ this.fetch_result_count();
292
+ this.fetch_id_list();
293
+ }
294
+ set_page_size(page_size: number) {
295
+ this.stored_values.page_size.value = Math.min(
296
+ page_size,
297
+ this.options.page_limit,
298
+ );
299
+ this.onchange();
300
+ }
301
+ set_page_number(page_number: number) {
302
+ this.stored_values.page_number.value = Math.min(
303
+ page_number,
304
+ this.num_pages,
305
+ );
306
+ this.fetch_page();
307
+ this.onchange();
308
+ }
309
+ set_column_enabled(column_key: string, enabled: boolean) {
310
+ this.column_manager.set_enabled(column_key, enabled);
311
+ if (enabled && !this.fetched_columns.has(column_key)) this.fetch_page();
312
+ this.onchange();
313
+ }
314
+ set_sort_key(sort_key: string) {
315
+ console.assert(this.valid_sort_keys.has(sort_key));
316
+ this.stored_values.sort_key.value = sort_key;
317
+ this.fetch_data();
318
+ this.onchange();
319
+ }
320
+ sort_by_function(function_key: string) {
321
+ this.set_sort_key(column_sorting.make_function_sort_key(function_key));
322
+ }
323
+ sort_by_column(column_key: string, direction: "forward" | "reverse") {
324
+ this.set_sort_key(
325
+ column_sorting.make_column_sort_key(column_key, direction),
326
+ );
327
+ }
328
+ set_column_width(column_key: string, width: number) {
329
+ this.column_manager.set_width(column_key, width);
330
+ this.onchange();
331
+ }
332
+
333
+ /** keys may include all sortable columns, not just the enabled ones */
334
+ set_column_order(keys: string[]) {
335
+ this.column_manager.set_order(keys);
336
+ this.onchange();
337
+ }
338
+
339
+ set_selected(pk: string, selected: boolean) {
340
+ const set = this.stored_values.selected_pks;
341
+ if (selected) set.add(pk);
342
+ else set.delete(pk);
343
+ this.onchange();
344
+ this.dispatch_selection_change_event();
345
+ }
346
+
347
+ bulk_set_selected(pks: Iterable<string>, selected: boolean) {
348
+ const set = new Set(this.stored_values.selected_pks.value);
349
+ if (selected) {
350
+ for (const value of pks) {
351
+ set.add(value);
352
+ }
353
+ } else {
354
+ for (const value of pks) {
355
+ set.delete(value);
356
+ }
357
+ }
358
+ this.stored_values.selected_pks.value = set;
359
+ this.onchange();
360
+ this.dispatch_selection_change_event();
361
+ }
362
+
363
+ private dispatch_selection_change_event() {
364
+ this.event_target.dispatchEvent(new CustomEvent(SELECTION_CHANGE));
365
+ }
366
+ }
367
+
368
+ function is_loading(xhr: XMLHttpRequest) {
369
+ return xhr.readyState != 4;
370
+ }
371
+ function log_error(xhr: XMLHttpRequest) {
372
+ /*
373
+ Always log the error but don't alert when status == 0, statusText == "" and responseText == "" because Safari
374
+ returns those conditions when a request is interrupted by the user making another request (navigating away) and we don't want to show
375
+ an empty alert.
376
+ These conditions are also met when there is a network failure. Ideally, we _would_ alert the user to that case in some way, but I'm not sure how to differentiate between the two.
377
+ */
378
+ console.warn("qnc_data_tables xhr error: ", xhr);
379
+ if (xhr.status != 0 || xhr.statusText != "" || xhr.responseText != "") {
380
+ alert("Data table error: " + xhr.responseText || xhr.statusText);
381
+ }
382
+ }
383
+ function get_filter_form(id: string): HTMLFormElement | null {
384
+ if (!id) return null;
385
+ const form = document.getElementById(id);
386
+ if (!(form instanceof HTMLFormElement))
387
+ throw new Error(`ID "${id}" does not reference a <form> on this page`);
388
+ return form;
389
+ }
390
+ /** Return a new FormData associate with the filter form if one is specified, else a new empty FormData */
391
+ function get_filter_form_data(id: string): FormData {
392
+ const form = get_filter_form(id);
393
+ if (form) return new FormData(form);
394
+ return new FormData();
395
+ }
396
+
397
+ /**
398
+ Read TableOptions and make various BoundStoredValues that always save to correct storage "area" when updated
399
+
400
+ Note that this
401
+ */
402
+ function make_bound_stored_values(options: TableOptions) {
403
+ const { key_prefix } = options;
404
+
405
+ const selection_storage = get_storage(options.selection_storage);
406
+ const navigation_storage = get_storage(options.navigation_storage);
407
+ const layout_storage = get_storage(options.layout_storage);
408
+ const sort_storage = get_storage(options.sort_storage);
409
+
410
+ return {
411
+ page_number: new BoundStoredValue(
412
+ navigation_storage,
413
+ key_prefix + "_page",
414
+ NUMBER_CONVERTER,
415
+ 1,
416
+ ),
417
+ page_size: new BoundStoredValue(
418
+ layout_storage,
419
+ key_prefix + "_page_size",
420
+ NUMBER_CONVERTER,
421
+ options.page_size,
422
+ ),
423
+ selected_pks: new BoundStoredStringSet(
424
+ selection_storage,
425
+ key_prefix + "_selected",
426
+ ),
427
+ sort_key: new BoundStoredValue(
428
+ sort_storage,
429
+ key_prefix + "_sort",
430
+ STRING_CONVERTER,
431
+ column_sorting.encode_sort(options.default_sort),
432
+ ),
433
+ };
434
+ }
@@ -0,0 +1,62 @@
1
+ import { ExtraSortFunction } from "./table_options.js";
2
+ import { ConfiguredColumns } from "./column_manager.js";
3
+ import { WatchedMutableValue } from "./watched_mutable_value.js";
4
+ /**
5
+ A SortStateInterpreter takes a column key and identifies whether we are currently:
6
+ - sorting by that column, in the forward direction ('forward')
7
+ - sorting by that column, in the reverse direction ('reverse')
8
+ - not sorting by that column (null)
9
+ */
10
+ export type SortStateInterpreter = (column_key: string) => "forward" | "reverse" | null;
11
+ export type SortState = {
12
+ type: "function";
13
+ function_key: string;
14
+ } | {
15
+ type: "column";
16
+ column_key: string;
17
+ direction: "forward" | "reverse";
18
+ };
19
+ /**
20
+ This should contain EVERYTHING needed to render a non-interactive version of the UI, at a point in time. This includes:
21
+ - fixed values read from "configuration"
22
+ - user controlled settings
23
+ - data fetched from backend
24
+
25
+ AppState does NOT contain any callback functions (for updating state). Those are implemented by StateMachine.
26
+ Actually, AppState MAY store some simple callback/update functions, in the form of WatchedMutableValues.
27
+ That's really only intended for simple things that are computed from the rendered layout, and which only affect other aspects of the rendered layout (ie. no other logic will likely be needed in response to updating the value).
28
+ */
29
+ export type AppState<cell_data_type> = Readonly<{
30
+ /**
31
+ Must be unique across all data tables on a given page (used in constructing CSS classes).
32
+ */
33
+ table_key: string;
34
+ page_number: number;
35
+ selected_pks: ReadonlySet<string>;
36
+ filtered_pks: string[];
37
+ /** null means we have not yet fetched any result count; should only occur before first result_count becomes available */
38
+ result_count: number | null;
39
+ /** Will generally be the same as result_count. MAY be less, if backend decides it's too expensive to return that many ids. */
40
+ accessible_result_count: number | null;
41
+ page_limit: number;
42
+ table_limit: number;
43
+ fill_last_page: boolean;
44
+ page_size: number;
45
+ /** indicates that we are fetching the total result count; any current result_count value is "stale" */
46
+ fetching_result_count: boolean;
47
+ /** indicates that we are fetching the list of matching ids; any current accessible_result_count is "stale" */
48
+ fetching_accessible_result_count: boolean;
49
+ fetching_page_data: boolean;
50
+ columns: ConfiguredColumns;
51
+ sort_state_interpreter: (column_key: string) => "forward" | "reverse" | null;
52
+ current_sort: SortState | null;
53
+ extra_sort_functions: ExtraSortFunction[];
54
+ table_data: ReadonlyArray<Readonly<[
55
+ string,
56
+ ReadonlyMap<string, cell_data_type>
57
+ ]>>;
58
+ include_selection_column: boolean;
59
+ table_overflows_left: WatchedMutableValue<boolean>;
60
+ table_overflows_right: WatchedMutableValue<boolean>;
61
+ row_width_style_string: WatchedMutableValue<string>;
62
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,84 @@
1
+ import { ExtraSortFunction } from "./table_options.js";
2
+ import { ConfiguredColumns } from "./column_manager.js";
3
+ import { WatchedMutableValue } from "./watched_mutable_value.js";
4
+
5
+ /**
6
+ A SortStateInterpreter takes a column key and identifies whether we are currently:
7
+ - sorting by that column, in the forward direction ('forward')
8
+ - sorting by that column, in the reverse direction ('reverse')
9
+ - not sorting by that column (null)
10
+ */
11
+ export type SortStateInterpreter = (
12
+ column_key: string,
13
+ ) => "forward" | "reverse" | null;
14
+
15
+ export type SortState =
16
+ | {
17
+ type: "function";
18
+ function_key: string;
19
+ }
20
+ | {
21
+ type: "column";
22
+ column_key: string;
23
+ direction: "forward" | "reverse";
24
+ };
25
+
26
+ /**
27
+ This should contain EVERYTHING needed to render a non-interactive version of the UI, at a point in time. This includes:
28
+ - fixed values read from "configuration"
29
+ - user controlled settings
30
+ - data fetched from backend
31
+
32
+ AppState does NOT contain any callback functions (for updating state). Those are implemented by StateMachine.
33
+ Actually, AppState MAY store some simple callback/update functions, in the form of WatchedMutableValues.
34
+ That's really only intended for simple things that are computed from the rendered layout, and which only affect other aspects of the rendered layout (ie. no other logic will likely be needed in response to updating the value).
35
+ */
36
+
37
+ export type AppState<cell_data_type> = Readonly<{
38
+ /**
39
+ Must be unique across all data tables on a given page (used in constructing CSS classes).
40
+ */
41
+ table_key: string;
42
+ page_number: number;
43
+ selected_pks: ReadonlySet<string>;
44
+ filtered_pks: string[];
45
+ /** null means we have not yet fetched any result count; should only occur before first result_count becomes available */
46
+ result_count: number | null;
47
+ /** Will generally be the same as result_count. MAY be less, if backend decides it's too expensive to return that many ids. */
48
+ accessible_result_count: number | null;
49
+ page_limit: number;
50
+ table_limit: number;
51
+ fill_last_page: boolean;
52
+ page_size: number;
53
+ /** indicates that we are fetching the total result count; any current result_count value is "stale" */
54
+ fetching_result_count: boolean;
55
+ /** indicates that we are fetching the list of matching ids; any current accessible_result_count is "stale" */
56
+ fetching_accessible_result_count: boolean;
57
+ fetching_page_data: boolean;
58
+ columns: ConfiguredColumns;
59
+
60
+ // note: now that we have current_sort, this is rather redundant
61
+ sort_state_interpreter: (
62
+ column_key: string,
63
+ ) => "forward" | "reverse" | null;
64
+
65
+ current_sort: SortState | null;
66
+ extra_sort_functions: ExtraSortFunction[];
67
+
68
+ table_data: ReadonlyArray<
69
+ // array of rows
70
+ Readonly<
71
+ [
72
+ string, // the pk of the row
73
+ ReadonlyMap<string, cell_data_type>, // column_key => cell data
74
+ ]
75
+ >
76
+ >;
77
+ include_selection_column: boolean;
78
+
79
+ // Note: this approach was implemented after mithril_view.HeaderComponent
80
+ // That component could have taken this approach (for "sticky_style_string") and it would not have needed to be stateful
81
+ table_overflows_left: WatchedMutableValue<boolean>;
82
+ table_overflows_right: WatchedMutableValue<boolean>;
83
+ row_width_style_string: WatchedMutableValue<string>;
84
+ }>;
@@ -0,0 +1,9 @@
1
+ import { StateMachine } from "./state_machine.js";
2
+ export declare class TableManager {
3
+ private readonly state_machine;
4
+ private event_target;
5
+ constructor(state_machine: StateMachine<any>, event_target: EventTarget);
6
+ add_selected_ids_changed_listener(callback: () => void): void;
7
+ remove_selected_ids_changed_listener(callback: () => void): void;
8
+ get selected_ids(): ReadonlySet<string>;
9
+ }
@@ -0,0 +1,16 @@
1
+ import { SELECTION_CHANGE } from "./event_names.js";
2
+ export class TableManager {
3
+ constructor(state_machine, event_target) {
4
+ this.state_machine = state_machine;
5
+ this.event_target = event_target;
6
+ }
7
+ add_selected_ids_changed_listener(callback) {
8
+ this.event_target.addEventListener(SELECTION_CHANGE, callback);
9
+ }
10
+ remove_selected_ids_changed_listener(callback) {
11
+ this.event_target.removeEventListener(SELECTION_CHANGE, callback);
12
+ }
13
+ get selected_ids() {
14
+ return this.state_machine.selected_ids;
15
+ }
16
+ }
@@ -0,0 +1,28 @@
1
+ import { StateMachine } from "./state_machine.js";
2
+ import { SELECTION_CHANGE } from "./event_names.js";
3
+
4
+ export class TableManager {
5
+ constructor(
6
+ private readonly state_machine: StateMachine<any>,
7
+ private event_target: EventTarget,
8
+ ) {}
9
+
10
+ add_selected_ids_changed_listener(callback: () => void) {
11
+ this.event_target.addEventListener(SELECTION_CHANGE, callback);
12
+ }
13
+ remove_selected_ids_changed_listener(callback: () => void) {
14
+ this.event_target.removeEventListener(SELECTION_CHANGE, callback);
15
+ }
16
+
17
+ get selected_ids(): ReadonlySet<string> {
18
+ return this.state_machine.selected_ids;
19
+ }
20
+ // Things we might want to implement:
21
+ // select(id: string, selected: boolean) {}
22
+ // set_selected(ids: ReadonlySet<string>) {}
23
+ // get table_id(): string {}
24
+ // reload() {}
25
+ // clear_selection() {}
26
+ // post_ids_to_new_window(url: string) {}
27
+ // post_ids_to_window_named(name: string, url: string) {}
28
+ }