@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.
- 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,6 @@
|
|
|
1
|
+
/** Two purposes: automatic key-prefixing, and the ability to create a "dummy storage" */
|
|
2
|
+
export declare function optional_storage_wrapper(storage: Storage | null, key_prefix: string): {
|
|
3
|
+
get(key: string): string | null;
|
|
4
|
+
set(key: string, value: string): void;
|
|
5
|
+
};
|
|
6
|
+
export type OptionalStorage = ReturnType<typeof optional_storage_wrapper>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Two purposes: automatic key-prefixing, and the ability to create a "dummy storage" */
|
|
2
|
+
export function optional_storage_wrapper(storage, key_prefix) {
|
|
3
|
+
if (!storage)
|
|
4
|
+
return {
|
|
5
|
+
get(key) {
|
|
6
|
+
return null;
|
|
7
|
+
},
|
|
8
|
+
set(key, value) { },
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
get(key) {
|
|
12
|
+
return storage.getItem(`${key_prefix}${key}`);
|
|
13
|
+
},
|
|
14
|
+
set(key, value) {
|
|
15
|
+
storage.setItem(`${key_prefix}${key}`, value);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Two purposes: automatic key-prefixing, and the ability to create a "dummy storage" */
|
|
2
|
+
export function optional_storage_wrapper(
|
|
3
|
+
storage: Storage | null,
|
|
4
|
+
key_prefix: string,
|
|
5
|
+
) {
|
|
6
|
+
if (!storage)
|
|
7
|
+
return {
|
|
8
|
+
get(key: string) {
|
|
9
|
+
return null;
|
|
10
|
+
},
|
|
11
|
+
set(key: string, value: string) {},
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
get(key: string) {
|
|
15
|
+
return storage.getItem(`${key_prefix}${key}`);
|
|
16
|
+
},
|
|
17
|
+
set(key: string, value: string) {
|
|
18
|
+
storage.setItem(`${key_prefix}${key}`, value);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OptionalStorage = ReturnType<typeof optional_storage_wrapper>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Responsible for toggling "[PREFIX][SIDE]" classes on a possibly-overflowing element.
|
|
3
|
+
|
|
4
|
+
Nothing project-specific here
|
|
5
|
+
*/
|
|
6
|
+
export declare class OverflowClassManager {
|
|
7
|
+
readonly root: Element;
|
|
8
|
+
readonly class_prefix: string;
|
|
9
|
+
constructor(root: Element, class_prefix: string);
|
|
10
|
+
/**
|
|
11
|
+
detector _may_ have zero size; that's okay (it _should_ have 0 size in at least one dimension)
|
|
12
|
+
|
|
13
|
+
detector _may_ be longer (in one dimension) than root; that's also okay
|
|
14
|
+
|
|
15
|
+
detector should be positioned [side]-most inside the root (this is tricky if root has padding).
|
|
16
|
+
|
|
17
|
+
If root element can scroll in both directions, you need to be careful about how you setup your detectors. Eg. right detector should either be full-height, or should use sticky positioning so that it never goes completely above or completely below root. The only way right detector should not intersect (at all) with root is if it is to-the-right-of root.
|
|
18
|
+
*/
|
|
19
|
+
setup_overflow_detector(detector: Element, side: "left" | "right" | "top" | "bottom"): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Responsible for toggling "[PREFIX][SIDE]" classes on a possibly-overflowing element.
|
|
3
|
+
|
|
4
|
+
Nothing project-specific here
|
|
5
|
+
*/
|
|
6
|
+
export class OverflowClassManager {
|
|
7
|
+
constructor(root, class_prefix) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
this.class_prefix = class_prefix;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
detector _may_ have zero size; that's okay (it _should_ have 0 size in at least one dimension)
|
|
13
|
+
|
|
14
|
+
detector _may_ be longer (in one dimension) than root; that's also okay
|
|
15
|
+
|
|
16
|
+
detector should be positioned [side]-most inside the root (this is tricky if root has padding).
|
|
17
|
+
|
|
18
|
+
If root element can scroll in both directions, you need to be careful about how you setup your detectors. Eg. right detector should either be full-height, or should use sticky positioning so that it never goes completely above or completely below root. The only way right detector should not intersect (at all) with root is if it is to-the-right-of root.
|
|
19
|
+
*/
|
|
20
|
+
setup_overflow_detector(detector, side) {
|
|
21
|
+
const class_name = `${this.class_prefix}${side}`;
|
|
22
|
+
new IntersectionObserver((entries, observer) => {
|
|
23
|
+
if (entries[0]) {
|
|
24
|
+
this.root.classList.toggle(class_name, !entries[0].isIntersecting);
|
|
25
|
+
}
|
|
26
|
+
}, {
|
|
27
|
+
root: this.root,
|
|
28
|
+
}).observe(detector);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Responsible for toggling "[PREFIX][SIDE]" classes on a possibly-overflowing element.
|
|
3
|
+
|
|
4
|
+
Nothing project-specific here
|
|
5
|
+
*/
|
|
6
|
+
export class OverflowClassManager {
|
|
7
|
+
constructor(
|
|
8
|
+
readonly root: Element,
|
|
9
|
+
readonly class_prefix: string,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
detector _may_ have zero size; that's okay (it _should_ have 0 size in at least one dimension)
|
|
14
|
+
|
|
15
|
+
detector _may_ be longer (in one dimension) than root; that's also okay
|
|
16
|
+
|
|
17
|
+
detector should be positioned [side]-most inside the root (this is tricky if root has padding).
|
|
18
|
+
|
|
19
|
+
If root element can scroll in both directions, you need to be careful about how you setup your detectors. Eg. right detector should either be full-height, or should use sticky positioning so that it never goes completely above or completely below root. The only way right detector should not intersect (at all) with root is if it is to-the-right-of root.
|
|
20
|
+
*/
|
|
21
|
+
setup_overflow_detector(
|
|
22
|
+
detector: Element,
|
|
23
|
+
side: "left" | "right" | "top" | "bottom",
|
|
24
|
+
) {
|
|
25
|
+
const class_name = `${this.class_prefix}${side}`;
|
|
26
|
+
new IntersectionObserver(
|
|
27
|
+
(entries, observer) => {
|
|
28
|
+
if (entries[0]) {
|
|
29
|
+
this.root.classList.toggle(
|
|
30
|
+
class_name,
|
|
31
|
+
!entries[0].isIntersecting,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
root: this.root,
|
|
37
|
+
},
|
|
38
|
+
).observe(detector);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import m from "mithril";
|
|
2
|
+
export declare class Renderer {
|
|
3
|
+
/**
|
|
4
|
+
Given some phrasing content and some help text, return an "augmented" version of that phrasing content with some means of accessing the help text.
|
|
5
|
+
The help text might be long; it should not take up space on the page.
|
|
6
|
+
The final rendered result should not be much wider than the initial phrasing content (this may be used inside space-constrained locations, like <th> elements).
|
|
7
|
+
*/
|
|
8
|
+
render_with_help_text(phrasing_content: m.Children, help: string): m.Vnode<any, any>;
|
|
9
|
+
render_with_optional_help_text(phrasing_content: m.Children, help: string): m.Children;
|
|
10
|
+
render_header(args: {
|
|
11
|
+
label: m.Children;
|
|
12
|
+
sortable: boolean;
|
|
13
|
+
sorted: "no" | "forward" | "reverse";
|
|
14
|
+
set_sort: (direction: "forward" | "reverse") => void;
|
|
15
|
+
}): m.Vnode<any, any>;
|
|
16
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Here we implement a class for performing various low-level rendering operations, which users might want to override (ie. so that the generated markup works better with their stylesheet or to use custom components that are used elsewhere on their site). You should NOT implement a custom Renderer in order to change or add functionality (and we'll do our best with the method signatures/documentation to prevent that).
|
|
3
|
+
*/
|
|
4
|
+
import m from "mithril";
|
|
5
|
+
export class Renderer {
|
|
6
|
+
/**
|
|
7
|
+
Given some phrasing content and some help text, return an "augmented" version of that phrasing content with some means of accessing the help text.
|
|
8
|
+
The help text might be long; it should not take up space on the page.
|
|
9
|
+
The final rendered result should not be much wider than the initial phrasing content (this may be used inside space-constrained locations, like <th> elements).
|
|
10
|
+
*/
|
|
11
|
+
render_with_help_text(phrasing_content, help) {
|
|
12
|
+
return m("span", { title: help }, phrasing_content, m("span", {
|
|
13
|
+
tabindex: 0,
|
|
14
|
+
onclick: function (e) {
|
|
15
|
+
alert(help);
|
|
16
|
+
// This allows you to render help text inside other interactive elements
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
},
|
|
20
|
+
onkeydown: function (e) {
|
|
21
|
+
if (e.key == "Enter" || e.key == " ") {
|
|
22
|
+
this.click();
|
|
23
|
+
// in case you pressed space, don't scroll page
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
className: "qnc-data-table-help-text",
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
render_with_optional_help_text(phrasing_content, help) {
|
|
31
|
+
if (help)
|
|
32
|
+
return this.render_with_help_text(phrasing_content, help);
|
|
33
|
+
return phrasing_content;
|
|
34
|
+
}
|
|
35
|
+
render_header(args) {
|
|
36
|
+
// entire header text is a (clickable) button (similar to many file browser and other UIs) :
|
|
37
|
+
if (args.sortable)
|
|
38
|
+
return m("button", {
|
|
39
|
+
onclick: () => args.set_sort(args.sorted == "forward" ? "reverse" : "forward"),
|
|
40
|
+
className: "qnc-data-table-header-button",
|
|
41
|
+
type: "button",
|
|
42
|
+
},
|
|
43
|
+
// The wrapping span is so that you can see (in your browser's dev tools) exactly how wide the button content is, in case you want to fix column width at that width
|
|
44
|
+
// (note - button _might_ also suffice for this, but users might style button as full-width, for increased click target size)
|
|
45
|
+
m("span", m("span", { className: "qnc-data-table-header-button-text" }, args.label), args.sorted == "forward" &&
|
|
46
|
+
m("span", { className: "qnc-data-table-sort-indicator" }, " ▲"), args.sorted == "reverse" &&
|
|
47
|
+
m("span", { className: "qnc-data-table-sort-indicator" }, " ▼")));
|
|
48
|
+
// The wrapping span is so that you can see (in your browser's dev tools) exactly how wide the text content is, in case you want to fix column width at that width
|
|
49
|
+
return m("span", args.label);
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/renderer.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Here we implement a class for performing various low-level rendering operations, which users might want to override (ie. so that the generated markup works better with their stylesheet or to use custom components that are used elsewhere on their site). You should NOT implement a custom Renderer in order to change or add functionality (and we'll do our best with the method signatures/documentation to prevent that).
|
|
3
|
+
*/
|
|
4
|
+
import m from "mithril";
|
|
5
|
+
|
|
6
|
+
export class Renderer {
|
|
7
|
+
/**
|
|
8
|
+
Given some phrasing content and some help text, return an "augmented" version of that phrasing content with some means of accessing the help text.
|
|
9
|
+
The help text might be long; it should not take up space on the page.
|
|
10
|
+
The final rendered result should not be much wider than the initial phrasing content (this may be used inside space-constrained locations, like <th> elements).
|
|
11
|
+
*/
|
|
12
|
+
render_with_help_text(phrasing_content: m.Children, help: string) {
|
|
13
|
+
return m(
|
|
14
|
+
"span",
|
|
15
|
+
{ title: help },
|
|
16
|
+
phrasing_content,
|
|
17
|
+
m("span", {
|
|
18
|
+
tabindex: 0,
|
|
19
|
+
onclick: function (e: MouseEvent) {
|
|
20
|
+
alert(help);
|
|
21
|
+
// This allows you to render help text inside other interactive elements
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
},
|
|
25
|
+
onkeydown: function (e: KeyboardEvent) {
|
|
26
|
+
if (e.key == "Enter" || e.key == " ") {
|
|
27
|
+
this.click();
|
|
28
|
+
// in case you pressed space, don't scroll page
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
className: "qnc-data-table-help-text",
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
render_with_optional_help_text(phrasing_content: m.Children, help: string) {
|
|
37
|
+
if (help) return this.render_with_help_text(phrasing_content, help);
|
|
38
|
+
return phrasing_content;
|
|
39
|
+
}
|
|
40
|
+
render_header(args: {
|
|
41
|
+
label: m.Children;
|
|
42
|
+
sortable: boolean;
|
|
43
|
+
sorted: "no" | "forward" | "reverse";
|
|
44
|
+
set_sort: (direction: "forward" | "reverse") => void;
|
|
45
|
+
}) {
|
|
46
|
+
// entire header text is a (clickable) button (similar to many file browser and other UIs) :
|
|
47
|
+
if (args.sortable)
|
|
48
|
+
return m(
|
|
49
|
+
"button",
|
|
50
|
+
{
|
|
51
|
+
onclick: () =>
|
|
52
|
+
args.set_sort(
|
|
53
|
+
args.sorted == "forward" ? "reverse" : "forward",
|
|
54
|
+
),
|
|
55
|
+
className: "qnc-data-table-header-button",
|
|
56
|
+
type: "button",
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// The wrapping span is so that you can see (in your browser's dev tools) exactly how wide the button content is, in case you want to fix column width at that width
|
|
60
|
+
// (note - button _might_ also suffice for this, but users might style button as full-width, for increased click target size)
|
|
61
|
+
m(
|
|
62
|
+
"span",
|
|
63
|
+
m(
|
|
64
|
+
"span",
|
|
65
|
+
{ className: "qnc-data-table-header-button-text" },
|
|
66
|
+
args.label,
|
|
67
|
+
),
|
|
68
|
+
args.sorted == "forward" &&
|
|
69
|
+
m(
|
|
70
|
+
"span",
|
|
71
|
+
{ className: "qnc-data-table-sort-indicator" },
|
|
72
|
+
" ▲",
|
|
73
|
+
),
|
|
74
|
+
args.sorted == "reverse" &&
|
|
75
|
+
m(
|
|
76
|
+
"span",
|
|
77
|
+
{ className: "qnc-data-table-sort-indicator" },
|
|
78
|
+
" ▼",
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// The wrapping span is so that you can see (in your browser's dev tools) exactly how wide the text content is, in case you want to fix column width at that width
|
|
84
|
+
return m("span", args.label);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Registers a custom element (<qdt-selection-fieldset-controller>) for wrapping a <fieldset> element.
|
|
3
|
+
The custom element:
|
|
4
|
+
- requires a "table-id" attribute to be set, which must be the id of a data table element
|
|
5
|
+
- dynamically updates the fieldset's "disabled" attribute
|
|
6
|
+
- dynamically updates the fieldset's legend (to include # of selected items)
|
|
7
|
+
- provides access to the TableManager of the connected data table
|
|
8
|
+
*/
|
|
9
|
+
export declare function register_selection_fieldset_controller_custom_element(): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BaseQncDataTable } from "./custom_element.js";
|
|
2
|
+
/**
|
|
3
|
+
Intended to wrap a <fieldset> element of "selection action buttons".
|
|
4
|
+
Responsible for:
|
|
5
|
+
- dynamically updating fieldset's "disabled" attribute
|
|
6
|
+
- dynamically updating fieldset's legend (to include # of selected items)
|
|
7
|
+
- providing access to the TableManager of the connected data table
|
|
8
|
+
|
|
9
|
+
Intended for use when you're generating selection action buttons on your backend (in initial page HTML).
|
|
10
|
+
|
|
11
|
+
If you're building selection action buttons on the front-end, this is probably not the best approach.
|
|
12
|
+
*/
|
|
13
|
+
class QdtSelectionFieldsetController extends HTMLElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.fieldset = null;
|
|
17
|
+
this.legend = null;
|
|
18
|
+
this.table = null;
|
|
19
|
+
this.on_selection_change = () => this.render();
|
|
20
|
+
}
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
this.connect_table();
|
|
23
|
+
this.connect_fieldset();
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
static get selected_attributes() {
|
|
27
|
+
return ["table-id"];
|
|
28
|
+
}
|
|
29
|
+
attributeChangedCallback(name, old_value, new_value) {
|
|
30
|
+
this.connect_table();
|
|
31
|
+
this.render();
|
|
32
|
+
}
|
|
33
|
+
connect_table() {
|
|
34
|
+
this.table = null;
|
|
35
|
+
const id = this.getAttribute("table-id");
|
|
36
|
+
if (!id) {
|
|
37
|
+
console.error(`${this.tagName} requires "table-id" attribute`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const table = document.getElementById(id);
|
|
41
|
+
if (!table) {
|
|
42
|
+
console.error(`"${id}" does not reference any element by id`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!(table instanceof BaseQncDataTable)) {
|
|
46
|
+
console.error(`"${id}" references an HTML element, but it is not a BaseQncDataTable`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.table = table;
|
|
50
|
+
this.table
|
|
51
|
+
.required_table_manager()
|
|
52
|
+
.add_selected_ids_changed_listener(this.on_selection_change);
|
|
53
|
+
}
|
|
54
|
+
connect_fieldset() {
|
|
55
|
+
this.fieldset = this.querySelector("fieldset");
|
|
56
|
+
if (!this.fieldset)
|
|
57
|
+
console.warn("Could not find a fieldset descendant. Our presence is meaningless.");
|
|
58
|
+
this.legend = this.querySelector("legend");
|
|
59
|
+
}
|
|
60
|
+
render() {
|
|
61
|
+
const selected_count = this.table
|
|
62
|
+
? this.table.required_table_manager().selected_ids.size
|
|
63
|
+
: 0;
|
|
64
|
+
if (this.fieldset)
|
|
65
|
+
this.fieldset.disabled = selected_count == 0;
|
|
66
|
+
if (this.legend)
|
|
67
|
+
this.legend.innerText = `With ${selected_count} selected item(s):`;
|
|
68
|
+
}
|
|
69
|
+
required_table_manager() {
|
|
70
|
+
if (!this.table)
|
|
71
|
+
throw new Error("We are not connected to a table");
|
|
72
|
+
return this.table.required_table_manager();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
Registers a custom element (<qdt-selection-fieldset-controller>) for wrapping a <fieldset> element.
|
|
77
|
+
The custom element:
|
|
78
|
+
- requires a "table-id" attribute to be set, which must be the id of a data table element
|
|
79
|
+
- dynamically updates the fieldset's "disabled" attribute
|
|
80
|
+
- dynamically updates the fieldset's legend (to include # of selected items)
|
|
81
|
+
- provides access to the TableManager of the connected data table
|
|
82
|
+
*/
|
|
83
|
+
export function register_selection_fieldset_controller_custom_element() {
|
|
84
|
+
customElements.define("qdt-selection-fieldset-controller", QdtSelectionFieldsetController);
|
|
85
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { TableManager } from "./table_manager.js";
|
|
2
|
+
import { BaseQncDataTable } from "./custom_element.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
Intended to wrap a <fieldset> element of "selection action buttons".
|
|
6
|
+
Responsible for:
|
|
7
|
+
- dynamically updating fieldset's "disabled" attribute
|
|
8
|
+
- dynamically updating fieldset's legend (to include # of selected items)
|
|
9
|
+
- providing access to the TableManager of the connected data table
|
|
10
|
+
|
|
11
|
+
Intended for use when you're generating selection action buttons on your backend (in initial page HTML).
|
|
12
|
+
|
|
13
|
+
If you're building selection action buttons on the front-end, this is probably not the best approach.
|
|
14
|
+
*/
|
|
15
|
+
class QdtSelectionFieldsetController extends HTMLElement {
|
|
16
|
+
private fieldset: HTMLFieldSetElement | null = null;
|
|
17
|
+
private legend: HTMLLegendElement | null = null;
|
|
18
|
+
private table: BaseQncDataTable | null = null;
|
|
19
|
+
|
|
20
|
+
private on_selection_change: () => void;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
this.on_selection_change = () => this.render();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
this.connect_table();
|
|
29
|
+
this.connect_fieldset();
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static get selected_attributes() {
|
|
34
|
+
return ["table-id"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
attributeChangedCallback(
|
|
38
|
+
name: string,
|
|
39
|
+
old_value: string,
|
|
40
|
+
new_value: string,
|
|
41
|
+
) {
|
|
42
|
+
this.connect_table();
|
|
43
|
+
this.render();
|
|
44
|
+
}
|
|
45
|
+
private connect_table() {
|
|
46
|
+
this.table = null;
|
|
47
|
+
|
|
48
|
+
const id = this.getAttribute("table-id");
|
|
49
|
+
if (!id) {
|
|
50
|
+
console.error(`${this.tagName} requires "table-id" attribute`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const table = document.getElementById(id);
|
|
54
|
+
if (!table) {
|
|
55
|
+
console.error(`"${id}" does not reference any element by id`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!(table instanceof BaseQncDataTable)) {
|
|
59
|
+
console.error(
|
|
60
|
+
`"${id}" references an HTML element, but it is not a BaseQncDataTable`,
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.table = table;
|
|
65
|
+
this.table
|
|
66
|
+
.required_table_manager()
|
|
67
|
+
.add_selected_ids_changed_listener(this.on_selection_change);
|
|
68
|
+
}
|
|
69
|
+
private connect_fieldset() {
|
|
70
|
+
this.fieldset = this.querySelector("fieldset");
|
|
71
|
+
if (!this.fieldset)
|
|
72
|
+
console.warn(
|
|
73
|
+
"Could not find a fieldset descendant. Our presence is meaningless.",
|
|
74
|
+
);
|
|
75
|
+
this.legend = this.querySelector("legend");
|
|
76
|
+
}
|
|
77
|
+
private render() {
|
|
78
|
+
const selected_count = this.table
|
|
79
|
+
? this.table.required_table_manager().selected_ids.size
|
|
80
|
+
: 0;
|
|
81
|
+
|
|
82
|
+
if (this.fieldset) this.fieldset.disabled = selected_count == 0;
|
|
83
|
+
if (this.legend)
|
|
84
|
+
this.legend.innerText = `With ${selected_count} selected item(s):`;
|
|
85
|
+
}
|
|
86
|
+
required_table_manager(): TableManager {
|
|
87
|
+
if (!this.table) throw new Error("We are not connected to a table");
|
|
88
|
+
return this.table.required_table_manager();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
Registers a custom element (<qdt-selection-fieldset-controller>) for wrapping a <fieldset> element.
|
|
93
|
+
The custom element:
|
|
94
|
+
- requires a "table-id" attribute to be set, which must be the id of a data table element
|
|
95
|
+
- dynamically updates the fieldset's "disabled" attribute
|
|
96
|
+
- dynamically updates the fieldset's legend (to include # of selected items)
|
|
97
|
+
- provides access to the TableManager of the connected data table
|
|
98
|
+
*/
|
|
99
|
+
export function register_selection_fieldset_controller_custom_element() {
|
|
100
|
+
customElements.define(
|
|
101
|
+
"qdt-selection-fieldset-controller",
|
|
102
|
+
QdtSelectionFieldsetController,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AppState, SortState } from "./state_types.js";
|
|
2
|
+
import { TableOptions } from "./table_options.js";
|
|
3
|
+
/**
|
|
4
|
+
The StateMachine is responsible for aggregating state from 3 sources of information:
|
|
5
|
+
- table configuration (eg. coming from JS or HTML attributes)
|
|
6
|
+
- saved user preferences
|
|
7
|
+
- data fetched from the backend
|
|
8
|
+
|
|
9
|
+
It is also responsible for:
|
|
10
|
+
- providing methods to adjust state
|
|
11
|
+
- fetching data from the back end
|
|
12
|
+
- automatically saving any changes of user-configurable "settings" for the table.
|
|
13
|
+
- dispatching custom events
|
|
14
|
+
*/
|
|
15
|
+
export declare class StateMachine<cell_data_type> {
|
|
16
|
+
private options;
|
|
17
|
+
/** This is used to "prepare" cell html from the backend for your UI library of choice */
|
|
18
|
+
private prepare_cell;
|
|
19
|
+
private event_target;
|
|
20
|
+
/** This represents all of the "user settings", EXCEPT those dealing with columns. We delegate the management of those settings to column_manager. */
|
|
21
|
+
private stored_values;
|
|
22
|
+
private onchange;
|
|
23
|
+
private result_count_xhr;
|
|
24
|
+
private id_list_xhr;
|
|
25
|
+
private table_data_xhr;
|
|
26
|
+
private result_count;
|
|
27
|
+
private accessible_result_count;
|
|
28
|
+
private id_list;
|
|
29
|
+
private table_data;
|
|
30
|
+
private column_manager;
|
|
31
|
+
private fetched_columns;
|
|
32
|
+
private valid_sort_keys;
|
|
33
|
+
after_render: (() => void) | null;
|
|
34
|
+
private table_overflows_left;
|
|
35
|
+
private table_overflows_right;
|
|
36
|
+
private row_width_style_string;
|
|
37
|
+
constructor(options: TableOptions,
|
|
38
|
+
/** This is used to "prepare" cell html from the backend for your UI library of choice */
|
|
39
|
+
prepare_cell: (html: string, column_key: string) => cell_data_type, event_target: EventTarget,
|
|
40
|
+
/** Will be called any time any of our state changes */
|
|
41
|
+
onchange: (state: AppState<cell_data_type>) => void);
|
|
42
|
+
on_column_manager_change(): void;
|
|
43
|
+
/** Get current app state. This will not be needed often, but you might want it for your initial rendering. */
|
|
44
|
+
get_state(): AppState<cell_data_type>;
|
|
45
|
+
get_sort_state(): SortState | null;
|
|
46
|
+
get selected_ids(): ReadonlySet<string>;
|
|
47
|
+
private setup_result_count_xhr;
|
|
48
|
+
private setup_id_list_xhr;
|
|
49
|
+
private setup_page_xhr;
|
|
50
|
+
private fetch_result_count;
|
|
51
|
+
private fetch_id_list;
|
|
52
|
+
private fetch_page;
|
|
53
|
+
private get num_pages();
|
|
54
|
+
fetch_data(): void;
|
|
55
|
+
set_page_size(page_size: number): void;
|
|
56
|
+
set_page_number(page_number: number): void;
|
|
57
|
+
set_column_enabled(column_key: string, enabled: boolean): void;
|
|
58
|
+
set_sort_key(sort_key: string): void;
|
|
59
|
+
sort_by_function(function_key: string): void;
|
|
60
|
+
sort_by_column(column_key: string, direction: "forward" | "reverse"): void;
|
|
61
|
+
set_column_width(column_key: string, width: number): void;
|
|
62
|
+
/** keys may include all sortable columns, not just the enabled ones */
|
|
63
|
+
set_column_order(keys: string[]): void;
|
|
64
|
+
set_selected(pk: string, selected: boolean): void;
|
|
65
|
+
bulk_set_selected(pks: Iterable<string>, selected: boolean): void;
|
|
66
|
+
private dispatch_selection_change_event;
|
|
67
|
+
}
|