@qnc/qnc_data_tables 1.0.3 → 1.0.6-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 (66) 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/package.json +12 -5
  64. package/dist/qnc_data_tables.d.ts +0 -57
  65. package/dist/qnc_data_tables.js +0 -1136
  66. package/dist/qnc_data_tables_inline_css.js +0 -8
@@ -0,0 +1,316 @@
1
+ import { SELECTION_CHANGE } from "./event_names.js";
2
+ import { get_storage } from "./table_options.js";
3
+ import * as tu from "@qnc/type_utils";
4
+ import { ColumnManager } from "./column_manager.js";
5
+ import * as column_sorting from "./column_sorting.js";
6
+ import { BoundStoredValue, BoundStoredStringSet, NUMBER_CONVERTER, STRING_CONVERTER, } from "./bound_stored_value.js";
7
+ import { WatchedMutableValue } from "./watched_mutable_value.js";
8
+ /**
9
+ The StateMachine is responsible for aggregating state from 3 sources of information:
10
+ - table configuration (eg. coming from JS or HTML attributes)
11
+ - saved user preferences
12
+ - data fetched from the backend
13
+
14
+ It is also responsible for:
15
+ - providing methods to adjust state
16
+ - fetching data from the back end
17
+ - automatically saving any changes of user-configurable "settings" for the table.
18
+ - dispatching custom events
19
+ */
20
+ export class StateMachine {
21
+ constructor(options,
22
+ /** This is used to "prepare" cell html from the backend for your UI library of choice */
23
+ prepare_cell, event_target,
24
+ /** Will be called any time any of our state changes */
25
+ onchange) {
26
+ this.options = options;
27
+ this.prepare_cell = prepare_cell;
28
+ this.event_target = event_target;
29
+ this.result_count_xhr = new XMLHttpRequest();
30
+ this.id_list_xhr = new XMLHttpRequest();
31
+ this.table_data_xhr = new XMLHttpRequest();
32
+ this.result_count = null;
33
+ this.accessible_result_count = null;
34
+ this.id_list = [];
35
+ this.table_data = [];
36
+ this.fetched_columns = new Set();
37
+ this.after_render = null;
38
+ this.column_manager = new ColumnManager(options.columns, get_storage(options.layout_storage), options.key_prefix + "_columns", this.on_column_manager_change);
39
+ this.stored_values = make_bound_stored_values(options);
40
+ this.onchange = () => onchange(this.get_state());
41
+ this.table_overflows_left = new WatchedMutableValue(false, this.onchange);
42
+ this.table_overflows_right = new WatchedMutableValue(false, this.onchange);
43
+ this.row_width_style_string = new WatchedMutableValue("", this.onchange);
44
+ this.setup_result_count_xhr(this.result_count_xhr);
45
+ this.setup_id_list_xhr(this.id_list_xhr);
46
+ this.setup_page_xhr(this.table_data_xhr);
47
+ this.valid_sort_keys = new Set();
48
+ for (const sort_function of options.extra_sort_functions) {
49
+ this.valid_sort_keys.add(column_sorting.make_function_sort_key(sort_function.key));
50
+ }
51
+ for (const column of options.columns) {
52
+ if (!column.sortable)
53
+ continue;
54
+ this.valid_sort_keys.add(column_sorting.make_column_sort_key(column.key, "forward"));
55
+ this.valid_sort_keys.add(column_sorting.make_column_sort_key(column.key, "reverse"));
56
+ }
57
+ this.fetch_data();
58
+ }
59
+ on_column_manager_change() { }
60
+ /** Get current app state. This will not be needed often, but you might want it for your initial rendering. */
61
+ get_state() {
62
+ return {
63
+ page_number: this.stored_values.page_number.value,
64
+ selected_pks: this.stored_values.selected_pks.value,
65
+ filtered_pks: this.id_list,
66
+ result_count: this.result_count,
67
+ page_limit: this.options.page_limit,
68
+ table_limit: this.options.table_limit,
69
+ fetching_page_data: is_loading(this.id_list_xhr) || is_loading(this.table_data_xhr),
70
+ fetching_result_count: is_loading(this.result_count_xhr),
71
+ columns: this.column_manager,
72
+ // Note: now that we've added current_sort, this could be replaced
73
+ sort_state_interpreter: (column_key) => column_sorting.current_sort_direction(column_key, this.stored_values.sort_key.value),
74
+ table_data: this.table_data,
75
+ fill_last_page: this.options.fill_last_page,
76
+ page_size: this.stored_values.page_size.value,
77
+ accessible_result_count: this.accessible_result_count,
78
+ fetching_accessible_result_count: is_loading(this.id_list_xhr),
79
+ table_key: this.options.key_prefix,
80
+ include_selection_column: this.options.allow_selection,
81
+ table_overflows_left: this.table_overflows_left,
82
+ table_overflows_right: this.table_overflows_right,
83
+ row_width_style_string: this.row_width_style_string,
84
+ current_sort: this.get_sort_state(),
85
+ extra_sort_functions: this.options.extra_sort_functions,
86
+ };
87
+ }
88
+ get_sort_state() {
89
+ try {
90
+ return column_sorting.parse_sort_key(this.stored_values.sort_key.value);
91
+ }
92
+ catch (e) {
93
+ console.error(e);
94
+ return null;
95
+ }
96
+ }
97
+ get selected_ids() {
98
+ return this.stored_values.selected_pks.value;
99
+ }
100
+ setup_result_count_xhr(xhr) {
101
+ xhr.onload = (e) => {
102
+ if (xhr.status != 200) {
103
+ log_error(xhr);
104
+ this.onchange();
105
+ return;
106
+ }
107
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
108
+ this.result_count = tu.require_number(data.count);
109
+ this.onchange();
110
+ };
111
+ xhr.onerror = () => {
112
+ log_error(xhr);
113
+ this.onchange();
114
+ };
115
+ }
116
+ setup_id_list_xhr(xhr) {
117
+ xhr.onload = () => {
118
+ if (xhr.status != 200) {
119
+ log_error(xhr);
120
+ this.onchange();
121
+ return;
122
+ }
123
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
124
+ this.id_list = tu.require_array(data.ids).map(tu.require_string);
125
+ this.accessible_result_count = this.id_list.length;
126
+ this.onchange();
127
+ this.fetch_page();
128
+ };
129
+ xhr.onerror = () => {
130
+ log_error(xhr);
131
+ this.onchange();
132
+ };
133
+ }
134
+ setup_page_xhr(xhr) {
135
+ xhr.onload = () => {
136
+ if (xhr.status != 200) {
137
+ log_error(xhr);
138
+ this.onchange();
139
+ return;
140
+ }
141
+ const data = tu.require_string_map(tu.parse_json(xhr.responseText));
142
+ this.table_data = tu.require_array(data.rows).map((row) => {
143
+ if (!tu.is_2_tuple(row))
144
+ throw new Error("row data not an array");
145
+ const pk = tu.require_string(row[0]);
146
+ const cell_data = tu.require_array(row[1]);
147
+ const cells = new Map();
148
+ for (const column of cell_data) {
149
+ // const array = tu.require_array(column)
150
+ // column
151
+ const [column_key, html] = tu.require_2_tuple(column);
152
+ tu.assert_string(column_key);
153
+ tu.assert_string(html);
154
+ cells.set(column_key, this.prepare_cell(html, column_key));
155
+ }
156
+ return [pk, cells];
157
+ });
158
+ this.onchange();
159
+ };
160
+ xhr.onerror = () => {
161
+ log_error(xhr);
162
+ this.onchange();
163
+ };
164
+ }
165
+ fetch_result_count() {
166
+ const data = get_filter_form_data(this.options.filter_form_id);
167
+ data.append(`${this.options.key_prefix}_action`, "get_result_count");
168
+ this.result_count_xhr.open("POST", this.options.api_url);
169
+ this.result_count_xhr.send(data);
170
+ this.onchange();
171
+ }
172
+ fetch_id_list() {
173
+ const data = get_filter_form_data(this.options.filter_form_id);
174
+ data.append(`${this.options.key_prefix}_action`, "get_ids");
175
+ if (this.stored_values.sort_key.value) {
176
+ data.append(`${this.options.key_prefix}_sort_key`, this.stored_values.sort_key.value);
177
+ }
178
+ this.id_list_xhr.open("POST", this.options.api_url);
179
+ this.id_list_xhr.send(data);
180
+ this.onchange();
181
+ }
182
+ fetch_page() {
183
+ // Note - most pages won't need data from filter_form to return results
184
+ // However, some pages MAY include controls in the filter_form which impact how the columns/data are generated
185
+ // It's part of our documented contract that we always submit filter_form data with each post request
186
+ const data = get_filter_form_data(this.options.filter_form_id);
187
+ data.append(`${this.options.key_prefix}_action`, "get_table_data");
188
+ const column_keys = this.column_manager.all_enabled_columns.map((c) => c.key);
189
+ this.fetched_columns = new Set(column_keys);
190
+ data.append(`${this.options.key_prefix}_columns`, JSON.stringify(column_keys));
191
+ const page_size = this.stored_values.page_size.value;
192
+ const start_index = (this.stored_values.page_number.value - 1) * page_size;
193
+ data.append(`${this.options.key_prefix}_ids`, JSON.stringify(this.id_list.slice(start_index, start_index + page_size)));
194
+ this.table_data_xhr.open("POST", this.options.api_url);
195
+ this.table_data_xhr.send(data);
196
+ }
197
+ get num_pages() {
198
+ return Math.ceil(this.id_list.length / this.stored_values.page_size.value);
199
+ }
200
+ fetch_data() {
201
+ this.fetch_result_count();
202
+ this.fetch_id_list();
203
+ }
204
+ set_page_size(page_size) {
205
+ this.stored_values.page_size.value = Math.min(page_size, this.options.page_limit);
206
+ this.onchange();
207
+ }
208
+ set_page_number(page_number) {
209
+ this.stored_values.page_number.value = Math.min(page_number, this.num_pages);
210
+ this.fetch_page();
211
+ this.onchange();
212
+ }
213
+ set_column_enabled(column_key, enabled) {
214
+ this.column_manager.set_enabled(column_key, enabled);
215
+ if (enabled && !this.fetched_columns.has(column_key))
216
+ this.fetch_page();
217
+ this.onchange();
218
+ }
219
+ set_sort_key(sort_key) {
220
+ console.assert(this.valid_sort_keys.has(sort_key));
221
+ this.stored_values.sort_key.value = sort_key;
222
+ this.fetch_data();
223
+ this.onchange();
224
+ }
225
+ sort_by_function(function_key) {
226
+ this.set_sort_key(column_sorting.make_function_sort_key(function_key));
227
+ }
228
+ sort_by_column(column_key, direction) {
229
+ this.set_sort_key(column_sorting.make_column_sort_key(column_key, direction));
230
+ }
231
+ set_column_width(column_key, width) {
232
+ this.column_manager.set_width(column_key, width);
233
+ this.onchange();
234
+ }
235
+ /** keys may include all sortable columns, not just the enabled ones */
236
+ set_column_order(keys) {
237
+ this.column_manager.set_order(keys);
238
+ this.onchange();
239
+ }
240
+ set_selected(pk, selected) {
241
+ const set = this.stored_values.selected_pks;
242
+ if (selected)
243
+ set.add(pk);
244
+ else
245
+ set.delete(pk);
246
+ this.onchange();
247
+ this.dispatch_selection_change_event();
248
+ }
249
+ bulk_set_selected(pks, selected) {
250
+ const set = new Set(this.stored_values.selected_pks.value);
251
+ if (selected) {
252
+ for (const value of pks) {
253
+ set.add(value);
254
+ }
255
+ }
256
+ else {
257
+ for (const value of pks) {
258
+ set.delete(value);
259
+ }
260
+ }
261
+ this.stored_values.selected_pks.value = set;
262
+ this.onchange();
263
+ this.dispatch_selection_change_event();
264
+ }
265
+ dispatch_selection_change_event() {
266
+ this.event_target.dispatchEvent(new CustomEvent(SELECTION_CHANGE));
267
+ }
268
+ }
269
+ function is_loading(xhr) {
270
+ return xhr.readyState != 4;
271
+ }
272
+ function log_error(xhr) {
273
+ /*
274
+ Always log the error but don't alert when status == 0, statusText == "" and responseText == "" because Safari
275
+ returns those conditions when a request is interrupted by the user making another request (navigating away) and we don't want to show
276
+ an empty alert.
277
+ 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.
278
+ */
279
+ console.warn("qnc_data_tables xhr error: ", xhr);
280
+ if (xhr.status != 0 || xhr.statusText != "" || xhr.responseText != "") {
281
+ alert("Data table error: " + xhr.responseText || xhr.statusText);
282
+ }
283
+ }
284
+ function get_filter_form(id) {
285
+ if (!id)
286
+ return null;
287
+ const form = document.getElementById(id);
288
+ if (!(form instanceof HTMLFormElement))
289
+ throw new Error(`ID "${id}" does not reference a <form> on this page`);
290
+ return form;
291
+ }
292
+ /** Return a new FormData associate with the filter form if one is specified, else a new empty FormData */
293
+ function get_filter_form_data(id) {
294
+ const form = get_filter_form(id);
295
+ if (form)
296
+ return new FormData(form);
297
+ return new FormData();
298
+ }
299
+ /**
300
+ Read TableOptions and make various BoundStoredValues that always save to correct storage "area" when updated
301
+
302
+ Note that this
303
+ */
304
+ function make_bound_stored_values(options) {
305
+ const { key_prefix } = options;
306
+ const selection_storage = get_storage(options.selection_storage);
307
+ const navigation_storage = get_storage(options.navigation_storage);
308
+ const layout_storage = get_storage(options.layout_storage);
309
+ const sort_storage = get_storage(options.sort_storage);
310
+ return {
311
+ page_number: new BoundStoredValue(navigation_storage, key_prefix + "_page", NUMBER_CONVERTER, 1),
312
+ page_size: new BoundStoredValue(layout_storage, key_prefix + "_page_size", NUMBER_CONVERTER, options.page_size),
313
+ selected_pks: new BoundStoredStringSet(selection_storage, key_prefix + "_selected"),
314
+ sort_key: new BoundStoredValue(sort_storage, key_prefix + "_sort", STRING_CONVERTER, column_sorting.encode_sort(options.default_sort)),
315
+ };
316
+ }