selective-ui 1.0.2
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/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/selective-ui.css +569 -0
- package/dist/selective-ui.css.map +1 -0
- package/dist/selective-ui.esm.js +6101 -0
- package/dist/selective-ui.esm.js.map +1 -0
- package/dist/selective-ui.esm.min.js +1 -0
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.css +1 -0
- package/dist/selective-ui.min.css.br +0 -0
- package/dist/selective-ui.min.js +2 -0
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +6115 -0
- package/dist/selective-ui.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/css/components/accessorybox.css +64 -0
- package/src/css/components/directive.css +20 -0
- package/src/css/components/empty-state.css +26 -0
- package/src/css/components/loading-state.css +26 -0
- package/src/css/components/optgroup.css +62 -0
- package/src/css/components/option-handle.css +34 -0
- package/src/css/components/option.css +130 -0
- package/src/css/components/placeholder.css +15 -0
- package/src/css/components/popup.css +39 -0
- package/src/css/components/searchbox.css +29 -0
- package/src/css/components/selectbox.css +54 -0
- package/src/css/index.css +75 -0
- package/src/js/adapter/mixed-adapter.js +435 -0
- package/src/js/components/accessorybox.js +125 -0
- package/src/js/components/directive.js +38 -0
- package/src/js/components/empty-state.js +68 -0
- package/src/js/components/loading-state.js +60 -0
- package/src/js/components/option-handle.js +114 -0
- package/src/js/components/placeholder.js +57 -0
- package/src/js/components/popup.js +471 -0
- package/src/js/components/searchbox.js +168 -0
- package/src/js/components/selectbox.js +693 -0
- package/src/js/core/base/adapter.js +163 -0
- package/src/js/core/base/model.js +59 -0
- package/src/js/core/base/recyclerview.js +83 -0
- package/src/js/core/base/view.js +62 -0
- package/src/js/core/model-manager.js +286 -0
- package/src/js/core/search-controller.js +522 -0
- package/src/js/index.js +137 -0
- package/src/js/models/group-model.js +143 -0
- package/src/js/models/option-model.js +237 -0
- package/src/js/services/dataset-observer.js +73 -0
- package/src/js/services/ea-observer.js +88 -0
- package/src/js/services/effector.js +404 -0
- package/src/js/services/refresher.js +40 -0
- package/src/js/services/resize-observer.js +152 -0
- package/src/js/services/select-observer.js +61 -0
- package/src/js/types/adapter.type.js +33 -0
- package/src/js/types/effector.type.js +24 -0
- package/src/js/types/ievents.type.js +11 -0
- package/src/js/types/libs.type.js +28 -0
- package/src/js/types/model.type.js +11 -0
- package/src/js/types/recyclerview.type.js +12 -0
- package/src/js/types/resize-observer.type.js +19 -0
- package/src/js/types/view.group.type.js +13 -0
- package/src/js/types/view.option.type.js +15 -0
- package/src/js/types/view.type.js +11 -0
- package/src/js/utils/guard.js +47 -0
- package/src/js/utils/ievents.js +83 -0
- package/src/js/utils/istorage.js +61 -0
- package/src/js/utils/libs.js +619 -0
- package/src/js/utils/selective.js +386 -0
- package/src/js/views/group-view.js +103 -0
- package/src/js/views/option-view.js +153 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Libs } from "../../utils/libs";
|
|
2
|
+
import { Model } from "./model";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @template {ModelContract<any, any>} TItem
|
|
6
|
+
* @implements {AdapterContract<TItem>}
|
|
7
|
+
*/
|
|
8
|
+
export class Adapter {
|
|
9
|
+
/** @type {TItem[]} */
|
|
10
|
+
items = [];
|
|
11
|
+
adapterKey = Libs.randomString(12);
|
|
12
|
+
|
|
13
|
+
isSkipEvent = false;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the adapter with an optional array of items and invokes onInit()
|
|
18
|
+
* to perform any subclass-specific setup. Accepts a generic list of models.
|
|
19
|
+
*
|
|
20
|
+
* @param {TItem[]} [items=[]] - Initial items to be managed by the adapter.
|
|
21
|
+
*/
|
|
22
|
+
constructor(items = []) {
|
|
23
|
+
this.items = items;
|
|
24
|
+
this.onInit();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lifecycle hook called once after construction. Override in subclasses to
|
|
29
|
+
* perform setup tasks (e.g., event wiring, cache building).
|
|
30
|
+
*/
|
|
31
|
+
onInit() {}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Binds an item model to its viewer at a given position. If the item has not
|
|
35
|
+
* been initialized yet, renders the viewer; otherwise triggers an update.
|
|
36
|
+
*
|
|
37
|
+
* @param {any} item - The model instance to bind to the view.
|
|
38
|
+
* @param {any} viewer - The view instance responsible for rendering the model.
|
|
39
|
+
* @param {number} position - The index of the item within the adapter.
|
|
40
|
+
*/
|
|
41
|
+
onViewHolder(item, viewer, position) {
|
|
42
|
+
if (!item.isInit) {
|
|
43
|
+
viewer.render();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
viewer.update();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Registers a pre-change (debounced) callback for a property change pipeline.
|
|
52
|
+
* The callback is scheduled with a minimal delay to batch rapid updates.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} propName - The property name to observe (e.g., "items").
|
|
55
|
+
* @param {Function} callback - Function to execute before the property changes.
|
|
56
|
+
*/
|
|
57
|
+
onPropChanging(propName, callback) {
|
|
58
|
+
Libs.timerProcess.setExecute(`${propName}ing_${this.adapterKey}`, callback, 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Registers a post-change callback for a property change pipeline.
|
|
63
|
+
* The callback is executed after the property is updated.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} propName - The property name to observe (e.g., "items").
|
|
66
|
+
* @param {Function} callback - Function to execute after the property changes.
|
|
67
|
+
*/
|
|
68
|
+
onPropChanged(propName, callback) {
|
|
69
|
+
Libs.timerProcess.setExecute(`${propName}_${this.adapterKey}`, callback);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Triggers the post-change pipeline for a given property, passing optional parameters
|
|
74
|
+
* to registered callbacks. Use this after mutating adapter state.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} propName - The property name to emit (e.g., "items").
|
|
77
|
+
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
78
|
+
*/
|
|
79
|
+
changeProp(propName, ...params) {
|
|
80
|
+
Libs.timerProcess.run(`${propName}_${this.adapterKey}`, ...params);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Triggers the pre-change pipeline for a given property, passing optional parameters
|
|
85
|
+
* to registered callbacks. Use this before mutating adapter state.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} propName - The property name to emit (e.g., "items").
|
|
88
|
+
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
89
|
+
*/
|
|
90
|
+
changingProp(propName, ...params) {
|
|
91
|
+
Libs.timerProcess.run(`${propName}ing_${this.adapterKey}`, ...params);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates and returns a viewer instance for the given item within the specified parent container.
|
|
97
|
+
* Override in subclasses to return a concrete view implementation tailored to TItem.
|
|
98
|
+
*
|
|
99
|
+
* @param {HTMLElement} parent - The container element that will host the viewer.
|
|
100
|
+
* @param {TItem} item - The model instance for which the viewer is created.
|
|
101
|
+
* @returns {any} - The created viewer instance; null by default.
|
|
102
|
+
*/
|
|
103
|
+
viewHolder(parent, item) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the total number of items currently managed by the adapter.
|
|
109
|
+
*
|
|
110
|
+
* @returns {number} - The item count.
|
|
111
|
+
*/
|
|
112
|
+
itemCount() {
|
|
113
|
+
return this.items.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Replaces the adapter's items with a new collection, emitting pre-change and post-change
|
|
118
|
+
* notifications to observers. Does not render; call updateRecyclerView() to apply to the DOM.
|
|
119
|
+
*
|
|
120
|
+
* @param {TItem[]} items - The new list of items to set.
|
|
121
|
+
*/
|
|
122
|
+
setItems(items) {
|
|
123
|
+
this.changingProp("items", items);
|
|
124
|
+
this.items = items;
|
|
125
|
+
this.changeProp("items", items);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Synchronizes adapter items from an external source by delegating to setItems().
|
|
130
|
+
* Useful for keeping adapter state aligned with another data store.
|
|
131
|
+
*
|
|
132
|
+
* @param {TItem[]} items - The source list of items to synchronize.
|
|
133
|
+
*/
|
|
134
|
+
syncFromSource(items) {
|
|
135
|
+
this.setItems(items);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Iterates through all items and ensures each has a viewer. For new items, calls viewHolder()
|
|
140
|
+
* to create the viewer, then binds via onViewHolder() and marks the item as initialized.
|
|
141
|
+
*
|
|
142
|
+
* @param {HTMLElement|null} parent - The container element in which item viewers are rendered.
|
|
143
|
+
*/
|
|
144
|
+
updateRecyclerView(parent) {
|
|
145
|
+
for (let index = 0; index < this.itemCount(); index++) {
|
|
146
|
+
let viewer = this.items[index].view;
|
|
147
|
+
if (!this.items[index].isInit) {
|
|
148
|
+
viewer = this.viewHolder(parent, this.items[index]);
|
|
149
|
+
}
|
|
150
|
+
this.onViewHolder(this.items[index], viewer, index);
|
|
151
|
+
|
|
152
|
+
this.items[index].isInit = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Updates adapter data without performing any default actions.
|
|
158
|
+
* Override in subclasses to implement custom data refresh logic.
|
|
159
|
+
*
|
|
160
|
+
* @param {TItem[]} items - The incoming data to apply to the adapter.
|
|
161
|
+
*/
|
|
162
|
+
updateData(items) { }
|
|
163
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template TTarget
|
|
3
|
+
* @template {Record<string, Element>} TTags
|
|
4
|
+
* @template {ViewContract<TTags>} TView
|
|
5
|
+
* @implements {ModelContract<TTarget, TView>}
|
|
6
|
+
*/
|
|
7
|
+
export class Model {
|
|
8
|
+
/** @type {TTarget | null} */
|
|
9
|
+
targetElement = null;
|
|
10
|
+
|
|
11
|
+
options = null;
|
|
12
|
+
|
|
13
|
+
/** @type {TView | null} */
|
|
14
|
+
view = null;
|
|
15
|
+
|
|
16
|
+
position = -1;
|
|
17
|
+
|
|
18
|
+
isInit = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the current value from the underlying target element's "value" attribute.
|
|
22
|
+
* For single-select, this is typically a string; for multi-select, may be an array depending on usage.
|
|
23
|
+
*
|
|
24
|
+
* @type {String|String[]}
|
|
25
|
+
*/
|
|
26
|
+
get value() {
|
|
27
|
+
return /** @type {HTMLElement} */ (this.targetElement).getAttribute("value");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Constructs a Model instance with configuration options and optional bindings to a target element and view.
|
|
32
|
+
* Stores references for later updates and rendering.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} options - Configuration options for the model.
|
|
35
|
+
* @param {TTarget|null} [targetElement=null] - The underlying element (e.g., <option> or group node).
|
|
36
|
+
* @param {TView|null} [view=null] - The associated view responsible for rendering the model.
|
|
37
|
+
*/
|
|
38
|
+
constructor(options, targetElement = null, view = null) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.targetElement = targetElement;
|
|
41
|
+
this.view = view;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Updates the bound target element reference and invokes the change hook.
|
|
46
|
+
*
|
|
47
|
+
* @param {TTarget|null} targetElement - The new target element to bind to the model.
|
|
48
|
+
*/
|
|
49
|
+
update(targetElement) {
|
|
50
|
+
this.targetElement = targetElement;
|
|
51
|
+
this.onTargetChanged();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hook invoked whenever the target element changes.
|
|
56
|
+
* Override in subclasses to react to attribute/content updates (e.g., text, disabled state).
|
|
57
|
+
*/
|
|
58
|
+
onTargetChanged() { }
|
|
59
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template {ModelContract<any, any>} TItem
|
|
3
|
+
* @template {AdapterContract<TItem>} TAdapter
|
|
4
|
+
* @implements {RecyclerViewContract<TAdapter>}
|
|
5
|
+
*/
|
|
6
|
+
export class RecyclerView {
|
|
7
|
+
/**
|
|
8
|
+
* @type {HTMLDivElement}
|
|
9
|
+
*/
|
|
10
|
+
viewElement = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {TAdapter}
|
|
14
|
+
*/
|
|
15
|
+
adapter = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Constructs a RecyclerView with an optional container element that will host rendered item views.
|
|
19
|
+
*
|
|
20
|
+
* @param {HTMLDivElement|null} [viewElement=null] - The root element where the adapter will render items.
|
|
21
|
+
*/
|
|
22
|
+
constructor(viewElement = null) {
|
|
23
|
+
this.viewElement = viewElement;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sets or updates the container element used to render the adapter's item views.
|
|
28
|
+
*
|
|
29
|
+
* @param {HTMLDivElement} viewElement - The root element for rendering.
|
|
30
|
+
*/
|
|
31
|
+
setView(viewElement) {
|
|
32
|
+
this.viewElement = viewElement;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Attaches an adapter to the RecyclerView and wires item-change lifecycle:
|
|
37
|
+
* - onPropChanging("items"): clears the container before items change,
|
|
38
|
+
* - onPropChanged("items"): re-renders after items change,
|
|
39
|
+
* then performs an initial render.
|
|
40
|
+
*
|
|
41
|
+
* @param {TAdapter} adapter - The adapter managing models and their views.
|
|
42
|
+
*/
|
|
43
|
+
setAdapter(adapter) {
|
|
44
|
+
this.adapter = adapter;
|
|
45
|
+
|
|
46
|
+
adapter.onPropChanging("items", () => {
|
|
47
|
+
this.clear();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
adapter.onPropChanged("items", () => {
|
|
51
|
+
this.render();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.render();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes all child nodes from the rendering container, if present.
|
|
59
|
+
* Used prior to re-rendering or when items are changing.
|
|
60
|
+
*/
|
|
61
|
+
clear() {
|
|
62
|
+
if (!this.viewElement) return;
|
|
63
|
+
this.viewElement.replaceChildren();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Renders the current adapter contents into the container.
|
|
68
|
+
* No-ops if either the adapter or the container is not set.
|
|
69
|
+
*/
|
|
70
|
+
render() {
|
|
71
|
+
if (!this.adapter || !this.viewElement) return;
|
|
72
|
+
|
|
73
|
+
this.adapter.updateRecyclerView(this.viewElement);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Forces a re-render of the current adapter state into the container.
|
|
78
|
+
* Useful when visual updates are required without changing the data.
|
|
79
|
+
*/
|
|
80
|
+
refresh() {
|
|
81
|
+
this.render();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @template {Record<string, Element>} TTags
|
|
4
|
+
* @implements {ViewContract<TTags>}
|
|
5
|
+
*/
|
|
6
|
+
export class View {
|
|
7
|
+
/** @type {Element|null} */
|
|
8
|
+
parent = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the view with a parent container element that will host its rendered content.
|
|
12
|
+
*
|
|
13
|
+
* @param {Element} parent - The parent element into which this view will render.
|
|
14
|
+
*/
|
|
15
|
+
constructor(parent) {
|
|
16
|
+
this.parent = parent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Renders the view into the parent container.
|
|
21
|
+
* Override in subclasses to create DOM structure and mount tags.
|
|
22
|
+
*/
|
|
23
|
+
render() {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Updates the view to reflect model or state changes.
|
|
27
|
+
* Override in subclasses to patch DOM nodes without full re-render.
|
|
28
|
+
*/
|
|
29
|
+
update() {}
|
|
30
|
+
|
|
31
|
+
/** @type {MountViewResult<TTags>} */
|
|
32
|
+
view = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the root HTMLElement of the mounted view.
|
|
36
|
+
*
|
|
37
|
+
* @returns {HTMLElement} - The root view element.
|
|
38
|
+
*/
|
|
39
|
+
getView() {
|
|
40
|
+
return this.view.view;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieves a single tagged element from the mounted view.
|
|
45
|
+
*
|
|
46
|
+
* @template {keyof TTags} K
|
|
47
|
+
* @param {K} tag - The tag key corresponding to the desired element.
|
|
48
|
+
* @returns {TTags[K]} - The element associated with the provided tag key.
|
|
49
|
+
*/
|
|
50
|
+
getTag(tag) {
|
|
51
|
+
return this.view.tags[tag];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Retrieves the full tag map for the mounted view.
|
|
56
|
+
*
|
|
57
|
+
* @returns {TTags} - An object map of all tagged elements.
|
|
58
|
+
*/
|
|
59
|
+
getTags() {
|
|
60
|
+
return this.view.tags;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { GroupModel } from "../models/group-model";
|
|
2
|
+
import { OptionModel } from "../models/option-model";
|
|
3
|
+
import { Adapter } from "./base/adapter";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @template {ModelContract<any, any>} TModel
|
|
7
|
+
* @template {Adapter} TAdapter
|
|
8
|
+
*/
|
|
9
|
+
export class ModelManager {
|
|
10
|
+
/** @type {Array<GroupModel|OptionModel>} */
|
|
11
|
+
#privModelList = [];
|
|
12
|
+
|
|
13
|
+
/** @type {new (...args: any[]) => TAdapter} */
|
|
14
|
+
#privAdapter;
|
|
15
|
+
|
|
16
|
+
/** @type {TAdapter} */
|
|
17
|
+
#privAdapterHandle;
|
|
18
|
+
|
|
19
|
+
/** @type {new (...args: any[]) => RecyclerViewContract<TAdapter>} */
|
|
20
|
+
#privRecyclerView;
|
|
21
|
+
|
|
22
|
+
/** @type {RecyclerViewContract<TAdapter>} */
|
|
23
|
+
#privRecyclerViewHandle;
|
|
24
|
+
|
|
25
|
+
options = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Constructs a ModelManager with configuration options used by created models and components.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} options - Configuration object passed to GroupModel/OptionModel and view infrastructure.
|
|
31
|
+
*/
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Registers the adapter class to be used for rendering and managing models.
|
|
38
|
+
*
|
|
39
|
+
* @param {new (...args: any[]) => TAdapter} adapter - The adapter constructor (class) to instantiate.
|
|
40
|
+
*/
|
|
41
|
+
setupAdapter(adapter) {
|
|
42
|
+
this.#privAdapter = adapter;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Registers the RecyclerView class responsible for hosting and updating item views.
|
|
47
|
+
*
|
|
48
|
+
* @param {new (...args: any[]) => RecyclerViewContract<TAdapter>} recyclerView - The recycler view constructor.
|
|
49
|
+
*/
|
|
50
|
+
setupRecyclerView(recyclerView) {
|
|
51
|
+
this.#privRecyclerView = recyclerView;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds model instances (GroupModel/OptionModel) from raw <optgroup>/<option> elements.
|
|
56
|
+
* Preserves grouping relationships and returns the structured list.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array<HTMLOptGroupElement|HTMLOptionElement>} modelData - Parsed DOM elements from the source <select>.
|
|
59
|
+
* @returns {Array<GroupModel|OptionModel>} - The ordered list of group and option models.
|
|
60
|
+
*/
|
|
61
|
+
createModelResources(modelData) {
|
|
62
|
+
this.#privModelList = [];
|
|
63
|
+
let currentGroup = null;
|
|
64
|
+
|
|
65
|
+
modelData.forEach(data => {
|
|
66
|
+
if (data.tagName === "OPTGROUP") {
|
|
67
|
+
currentGroup = new GroupModel(this.options, data);
|
|
68
|
+
this.#privModelList.push(currentGroup);
|
|
69
|
+
}
|
|
70
|
+
else if (data.tagName === "OPTION") {
|
|
71
|
+
const optionModel = new OptionModel(this.options, /** @type {HTMLOptionElement} */ (data));
|
|
72
|
+
|
|
73
|
+
if (data["__parentGroup"] && currentGroup &&
|
|
74
|
+
data["__parentGroup"] === currentGroup.targetElement) {
|
|
75
|
+
currentGroup.addItem(optionModel);
|
|
76
|
+
optionModel.group = currentGroup;
|
|
77
|
+
} else {
|
|
78
|
+
this.#privModelList.push(optionModel);
|
|
79
|
+
currentGroup = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return this.#privModelList;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Replaces the current model list with new data and syncs it into the adapter,
|
|
89
|
+
* then refreshes the view to reflect changes.
|
|
90
|
+
*
|
|
91
|
+
* @param {Array<HTMLOptGroupElement|HTMLOptionElement>} modelData - New source elements to rebuild models from.
|
|
92
|
+
*/
|
|
93
|
+
replace(modelData) {
|
|
94
|
+
this.createModelResources(modelData);
|
|
95
|
+
|
|
96
|
+
if (this.#privAdapterHandle) {
|
|
97
|
+
this.#privAdapterHandle.syncFromSource(this.#privModelList);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.refresh();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Requests a view refresh if an adapter has been initialized,
|
|
105
|
+
* typically used after external updates to model data.
|
|
106
|
+
*/
|
|
107
|
+
notify() {
|
|
108
|
+
if (!this.#privAdapterHandle) return;
|
|
109
|
+
|
|
110
|
+
this.refresh();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Initializes adapter and recycler view instances, attaches them to a container element,
|
|
115
|
+
* and applies optional configuration overrides for adapter and recyclerView.
|
|
116
|
+
*
|
|
117
|
+
* @param {HTMLElement} viewElement - The container element where items will be rendered.
|
|
118
|
+
* @param {object} [adapterOpt={}] - Optional properties to merge into the adapter instance.
|
|
119
|
+
* @param {object} [recyclerViewOpt={}] - Optional properties to merge into the recycler view instance.
|
|
120
|
+
*/
|
|
121
|
+
load(viewElement, adapterOpt = {}, recyclerViewOpt = {}) {
|
|
122
|
+
this.#privAdapterHandle = new this.#privAdapter(this.#privModelList);
|
|
123
|
+
Object.assign(this.#privAdapterHandle, adapterOpt);
|
|
124
|
+
|
|
125
|
+
this.#privRecyclerViewHandle = new this.#privRecyclerView(viewElement);
|
|
126
|
+
this.#privRecyclerViewHandle.setAdapter(this.#privAdapterHandle);
|
|
127
|
+
Object.assign(this.#privRecyclerViewHandle, recyclerViewOpt);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Diffs existing models against new <optgroup>/<option> data to update in place:
|
|
132
|
+
* reuses existing models when possible, updates positions and group membership,
|
|
133
|
+
* removes stale views, and notifies adapter and listeners about updates.
|
|
134
|
+
*
|
|
135
|
+
* @param {Array<HTMLOptGroupElement|HTMLOptionElement>} modelData - Fresh DOM elements reflecting the latest state.
|
|
136
|
+
*/
|
|
137
|
+
update(modelData) {
|
|
138
|
+
const oldModels = this.#privModelList;
|
|
139
|
+
const newModels = [];
|
|
140
|
+
|
|
141
|
+
const oldGroupMap = new Map();
|
|
142
|
+
const oldOptionMap = new Map();
|
|
143
|
+
|
|
144
|
+
oldModels.forEach(model => {
|
|
145
|
+
if (model instanceof GroupModel) {
|
|
146
|
+
oldGroupMap.set(model.label, model);
|
|
147
|
+
} else if (model instanceof OptionModel) {
|
|
148
|
+
oldOptionMap.set(model.value, model);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let currentGroup = null;
|
|
153
|
+
let position = 0;
|
|
154
|
+
|
|
155
|
+
modelData.forEach((data, index) => {
|
|
156
|
+
if (data.tagName === "OPTGROUP") {
|
|
157
|
+
let dataVset = /** @type {HTMLOptGroupElement} */ (data);
|
|
158
|
+
const existingGroup = oldGroupMap.get(dataVset.label);
|
|
159
|
+
|
|
160
|
+
if (existingGroup) {
|
|
161
|
+
existingGroup.update(dataVset);
|
|
162
|
+
existingGroup.position = position;
|
|
163
|
+
existingGroup.items = [];
|
|
164
|
+
currentGroup = existingGroup;
|
|
165
|
+
newModels.push(existingGroup);
|
|
166
|
+
oldGroupMap.delete(dataVset.label);
|
|
167
|
+
} else {
|
|
168
|
+
currentGroup = new GroupModel(this.options, dataVset);
|
|
169
|
+
currentGroup.position = position;
|
|
170
|
+
newModels.push(currentGroup);
|
|
171
|
+
}
|
|
172
|
+
position++;
|
|
173
|
+
}
|
|
174
|
+
else if (data.tagName === "OPTION") {
|
|
175
|
+
let dataVset = /** @type {HTMLOptionElement} */ (data);
|
|
176
|
+
const existingOption = oldOptionMap.get(dataVset.value);
|
|
177
|
+
|
|
178
|
+
if (existingOption) {
|
|
179
|
+
existingOption.update(dataVset);
|
|
180
|
+
existingOption.position = position;
|
|
181
|
+
|
|
182
|
+
if (dataVset["__parentGroup"] && currentGroup) {
|
|
183
|
+
currentGroup.addItem(existingOption);
|
|
184
|
+
existingOption.group = currentGroup;
|
|
185
|
+
} else {
|
|
186
|
+
existingOption.group = null;
|
|
187
|
+
newModels.push(existingOption);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
oldOptionMap.delete(dataVset.value);
|
|
191
|
+
} else {
|
|
192
|
+
const newOption = new OptionModel(this.options, dataVset);
|
|
193
|
+
newOption.position = position;
|
|
194
|
+
|
|
195
|
+
if (dataVset["__parentGroup"] && currentGroup) {
|
|
196
|
+
currentGroup.addItem(newOption);
|
|
197
|
+
newOption.group = currentGroup;
|
|
198
|
+
} else {
|
|
199
|
+
newModels.push(newOption);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
position++;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
oldGroupMap.forEach(removedGroup => {
|
|
207
|
+
if (removedGroup.view) {
|
|
208
|
+
removedGroup.view.getView()?.remove();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
oldOptionMap.forEach(removedOption => {
|
|
213
|
+
if (removedOption.view) {
|
|
214
|
+
removedOption.view.getView()?.remove();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.#privModelList = newModels;
|
|
219
|
+
|
|
220
|
+
if (this.#privAdapterHandle) {
|
|
221
|
+
this.#privAdapterHandle.updateData(this.#privModelList);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.onUpdated();
|
|
225
|
+
this.refresh();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Hook invoked after the manager completes an update or refresh cycle.
|
|
230
|
+
* Override to run side effects (e.g., layout adjustments or analytics).
|
|
231
|
+
*/
|
|
232
|
+
onUpdated() { }
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Instructs the adapter to temporarily skip event handling (e.g., during batch updates).
|
|
236
|
+
*
|
|
237
|
+
* @param {boolean} value - True to skip events; false to restore normal behavior.
|
|
238
|
+
*/
|
|
239
|
+
skipEvent(value) {
|
|
240
|
+
this.#privAdapterHandle.isSkipEvent = value;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Re-renders the recycler view if present and invokes the post-refresh hook.
|
|
245
|
+
* No-op if the recycler view is not initialized.
|
|
246
|
+
*/
|
|
247
|
+
refresh() {
|
|
248
|
+
if (!this.#privRecyclerViewHandle) return;
|
|
249
|
+
this.#privRecyclerViewHandle.refresh();
|
|
250
|
+
this.onUpdated();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Returns handles to the current resources, including the model list,
|
|
255
|
+
* adapter instance, and recycler view instance.
|
|
256
|
+
*
|
|
257
|
+
* @returns {{modelList: (GroupModel|OptionModel)[], adapter: TAdapter, recyclerView: RecyclerViewContract<TAdapter>}}
|
|
258
|
+
*/
|
|
259
|
+
getResources() {
|
|
260
|
+
return {
|
|
261
|
+
modelList: this.#privModelList,
|
|
262
|
+
adapter: this.#privAdapterHandle,
|
|
263
|
+
recyclerView: this.#privRecyclerViewHandle
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Triggers the adapter's pre-change pipeline for a named event,
|
|
269
|
+
* enabling observers to react before a change is applied.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} event_name - The event or property name (e.g., "items", "select").
|
|
272
|
+
*/
|
|
273
|
+
triggerChanging(event_name) {
|
|
274
|
+
this.#privAdapterHandle.changingProp(event_name);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Triggers the adapter's post-change pipeline for a named event,
|
|
279
|
+
* notifying observers after a change has been applied.
|
|
280
|
+
*
|
|
281
|
+
* @param {string} event_name - The event or property name (e.g., "items", "select").
|
|
282
|
+
*/
|
|
283
|
+
triggerChanged(event_name) {
|
|
284
|
+
this.#privAdapterHandle.changeProp(event_name);
|
|
285
|
+
}
|
|
286
|
+
}
|