@qnc/qnc_data_tables 1.0.4 → 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.
- package/README.md +36 -0
- package/dist/bound_stored_value.d.ts +39 -0
- package/dist/bound_stored_value.js +134 -0
- package/dist/bound_stored_value.ts +158 -0
- package/dist/column_manager.d.ts +43 -0
- package/dist/column_manager.js +124 -0
- package/dist/column_manager.ts +158 -0
- package/dist/column_resizing.d.ts +3 -0
- package/dist/column_resizing.js +30 -0
- package/dist/column_resizing.ts +52 -0
- package/dist/column_sorting.d.ts +11 -0
- package/dist/column_sorting.js +53 -0
- package/dist/column_sorting.ts +63 -0
- package/dist/conditionally_wrapped_element.d.ts +5 -0
- package/dist/conditionally_wrapped_element.js +14 -0
- package/dist/conditionally_wrapped_element.ts +17 -0
- package/dist/create_mithril_app.d.ts +3 -0
- package/dist/create_mithril_app.js +25 -0
- package/dist/create_mithril_app.ts +35 -0
- package/dist/create_style.d.ts +4 -0
- package/dist/create_style.js +9 -0
- package/dist/create_style.ts +10 -0
- package/dist/custom_element.d.ts +23 -0
- package/dist/custom_element.js +63 -0
- package/dist/custom_element.ts +71 -0
- package/dist/event_names.d.ts +1 -0
- package/dist/event_names.js +1 -0
- package/dist/event_names.ts +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/index.ts +5 -0
- package/dist/mithril_view.d.ts +16 -0
- package/dist/mithril_view.js +484 -0
- package/dist/mithril_view.ts +1014 -0
- package/dist/optional_storage.d.ts +6 -0
- package/dist/optional_storage.js +18 -0
- package/dist/optional_storage.ts +23 -0
- package/dist/overflow_class_manager.d.ts +20 -0
- package/dist/overflow_class_manager.js +30 -0
- package/dist/overflow_class_manager.ts +40 -0
- package/dist/renderer.d.ts +16 -0
- package/dist/renderer.js +51 -0
- package/dist/renderer.ts +86 -0
- package/dist/selection_fieldset_controller.d.ts +9 -0
- package/dist/selection_fieldset_controller.js +85 -0
- package/dist/selection_fieldset_controller.ts +104 -0
- package/dist/state_machine.d.ts +67 -0
- package/dist/state_machine.js +316 -0
- package/dist/state_machine.ts +434 -0
- package/dist/state_types.d.ts +62 -0
- package/dist/state_types.js +1 -0
- package/dist/state_types.ts +84 -0
- package/dist/table_manager.d.ts +9 -0
- package/dist/table_manager.js +16 -0
- package/dist/table_manager.ts +28 -0
- package/dist/table_options.d.ts +164 -0
- package/dist/table_options.js +97 -0
- package/dist/table_options.ts +132 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/watched_mutable_value.d.ts +15 -0
- package/dist/watched_mutable_value.js +23 -0
- package/dist/watched_mutable_value.ts +23 -0
- package/package.json +12 -5
- package/dist/qnc_data_tables.d.ts +0 -57
- package/dist/qnc_data_tables.js +0 -1136
- 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
|
+
}
|