selective-ui 1.2.3 → 1.2.4
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/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +2106 -624
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +2 -2
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +2107 -625
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/ts/adapter/mixed-adapter.ts +156 -65
- package/src/ts/components/accessorybox.ts +153 -30
- package/src/ts/components/directive.ts +62 -11
- package/src/ts/components/option-handle.ts +124 -28
- package/src/ts/components/placeholder.ts +73 -16
- package/src/ts/components/popup/empty-state.ts +126 -0
- package/src/ts/components/popup/loading-state.ts +120 -0
- package/src/ts/components/{popup.ts → popup/popup.ts} +168 -71
- package/src/ts/components/searchbox.ts +82 -16
- package/src/ts/components/selectbox.ts +208 -109
- package/src/ts/core/base/adapter.ts +110 -44
- package/src/ts/core/base/lifecycle.ts +175 -0
- package/src/ts/core/base/model.ts +63 -32
- package/src/ts/core/base/recyclerview.ts +56 -18
- package/src/ts/core/base/view.ts +56 -19
- package/src/ts/core/base/virtual-recyclerview.ts +317 -126
- package/src/ts/core/model-manager.ts +4 -4
- package/src/ts/core/search-controller.ts +149 -24
- package/src/ts/global.ts +5 -5
- package/src/ts/index.ts +5 -5
- package/src/ts/models/group-model.ts +27 -6
- package/src/ts/models/option-model.ts +29 -6
- package/src/ts/services/ea-observer.ts +6 -6
- package/src/ts/types/components/searchbox.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +2 -1
- package/src/ts/types/core/base/lifecycle.type.ts +62 -0
- package/src/ts/types/core/base/model.type.ts +3 -1
- package/src/ts/types/core/base/recyclerview.type.ts +2 -8
- package/src/ts/types/core/base/view.type.ts +36 -24
- package/src/ts/utils/istorage.ts +1 -1
- package/src/ts/utils/selective.ts +153 -36
- package/src/ts/views/group-view.ts +59 -21
- package/src/ts/views/option-view.ts +137 -68
- package/src/ts/components/empty-state.ts +0 -68
- package/src/ts/components/loading-state.ts +0 -66
- /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
- /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
- /package/src/css/components/{popup.css → popup/popup.css} +0 -0
- /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
- /package/src/css/{components/option.css → views/option-view.css} +0 -0
package/dist/selective-ui.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Selective UI v1.2.
|
|
1
|
+
/*! Selective UI v1.2.4 | MIT License */
|
|
2
2
|
/**
|
|
3
3
|
* @class
|
|
4
4
|
*/
|
|
@@ -38,7 +38,7 @@ class iStorage {
|
|
|
38
38
|
textAccessoryDeselect: "Deselect: ",
|
|
39
39
|
animationtime: 200, // millisecond
|
|
40
40
|
delaysearchtime: 200, // millisecond
|
|
41
|
-
allowHtml:
|
|
41
|
+
allowHtml: false,
|
|
42
42
|
maxSelected: 0,
|
|
43
43
|
labelHalign: "left",
|
|
44
44
|
labelValign: "center",
|
|
@@ -830,112 +830,425 @@ class Refresher {
|
|
|
830
830
|
}
|
|
831
831
|
|
|
832
832
|
/**
|
|
833
|
-
*
|
|
833
|
+
* Enumerates the finite lifecycle states used across core classes (e.g., View/Model).
|
|
834
|
+
*
|
|
835
|
+
* State flow (happy path):
|
|
836
|
+
* NEW → INITIALIZED → MOUNTED → UPDATED → DESTROYED
|
|
837
|
+
*
|
|
838
|
+
* Notes:
|
|
839
|
+
* - `UPDATED` may be emitted multiple times after mount, depending on implementation.
|
|
840
|
+
* - `DESTROYED` is terminal; no further transitions should occur.
|
|
841
|
+
*/
|
|
842
|
+
var LifecycleState;
|
|
843
|
+
(function (LifecycleState) {
|
|
844
|
+
/** The instance has been created but not initialized. */
|
|
845
|
+
LifecycleState["NEW"] = "new";
|
|
846
|
+
/** Initialization logic has completed; ready to be mounted. */
|
|
847
|
+
LifecycleState["INITIALIZED"] = "initialized";
|
|
848
|
+
/** The instance is mounted/attached to its environment (e.g., DOM). */
|
|
849
|
+
LifecycleState["MOUNTED"] = "mounted";
|
|
850
|
+
/**
|
|
851
|
+
* The instance has been updated at least once after being mounted.
|
|
852
|
+
* Further updates typically keep the state at UPDATED.
|
|
853
|
+
*/
|
|
854
|
+
LifecycleState["UPDATED"] = "updated";
|
|
855
|
+
/** Teardown has run; resources/hooks have been released. */
|
|
856
|
+
LifecycleState["DESTROYED"] = "destroyed";
|
|
857
|
+
})(LifecycleState || (LifecycleState = {}));
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Lightweight lifecycle manager that provides:
|
|
861
|
+
* - A finite-state lifecycle machine (`NEW` → `INITIALIZED` → `MOUNTED` → `UPDATED` → `DESTROYED`)
|
|
862
|
+
* - A simple hooks system to subscribe to lifecycle events (`onInit`, `onMount`, `onUpdate`, `onDestroy`)
|
|
863
|
+
*
|
|
864
|
+
* Classes in the core (e.g., Model/View) can extend this to standardize initialization,
|
|
865
|
+
* mounting, updates, and teardown. Hooks are stored as in-memory callbacks and cleared on destroy.
|
|
834
866
|
*/
|
|
835
|
-
class
|
|
867
|
+
class Lifecycle {
|
|
868
|
+
/**
|
|
869
|
+
* Constructs the lifecycle manager and pre-registers hook containers.
|
|
870
|
+
* No hooks are executed during construction.
|
|
871
|
+
*/
|
|
872
|
+
constructor() {
|
|
873
|
+
/** Current lifecycle state */
|
|
874
|
+
this.state = LifecycleState.NEW;
|
|
875
|
+
/**
|
|
876
|
+
* Registered lifecycle hooks.
|
|
877
|
+
*
|
|
878
|
+
* Uses a Set per hook to:
|
|
879
|
+
* - Avoid duplicate registrations
|
|
880
|
+
* - Preserve insertion order for deterministic execution
|
|
881
|
+
*/
|
|
882
|
+
this.hooks = new Map();
|
|
883
|
+
this.hooks.set("onInit", new Set());
|
|
884
|
+
this.hooks.set("onMount", new Set());
|
|
885
|
+
this.hooks.set("onUpdate", new Set());
|
|
886
|
+
this.hooks.set("onDestroy", new Set());
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Subscribes a callback to a lifecycle hook.
|
|
890
|
+
*
|
|
891
|
+
* @param hook - The lifecycle hook name to listen to
|
|
892
|
+
* @param fn - The callback to execute when the hook is emitted
|
|
893
|
+
* @returns `this` for chaining
|
|
894
|
+
*/
|
|
895
|
+
on(hook, fn) {
|
|
896
|
+
this.hooks.get(hook).add(fn);
|
|
897
|
+
return this;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Unsubscribes a previously registered callback from a lifecycle hook.
|
|
901
|
+
*
|
|
902
|
+
* @param hook - The lifecycle hook name
|
|
903
|
+
* @param fn - The callback to remove
|
|
904
|
+
* @returns `this` for chaining
|
|
905
|
+
*/
|
|
906
|
+
off(hook, fn) {
|
|
907
|
+
this.hooks.get(hook).delete(fn);
|
|
908
|
+
return this;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Emits a lifecycle hook, executing all registered callbacks
|
|
912
|
+
* in the order they were added.
|
|
913
|
+
*
|
|
914
|
+
* @param hook - The lifecycle hook to emit
|
|
915
|
+
* @internal Prefer using the public lifecycle methods (`init/mount/update/destroy`)
|
|
916
|
+
* which call `emit` at the correct times.
|
|
917
|
+
*/
|
|
918
|
+
emit(hook, prevState) {
|
|
919
|
+
const ctx = {
|
|
920
|
+
state: this.state,
|
|
921
|
+
prevState,
|
|
922
|
+
};
|
|
923
|
+
for (const fn of this.hooks.get(hook)) {
|
|
924
|
+
try {
|
|
925
|
+
fn(ctx);
|
|
926
|
+
}
|
|
927
|
+
catch (err) {
|
|
928
|
+
this.handleHookError(err, hook);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
handleHookError(error, hook) {
|
|
933
|
+
console.error(`[Lifecycle:${hook}]`, error);
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Transitions from `NEW` → `INITIALIZED` and emits `onInit`.
|
|
937
|
+
*
|
|
938
|
+
* Safe to call multiple times; subsequent calls are no-ops unless current state is `NEW`.
|
|
939
|
+
*/
|
|
940
|
+
init() {
|
|
941
|
+
if (this.state !== LifecycleState.NEW)
|
|
942
|
+
return;
|
|
943
|
+
const prev = this.state;
|
|
944
|
+
this.state = LifecycleState.INITIALIZED;
|
|
945
|
+
this.emit("onInit", prev);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Transitions from `INITIALIZED` → `MOUNTED` and emits `onMount`.
|
|
949
|
+
*
|
|
950
|
+
* No-ops if current state is not `INITIALIZED` (guards against invalid order).
|
|
951
|
+
*/
|
|
952
|
+
mount() {
|
|
953
|
+
if (this.state !== LifecycleState.INITIALIZED)
|
|
954
|
+
return;
|
|
955
|
+
const prev = this.state;
|
|
956
|
+
this.state = LifecycleState.MOUNTED;
|
|
957
|
+
this.emit("onMount", prev);
|
|
958
|
+
}
|
|
836
959
|
/**
|
|
837
|
-
*
|
|
838
|
-
*
|
|
960
|
+
* Transitions from `MOUNTED` → `UPDATED` and emits `onUpdate`.
|
|
961
|
+
*
|
|
962
|
+
* No-ops if not currently `MOUNTED`. Subsequent `update()` calls will keep the state
|
|
963
|
+
* at `UPDATED` while still emitting `onUpdate`.
|
|
964
|
+
*/
|
|
965
|
+
update() {
|
|
966
|
+
if (this.state !== LifecycleState.MOUNTED &&
|
|
967
|
+
this.state !== LifecycleState.UPDATED) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const prev = this.state;
|
|
971
|
+
this.state = LifecycleState.UPDATED;
|
|
972
|
+
this.emit("onUpdate", prev);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Transitions to `DESTROYED` and emits `onDestroy`, then clears all hooks.
|
|
976
|
+
*
|
|
977
|
+
* Idempotent: calling `destroy()` multiple times after destruction has no effect.
|
|
978
|
+
*/
|
|
979
|
+
destroy() {
|
|
980
|
+
if (this.state === LifecycleState.DESTROYED)
|
|
981
|
+
return;
|
|
982
|
+
const prev = this.state;
|
|
983
|
+
this.state = LifecycleState.DESTROYED;
|
|
984
|
+
this.emit("onDestroy", prev);
|
|
985
|
+
this.clearHooks();
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Returns the current lifecycle state.
|
|
989
|
+
*/
|
|
990
|
+
getState() {
|
|
991
|
+
return this.state;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Checks if the lifecycle is in the specified state.
|
|
995
|
+
*
|
|
996
|
+
* @param state - The state to compare against
|
|
997
|
+
* @returns True if the current state matches; otherwise false
|
|
998
|
+
*/
|
|
999
|
+
is(state) {
|
|
1000
|
+
return this.state === state;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Clears all registered lifecycle hooks.
|
|
1004
|
+
*
|
|
1005
|
+
* Called automatically during `destroy()`.
|
|
1006
|
+
*/
|
|
1007
|
+
clearHooks() {
|
|
1008
|
+
for (const set of this.hooks.values()) {
|
|
1009
|
+
set.clear();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* UI component representing a placeholder for the Select UI.
|
|
1016
|
+
*
|
|
1017
|
+
* The placeholder displays contextual guidance when no value is selected.
|
|
1018
|
+
* It supports dynamic updates and optional HTML content, depending on configuration.
|
|
1019
|
+
*
|
|
1020
|
+
* The component manages a single DOM node and participates
|
|
1021
|
+
* in the standard `Lifecycle`.
|
|
1022
|
+
*
|
|
1023
|
+
* @extends Lifecycle
|
|
1024
|
+
*/
|
|
1025
|
+
class PlaceHolder extends Lifecycle {
|
|
1026
|
+
/**
|
|
1027
|
+
* Creates a new PlaceHolder instance.
|
|
1028
|
+
*
|
|
1029
|
+
* If options are provided, the component is initialized immediately.
|
|
1030
|
+
*
|
|
1031
|
+
* @param options - Configuration object containing placeholder text
|
|
1032
|
+
* and HTML rendering settings.
|
|
839
1033
|
*/
|
|
840
1034
|
constructor(options) {
|
|
1035
|
+
super();
|
|
1036
|
+
/**
|
|
1037
|
+
* Root DOM element of the placeholder component.
|
|
1038
|
+
* Created during initialization and removed on destroy.
|
|
1039
|
+
*/
|
|
841
1040
|
this.node = null;
|
|
1041
|
+
/**
|
|
1042
|
+
* Configuration options containing placeholder text
|
|
1043
|
+
* and rendering preferences (e.g., allowHtml).
|
|
1044
|
+
*/
|
|
842
1045
|
this.options = null;
|
|
843
1046
|
if (options)
|
|
844
|
-
this.
|
|
1047
|
+
this.initialize(options);
|
|
845
1048
|
}
|
|
846
1049
|
/**
|
|
847
|
-
* Initializes the placeholder
|
|
1050
|
+
* Initializes the placeholder component.
|
|
848
1051
|
*
|
|
849
|
-
*
|
|
1052
|
+
* Creates the DOM node, applies base styling,
|
|
1053
|
+
* renders the initial placeholder content,
|
|
1054
|
+
* stores configuration options, and starts the lifecycle.
|
|
1055
|
+
*
|
|
1056
|
+
* @param options - Configuration object containing placeholder settings.
|
|
850
1057
|
*/
|
|
851
|
-
|
|
1058
|
+
initialize(options) {
|
|
852
1059
|
this.node = Libs.nodeCreator({
|
|
853
1060
|
node: "div",
|
|
854
1061
|
classList: "selective-ui-placeholder",
|
|
855
1062
|
innerHTML: options.placeholder,
|
|
856
1063
|
});
|
|
857
1064
|
this.options = options;
|
|
1065
|
+
this.init();
|
|
858
1066
|
}
|
|
859
1067
|
/**
|
|
860
|
-
*
|
|
1068
|
+
* Returns the current placeholder text from the configuration.
|
|
861
1069
|
*
|
|
862
|
-
* @returns
|
|
1070
|
+
* @returns The current placeholder value, or an empty string if not set.
|
|
863
1071
|
*/
|
|
864
1072
|
get() {
|
|
865
1073
|
return this.options?.placeholder ?? "";
|
|
866
1074
|
}
|
|
867
1075
|
/**
|
|
868
|
-
* Updates the placeholder
|
|
869
|
-
*
|
|
1076
|
+
* Updates the placeholder content.
|
|
1077
|
+
*
|
|
1078
|
+
* The value can optionally be persisted back into the configuration.
|
|
1079
|
+
* HTML rendering is controlled by the `allowHtml` option:
|
|
1080
|
+
* - When enabled, translated HTML is rendered
|
|
1081
|
+
* - When disabled, HTML tags are stripped for safety
|
|
870
1082
|
*
|
|
871
|
-
* @param
|
|
872
|
-
* @param
|
|
1083
|
+
* @param value - The new placeholder content.
|
|
1084
|
+
* @param isSave - Whether to persist the value in the configuration.
|
|
1085
|
+
* Defaults to `true`.
|
|
873
1086
|
*/
|
|
874
1087
|
set(value, isSave = true) {
|
|
875
1088
|
if (!this.node || !this.options)
|
|
876
1089
|
return;
|
|
877
|
-
if (isSave)
|
|
1090
|
+
if (isSave) {
|
|
878
1091
|
this.options.placeholder = value;
|
|
1092
|
+
}
|
|
879
1093
|
const translated = Libs.tagTranslate(value);
|
|
880
|
-
this.node.innerHTML = this.options.allowHtml
|
|
1094
|
+
this.node.innerHTML = this.options.allowHtml
|
|
1095
|
+
? translated
|
|
1096
|
+
: Libs.stripHtml(translated);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Destroys the placeholder component.
|
|
1100
|
+
*
|
|
1101
|
+
* Removes the DOM node, clears stored options,
|
|
1102
|
+
* and terminates the lifecycle.
|
|
1103
|
+
*/
|
|
1104
|
+
destroy() {
|
|
1105
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.node?.remove();
|
|
1109
|
+
this.node = null;
|
|
1110
|
+
this.options = null;
|
|
1111
|
+
super.destroy();
|
|
881
1112
|
}
|
|
882
1113
|
}
|
|
883
1114
|
|
|
884
1115
|
/**
|
|
885
|
-
*
|
|
1116
|
+
* Base directive class representing a lightweight UI control.
|
|
1117
|
+
*
|
|
1118
|
+
* A Directive is a small interactive UI element that:
|
|
1119
|
+
* - Owns a single root HTMLElement
|
|
1120
|
+
* - Participates in the standard lifecycle (`init → mount → destroy`)
|
|
1121
|
+
* - Encapsulates behavior rather than complex rendering logic
|
|
1122
|
+
*
|
|
1123
|
+
* This particular implementation acts as a toggle control
|
|
1124
|
+
* (e.g., to open/close a dropdown).
|
|
1125
|
+
*
|
|
1126
|
+
* @extends Lifecycle
|
|
886
1127
|
*/
|
|
887
|
-
class Directive {
|
|
1128
|
+
class Directive extends Lifecycle {
|
|
1129
|
+
/**
|
|
1130
|
+
* Creates a new Directive instance and immediately initializes it.
|
|
1131
|
+
*
|
|
1132
|
+
* The lifecycle is automatically started:
|
|
1133
|
+
* constructor → init → mount
|
|
1134
|
+
*/
|
|
888
1135
|
constructor() {
|
|
889
|
-
|
|
1136
|
+
super();
|
|
1137
|
+
this.init();
|
|
890
1138
|
}
|
|
891
1139
|
/**
|
|
892
|
-
*
|
|
893
|
-
*
|
|
1140
|
+
* Initializes the directive's DOM structure.
|
|
1141
|
+
*
|
|
1142
|
+
* Creates a clickable element that behaves like a button
|
|
1143
|
+
* and applies appropriate ARIA attributes for accessibility.
|
|
1144
|
+
*
|
|
1145
|
+
* Automatically transitions the lifecycle from:
|
|
1146
|
+
* NEW → INITIALIZED → MOUNTED
|
|
894
1147
|
*/
|
|
895
1148
|
init() {
|
|
896
|
-
// Libs.nodeCreator returns Element, but this node
|
|
897
|
-
|
|
1149
|
+
// Libs.nodeCreator returns Element, but this node
|
|
1150
|
+
// is guaranteed to be an HTMLElement in this context.
|
|
1151
|
+
this.node = Libs.nodeCreator({
|
|
898
1152
|
node: "div",
|
|
899
1153
|
classList: "selective-ui-directive",
|
|
900
1154
|
role: "button",
|
|
901
1155
|
ariaLabel: "Toggle dropdown",
|
|
902
1156
|
});
|
|
1157
|
+
super.init();
|
|
1158
|
+
this.mount();
|
|
903
1159
|
}
|
|
904
1160
|
/**
|
|
905
|
-
*
|
|
1161
|
+
* Updates the visual dropdown state of the directive.
|
|
1162
|
+
*
|
|
1163
|
+
* Toggles the `drop-down` CSS class on the root node,
|
|
1164
|
+
* allowing presentation logic to be handled purely via styles.
|
|
906
1165
|
*
|
|
907
|
-
* @param
|
|
1166
|
+
* @param value - True to indicate dropdown is open; false otherwise.
|
|
908
1167
|
*/
|
|
909
1168
|
setDropdown(value) {
|
|
910
1169
|
this.node.classList.toggle("drop-down", !!value);
|
|
911
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Destroys the directive and cleans up DOM resources.
|
|
1173
|
+
*
|
|
1174
|
+
* Removes the root node from the DOM and ends the lifecycle.
|
|
1175
|
+
*/
|
|
1176
|
+
destroy() {
|
|
1177
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
this.node.remove();
|
|
1181
|
+
this.node = null;
|
|
1182
|
+
super.destroy();
|
|
1183
|
+
}
|
|
912
1184
|
}
|
|
913
1185
|
|
|
914
1186
|
/**
|
|
915
|
-
*
|
|
1187
|
+
* UI control that exposes "Select All" / "Deselect All" actions
|
|
1188
|
+
* for multiple-selection lists.
|
|
1189
|
+
*
|
|
1190
|
+
* Responsibilities:
|
|
1191
|
+
* - Renders two action controls (links/buttons)
|
|
1192
|
+
* - Shows/hides itself based on configuration flags
|
|
1193
|
+
* - Allows registration of callbacks for both actions
|
|
1194
|
+
* - Participates in the standard `Lifecycle`
|
|
1195
|
+
*
|
|
1196
|
+
* Visibility rule:
|
|
1197
|
+
* - Visible only when `options.multiple` and `options.selectall` are truthy.
|
|
1198
|
+
*
|
|
1199
|
+
* @extends Lifecycle
|
|
916
1200
|
*/
|
|
917
|
-
class OptionHandle {
|
|
1201
|
+
class OptionHandle extends Lifecycle {
|
|
918
1202
|
/**
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1203
|
+
* Creates a new OptionHandle control.
|
|
1204
|
+
*
|
|
1205
|
+
* If `options` are provided, the component is initialized immediately and
|
|
1206
|
+
* enters the lifecycle (init). Otherwise, call a custom initializer later
|
|
1207
|
+
* to set it up.
|
|
1208
|
+
*
|
|
1209
|
+
* @param options - Configuration with texts and feature flags.
|
|
922
1210
|
*/
|
|
923
1211
|
constructor(options = null) {
|
|
1212
|
+
super();
|
|
1213
|
+
/**
|
|
1214
|
+
* Internal reference to the mounted node structure returned by `Libs.mountNode`.
|
|
1215
|
+
* Used to access typed tags if needed. Null before initialization.
|
|
1216
|
+
*/
|
|
924
1217
|
this.nodeMounted = null;
|
|
1218
|
+
/**
|
|
1219
|
+
* Root DOM element of the option handle component.
|
|
1220
|
+
* Created during initialization and removed on destroy.
|
|
1221
|
+
*/
|
|
925
1222
|
this.node = null;
|
|
1223
|
+
/**
|
|
1224
|
+
* Configuration options controlling labels and feature flags.
|
|
1225
|
+
* (e.g., textSelectAll, textDeselectAll, multiple, selectall)
|
|
1226
|
+
*/
|
|
926
1227
|
this.options = null;
|
|
1228
|
+
/**
|
|
1229
|
+
* Registered callbacks executed when "Select All" is activated.
|
|
1230
|
+
*/
|
|
927
1231
|
this.actionOnSelectAll = [];
|
|
1232
|
+
/**
|
|
1233
|
+
* Registered callbacks executed when "Deselect All" is activated.
|
|
1234
|
+
*/
|
|
928
1235
|
this.actionOnDeSelectAll = [];
|
|
929
1236
|
if (options)
|
|
930
|
-
this.
|
|
1237
|
+
this.initialize(options);
|
|
931
1238
|
}
|
|
932
1239
|
/**
|
|
933
|
-
* Initializes the option handle UI
|
|
934
|
-
*
|
|
1240
|
+
* Initializes the option handle UI.
|
|
1241
|
+
*
|
|
1242
|
+
* Builds the DOM:
|
|
1243
|
+
* - Root: `.selective-ui-option-handle.hide`
|
|
1244
|
+
* - Children: "Select All" and "Deselect All" controls
|
|
935
1245
|
*
|
|
936
|
-
*
|
|
1246
|
+
* Wires their click handlers to invoke registered callbacks
|
|
1247
|
+
* via the `iEvents.callFunctions` helper.
|
|
1248
|
+
*
|
|
1249
|
+
* @param options - Configuration providing labels and feature flags.
|
|
937
1250
|
*/
|
|
938
|
-
|
|
1251
|
+
initialize(options) {
|
|
939
1252
|
this.nodeMounted = Libs.mountNode({
|
|
940
1253
|
OptionHandle: {
|
|
941
1254
|
tag: { node: "div", classList: ["selective-ui-option-handle", "hide"] },
|
|
@@ -965,11 +1278,17 @@ class OptionHandle {
|
|
|
965
1278
|
});
|
|
966
1279
|
this.node = this.nodeMounted.view;
|
|
967
1280
|
this.options = options;
|
|
1281
|
+
this.init();
|
|
968
1282
|
}
|
|
969
1283
|
/**
|
|
970
|
-
*
|
|
1284
|
+
* Returns whether the handle should be available (and thus visible)
|
|
1285
|
+
* based on current configuration flags.
|
|
1286
|
+
*
|
|
1287
|
+
* Availability requires:
|
|
1288
|
+
* - `multiple` is truthy
|
|
1289
|
+
* - `selectall` is truthy
|
|
971
1290
|
*
|
|
972
|
-
* @returns
|
|
1291
|
+
* @returns True if both features are enabled; otherwise false.
|
|
973
1292
|
*/
|
|
974
1293
|
available() {
|
|
975
1294
|
if (!this.options)
|
|
@@ -977,19 +1296,28 @@ class OptionHandle {
|
|
|
977
1296
|
return Libs.string2Boolean(this.options.multiple) && Libs.string2Boolean(this.options.selectall);
|
|
978
1297
|
}
|
|
979
1298
|
/**
|
|
980
|
-
* Refreshes the visibility
|
|
981
|
-
*
|
|
1299
|
+
* Refreshes the visibility based on `available()` and emits the update lifecycle.
|
|
1300
|
+
*
|
|
1301
|
+
* - Shows the handle when available
|
|
1302
|
+
* - Hides it otherwise
|
|
1303
|
+
*
|
|
1304
|
+
* Note: `super.update()` transitions lifecycle to `UPDATED` (idempotent after first call).
|
|
982
1305
|
*/
|
|
983
|
-
|
|
984
|
-
if (
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1306
|
+
update() {
|
|
1307
|
+
if (this.node) {
|
|
1308
|
+
if (this.available()) {
|
|
1309
|
+
this.show();
|
|
1310
|
+
}
|
|
1311
|
+
else {
|
|
1312
|
+
this.hide();
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
super.update();
|
|
990
1316
|
}
|
|
991
1317
|
/**
|
|
992
|
-
* Makes the option handle visible
|
|
1318
|
+
* Makes the option handle visible.
|
|
1319
|
+
*
|
|
1320
|
+
* Removes the `hide` class from the root node.
|
|
993
1321
|
*/
|
|
994
1322
|
show() {
|
|
995
1323
|
if (!this.node)
|
|
@@ -997,7 +1325,9 @@ class OptionHandle {
|
|
|
997
1325
|
this.node.classList.remove("hide");
|
|
998
1326
|
}
|
|
999
1327
|
/**
|
|
1000
|
-
* Hides the option handle
|
|
1328
|
+
* Hides the option handle.
|
|
1329
|
+
*
|
|
1330
|
+
* Adds the `hide` class to the root node.
|
|
1001
1331
|
*/
|
|
1002
1332
|
hide() {
|
|
1003
1333
|
if (!this.node)
|
|
@@ -1005,45 +1335,94 @@ class OptionHandle {
|
|
|
1005
1335
|
this.node.classList.add("hide");
|
|
1006
1336
|
}
|
|
1007
1337
|
/**
|
|
1008
|
-
* Registers a callback
|
|
1338
|
+
* Registers a callback for the "Select All" action.
|
|
1339
|
+
*
|
|
1340
|
+
* The callback will be invoked with the arguments provided
|
|
1341
|
+
* by the action dispatcher (if any).
|
|
1009
1342
|
*
|
|
1010
|
-
* @param
|
|
1343
|
+
* @param action - Function to execute when "Select All" is triggered.
|
|
1011
1344
|
*/
|
|
1012
|
-
|
|
1013
|
-
if (typeof action === "function")
|
|
1345
|
+
onSelectAll(action = null) {
|
|
1346
|
+
if (typeof action === "function") {
|
|
1014
1347
|
this.actionOnSelectAll.push(action);
|
|
1348
|
+
}
|
|
1015
1349
|
}
|
|
1016
1350
|
/**
|
|
1017
|
-
* Registers a callback
|
|
1351
|
+
* Registers a callback for the "Deselect All" action.
|
|
1352
|
+
*
|
|
1353
|
+
* The callback will be invoked with the arguments provided
|
|
1354
|
+
* by the action dispatcher (if any).
|
|
1018
1355
|
*
|
|
1019
|
-
* @param
|
|
1356
|
+
* @param action - Function to execute when "Deselect All" is triggered.
|
|
1020
1357
|
*/
|
|
1021
|
-
|
|
1022
|
-
if (typeof action === "function")
|
|
1358
|
+
onDeSelectAll(action = null) {
|
|
1359
|
+
if (typeof action === "function") {
|
|
1023
1360
|
this.actionOnDeSelectAll.push(action);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Destroys the option handle component.
|
|
1365
|
+
*
|
|
1366
|
+
* Removes the DOM node, clears stored options,
|
|
1367
|
+
* and terminates the lifecycle.
|
|
1368
|
+
*/
|
|
1369
|
+
destroy() {
|
|
1370
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
this.node.remove();
|
|
1374
|
+
this.options = null;
|
|
1375
|
+
this.actionOnSelectAll = null;
|
|
1376
|
+
this.actionOnDeSelectAll = null;
|
|
1377
|
+
this.node = null;
|
|
1378
|
+
super.destroy();
|
|
1024
1379
|
}
|
|
1025
1380
|
}
|
|
1026
1381
|
|
|
1027
1382
|
/**
|
|
1028
|
-
*
|
|
1383
|
+
* UI component that represents an empty state.
|
|
1384
|
+
*
|
|
1385
|
+
* The empty state is used to display contextual feedback when:
|
|
1386
|
+
* - No data is available
|
|
1387
|
+
* - A search yields no matching results
|
|
1388
|
+
*
|
|
1389
|
+
* It manages a single DOM node and participates in the standard lifecycle.
|
|
1390
|
+
*
|
|
1391
|
+
* @extends Lifecycle
|
|
1029
1392
|
*/
|
|
1030
|
-
class EmptyState {
|
|
1393
|
+
class EmptyState extends Lifecycle {
|
|
1031
1394
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1395
|
+
* Creates a new EmptyState instance.
|
|
1396
|
+
*
|
|
1397
|
+
* If options are provided, the component is initialized immediately.
|
|
1398
|
+
*
|
|
1399
|
+
* @param options - Configuration containing messages for
|
|
1400
|
+
* "no data" and "not found" states.
|
|
1034
1401
|
*/
|
|
1035
1402
|
constructor(options = null) {
|
|
1403
|
+
super();
|
|
1404
|
+
/**
|
|
1405
|
+
* Root DOM element of the empty state component.
|
|
1406
|
+
* Created during initialization and removed on destroy.
|
|
1407
|
+
*/
|
|
1036
1408
|
this.node = null;
|
|
1409
|
+
/**
|
|
1410
|
+
* Configuration options providing display text
|
|
1411
|
+
* for different empty state scenarios.
|
|
1412
|
+
*/
|
|
1037
1413
|
this.options = null;
|
|
1038
1414
|
if (options)
|
|
1039
|
-
this.
|
|
1415
|
+
this.initialize(options);
|
|
1040
1416
|
}
|
|
1041
1417
|
/**
|
|
1042
|
-
* Initializes the empty state
|
|
1418
|
+
* Initializes the empty state component.
|
|
1419
|
+
*
|
|
1420
|
+
* Creates the root DOM element, applies accessibility attributes,
|
|
1421
|
+
* stores configuration options, and starts the lifecycle.
|
|
1043
1422
|
*
|
|
1044
|
-
* @param
|
|
1423
|
+
* @param options - Configuration object containing empty state messages.
|
|
1045
1424
|
*/
|
|
1046
|
-
|
|
1425
|
+
initialize(options) {
|
|
1047
1426
|
this.options = options;
|
|
1048
1427
|
this.node = Libs.nodeCreator({
|
|
1049
1428
|
node: "div",
|
|
@@ -1051,22 +1430,32 @@ class EmptyState {
|
|
|
1051
1430
|
role: "status",
|
|
1052
1431
|
ariaLive: "polite",
|
|
1053
1432
|
});
|
|
1433
|
+
this.init();
|
|
1054
1434
|
}
|
|
1055
1435
|
/**
|
|
1056
|
-
* Displays the empty state message
|
|
1436
|
+
* Displays the empty state message.
|
|
1057
1437
|
*
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1438
|
+
* The message content depends on the provided type:
|
|
1439
|
+
* - `"nodata"`: no data available
|
|
1440
|
+
* - `"notfound"`: no matching search results
|
|
1441
|
+
*
|
|
1442
|
+
* @param type - Type of empty state to display.
|
|
1443
|
+
* Defaults to `"nodata"`.
|
|
1060
1444
|
*/
|
|
1061
1445
|
show(type = "nodata") {
|
|
1062
1446
|
if (!this.node || !this.options)
|
|
1063
1447
|
return;
|
|
1064
|
-
const text = type === "notfound"
|
|
1448
|
+
const text = type === "notfound"
|
|
1449
|
+
? this.options.textNotFound
|
|
1450
|
+
: this.options.textNoData;
|
|
1065
1451
|
this.node.textContent = text;
|
|
1066
1452
|
this.node.classList.remove("hide");
|
|
1067
1453
|
}
|
|
1068
1454
|
/**
|
|
1069
|
-
* Hides the empty state
|
|
1455
|
+
* Hides the empty state component.
|
|
1456
|
+
*
|
|
1457
|
+
* This does not remove the element from the DOM;
|
|
1458
|
+
* it only updates its visibility via CSS.
|
|
1070
1459
|
*/
|
|
1071
1460
|
hide() {
|
|
1072
1461
|
if (!this.node)
|
|
@@ -1076,33 +1465,71 @@ class EmptyState {
|
|
|
1076
1465
|
/**
|
|
1077
1466
|
* Indicates whether the empty state is currently visible.
|
|
1078
1467
|
*
|
|
1079
|
-
* @returns
|
|
1468
|
+
* @returns True if the empty state is shown; otherwise false.
|
|
1080
1469
|
*/
|
|
1081
1470
|
get isVisible() {
|
|
1082
1471
|
return !!this.node && !this.node.classList.contains("hide");
|
|
1083
1472
|
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Destroys the empty state component.
|
|
1475
|
+
*
|
|
1476
|
+
* Removes the DOM node, clears stored options,
|
|
1477
|
+
* and terminates the lifecycle.
|
|
1478
|
+
*/
|
|
1479
|
+
destroy() {
|
|
1480
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
this.options = null;
|
|
1484
|
+
this.node?.remove();
|
|
1485
|
+
this.node = null;
|
|
1486
|
+
super.destroy();
|
|
1487
|
+
}
|
|
1084
1488
|
}
|
|
1085
1489
|
|
|
1086
1490
|
/**
|
|
1087
|
-
*
|
|
1491
|
+
* UI component representing a loading state.
|
|
1492
|
+
*
|
|
1493
|
+
* The loading state is displayed while data is being fetched,
|
|
1494
|
+
* processed, or updated asynchronously.
|
|
1495
|
+
*
|
|
1496
|
+
* It manages a single DOM element and participates in the
|
|
1497
|
+
* standard lifecycle provided by `Lifecycle`.
|
|
1498
|
+
*
|
|
1499
|
+
* @extends Lifecycle
|
|
1088
1500
|
*/
|
|
1089
|
-
class LoadingState {
|
|
1501
|
+
class LoadingState extends Lifecycle {
|
|
1090
1502
|
/**
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1503
|
+
* Creates a new LoadingState instance.
|
|
1504
|
+
*
|
|
1505
|
+
* If options are provided, the component is initialized immediately.
|
|
1506
|
+
*
|
|
1507
|
+
* @param options - Configuration object containing the loading message text.
|
|
1093
1508
|
*/
|
|
1094
1509
|
constructor(options = null) {
|
|
1510
|
+
super();
|
|
1511
|
+
/**
|
|
1512
|
+
* Root DOM element of the loading state component.
|
|
1513
|
+
* Created during initialization and removed on destroy.
|
|
1514
|
+
*/
|
|
1095
1515
|
this.node = null;
|
|
1516
|
+
/**
|
|
1517
|
+
* Configuration options containing the loading message text.
|
|
1518
|
+
*/
|
|
1096
1519
|
this.options = null;
|
|
1097
1520
|
if (options)
|
|
1098
|
-
this.
|
|
1521
|
+
this.initialize(options);
|
|
1099
1522
|
}
|
|
1100
1523
|
/**
|
|
1101
|
-
* Initializes the loading state
|
|
1524
|
+
* Initializes the loading state component.
|
|
1525
|
+
*
|
|
1526
|
+
* Creates the root DOM element, sets the initial loading text,
|
|
1527
|
+
* applies accessibility attributes, stores configuration options,
|
|
1528
|
+
* and starts the lifecycle.
|
|
1102
1529
|
*
|
|
1103
|
-
* @param
|
|
1530
|
+
* @param options - Configuration object containing loading text.
|
|
1104
1531
|
*/
|
|
1105
|
-
|
|
1532
|
+
initialize(options) {
|
|
1106
1533
|
this.options = options;
|
|
1107
1534
|
this.node = Libs.nodeCreator({
|
|
1108
1535
|
node: "div",
|
|
@@ -1111,11 +1538,16 @@ class LoadingState {
|
|
|
1111
1538
|
role: "status",
|
|
1112
1539
|
ariaLive: "polite",
|
|
1113
1540
|
});
|
|
1541
|
+
this.init();
|
|
1114
1542
|
}
|
|
1115
1543
|
/**
|
|
1116
|
-
* Displays the loading state
|
|
1544
|
+
* Displays the loading state.
|
|
1117
1545
|
*
|
|
1118
|
-
*
|
|
1546
|
+
* When items are already present, the loading state can be shown
|
|
1547
|
+
* in a compact form by applying a reduced ("small") style.
|
|
1548
|
+
*
|
|
1549
|
+
* @param hasItems - True if existing items are present,
|
|
1550
|
+
* enabling a compact loading indicator.
|
|
1119
1551
|
*/
|
|
1120
1552
|
show(hasItems) {
|
|
1121
1553
|
if (!this.node || !this.options)
|
|
@@ -1125,7 +1557,10 @@ class LoadingState {
|
|
|
1125
1557
|
this.node.classList.remove("hide");
|
|
1126
1558
|
}
|
|
1127
1559
|
/**
|
|
1128
|
-
* Hides the loading state
|
|
1560
|
+
* Hides the loading state.
|
|
1561
|
+
*
|
|
1562
|
+
* This only toggles visibility via CSS and does not
|
|
1563
|
+
* remove the element from the DOM.
|
|
1129
1564
|
*/
|
|
1130
1565
|
hide() {
|
|
1131
1566
|
if (!this.node)
|
|
@@ -1135,11 +1570,26 @@ class LoadingState {
|
|
|
1135
1570
|
/**
|
|
1136
1571
|
* Indicates whether the loading state is currently visible.
|
|
1137
1572
|
*
|
|
1138
|
-
* @returns
|
|
1573
|
+
* @returns True if the loading indicator is shown; otherwise false.
|
|
1139
1574
|
*/
|
|
1140
1575
|
get isVisible() {
|
|
1141
1576
|
return !!this.node && !this.node.classList.contains("hide");
|
|
1142
1577
|
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Destroys the loading state component.
|
|
1580
|
+
*
|
|
1581
|
+
* Removes the DOM node, clears stored options,
|
|
1582
|
+
* and terminates the lifecycle.
|
|
1583
|
+
*/
|
|
1584
|
+
destroy() {
|
|
1585
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
this.options = null;
|
|
1589
|
+
this.node?.remove();
|
|
1590
|
+
this.node = null;
|
|
1591
|
+
super.destroy();
|
|
1592
|
+
}
|
|
1143
1593
|
}
|
|
1144
1594
|
|
|
1145
1595
|
/**
|
|
@@ -1268,50 +1718,90 @@ class ResizeObserverService {
|
|
|
1268
1718
|
}
|
|
1269
1719
|
|
|
1270
1720
|
/**
|
|
1271
|
-
*
|
|
1721
|
+
* Popup panel that renders and manages the dropdown surface.
|
|
1722
|
+
*
|
|
1723
|
+
* Responsibilities:
|
|
1724
|
+
* - Build and attach the dropdown DOM structure
|
|
1725
|
+
* - Integrate state components (OptionHandle, LoadingState, EmptyState)
|
|
1726
|
+
* - Bind to ModelManager resources (adapter + recycler view)
|
|
1727
|
+
* - Handle virtual scrolling and infinite scroll
|
|
1728
|
+
* - Compute placement (top/bottom) and animate open/close/resize via Effector
|
|
1729
|
+
* - Keep "empty/not found" states in sync with adapter visibility stats
|
|
1730
|
+
*
|
|
1731
|
+
* Lifecycle:
|
|
1732
|
+
* - Created via constructor → `initialize()` (when args provided) → `init()` → `mount()`
|
|
1733
|
+
* - `open()` attaches and animates the panel
|
|
1734
|
+
* - `close()` collapses the panel
|
|
1735
|
+
* - `destroy()` fully tears down all resources
|
|
1736
|
+
*
|
|
1737
|
+
* @extends Lifecycle
|
|
1272
1738
|
*/
|
|
1273
|
-
class Popup {
|
|
1739
|
+
class Popup extends Lifecycle {
|
|
1274
1740
|
/**
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1741
|
+
* Creates a Popup instance that manages the dropdown panel for a Select-like UI.
|
|
1742
|
+
*
|
|
1743
|
+
* If `select` and `options` are provided, the popup is initialized immediately.
|
|
1277
1744
|
*
|
|
1278
|
-
* @param
|
|
1279
|
-
* @param
|
|
1280
|
-
* @param
|
|
1745
|
+
* @param select - Source `<select>` element this popup is bound to.
|
|
1746
|
+
* @param options - Configuration options (panel sizing, flags, texts, etc.).
|
|
1747
|
+
* @param modelManager - Model manager that supplies the adapter and recycler view.
|
|
1281
1748
|
*/
|
|
1282
1749
|
constructor(select = null, options = null, modelManager = null) {
|
|
1750
|
+
super();
|
|
1751
|
+
/** Active configuration for the popup behavior and text labels */
|
|
1283
1752
|
this.options = null;
|
|
1753
|
+
/** Indicates whether the popup DOM has been attached to the document body at least once */
|
|
1284
1754
|
this.isCreated = false;
|
|
1755
|
+
/** Mixed adapter handling items/models and visibility stats */
|
|
1285
1756
|
this.optionAdapter = null;
|
|
1757
|
+
/** Root popup container (the floating panel) */
|
|
1286
1758
|
this.node = null;
|
|
1759
|
+
/** Effector service used to measure/animate the popup */
|
|
1287
1760
|
this.effSvc = null;
|
|
1761
|
+
/** Resize observer to react to parent panel size changes */
|
|
1288
1762
|
this.resizeObser = null;
|
|
1763
|
+
/** Binder map for parent elements (anchors to compute placement from) */
|
|
1289
1764
|
this.parent = null;
|
|
1765
|
+
/** Header control exposing "Select All / Deselect All" actions */
|
|
1290
1766
|
this.optionHandle = null;
|
|
1767
|
+
/** "Empty / Not found" feedback component */
|
|
1291
1768
|
this.emptyState = null;
|
|
1769
|
+
/** Loading indicator component */
|
|
1292
1770
|
this.loadingState = null;
|
|
1771
|
+
/** Virtualized recycler view for performant lists */
|
|
1293
1772
|
this.recyclerView = null;
|
|
1773
|
+
/** Container that holds the list of options */
|
|
1294
1774
|
this.optionsContainer = null;
|
|
1775
|
+
/** Scroll handler used by infinite scroll */
|
|
1295
1776
|
this.scrollListener = null;
|
|
1777
|
+
/** Handle to defer hiding the loading indicator */
|
|
1296
1778
|
this.hideLoadHandle = null;
|
|
1779
|
+
/** Default virtual scroll configuration (tuned for typical option heights) */
|
|
1297
1780
|
this.virtualScrollConfig = {
|
|
1781
|
+
/** Estimated item height in pixels (improves initial layout calculation) */
|
|
1298
1782
|
estimateItemHeight: 36,
|
|
1783
|
+
/** Number of extra items to render above/below the viewport */
|
|
1299
1784
|
overscan: 8,
|
|
1785
|
+
/** Whether the list contains items with dynamic (non-uniform) heights */
|
|
1300
1786
|
dynamicHeights: true
|
|
1301
1787
|
};
|
|
1302
1788
|
this.modelManager = modelManager;
|
|
1303
1789
|
if (select && options) {
|
|
1304
|
-
this.
|
|
1790
|
+
this.initialize(select, options);
|
|
1305
1791
|
}
|
|
1306
1792
|
}
|
|
1307
1793
|
/**
|
|
1308
|
-
* Initializes the popup UI:
|
|
1309
|
-
*
|
|
1794
|
+
* Initializes the popup UI:
|
|
1795
|
+
* - Creates the container and child components (OptionHandle, LoadingState, EmptyState)
|
|
1796
|
+
* - Binds to ModelManager resources (adapter/recyclerView)
|
|
1797
|
+
* - Enables empty-state synchronization with adapter
|
|
1798
|
+
* - Applies virtual scroll options (when enabled)
|
|
1310
1799
|
*
|
|
1311
|
-
* @param
|
|
1312
|
-
* @param
|
|
1800
|
+
* @param select - The source select element to bind.
|
|
1801
|
+
* @param options - Panel configuration (dimensions, IDs, labels, flags).
|
|
1802
|
+
* @throws Error if a ModelManager is not provided.
|
|
1313
1803
|
*/
|
|
1314
|
-
|
|
1804
|
+
initialize(select, options) {
|
|
1315
1805
|
if (!this.modelManager)
|
|
1316
1806
|
throw new Error("Popup requires a ModelManager instance.");
|
|
1317
1807
|
this.optionHandle = new OptionHandle(options);
|
|
@@ -1343,6 +1833,7 @@ class Popup {
|
|
|
1343
1833
|
this.optionsContainer = nodeMounted.tags.OptionsContainer;
|
|
1344
1834
|
this.parent = Libs.getBinderMap(select);
|
|
1345
1835
|
this.options = options;
|
|
1836
|
+
this.init();
|
|
1346
1837
|
const recyclerViewOpt = options.virtualScroll
|
|
1347
1838
|
? {
|
|
1348
1839
|
scrollEl: this.node,
|
|
@@ -1351,22 +1842,28 @@ class Popup {
|
|
|
1351
1842
|
dynamicHeights: this.virtualScrollConfig.dynamicHeights
|
|
1352
1843
|
}
|
|
1353
1844
|
: {};
|
|
1354
|
-
// Load ModelManager resources into container
|
|
1845
|
+
// Load ModelManager resources into the list container
|
|
1355
1846
|
this.modelManager.load(this.optionsContainer, { isMultiple: options.multiple }, recyclerViewOpt);
|
|
1356
1847
|
const MMResources = this.modelManager.getResources();
|
|
1357
1848
|
this.optionAdapter = MMResources.adapter;
|
|
1358
1849
|
this.recyclerView = MMResources.recyclerView;
|
|
1359
|
-
this.optionHandle.
|
|
1850
|
+
this.optionHandle.onSelectAll(() => {
|
|
1360
1851
|
MMResources.adapter.checkAll(true);
|
|
1361
1852
|
});
|
|
1362
|
-
this.optionHandle.
|
|
1853
|
+
this.optionHandle.onDeSelectAll(() => {
|
|
1363
1854
|
MMResources.adapter.checkAll(false);
|
|
1364
1855
|
});
|
|
1365
1856
|
this.setupEmptyStateLogic();
|
|
1857
|
+
this.mount();
|
|
1366
1858
|
}
|
|
1367
1859
|
/**
|
|
1368
|
-
* Shows the loading state and temporarily
|
|
1369
|
-
*
|
|
1860
|
+
* Shows the loading state and temporarily suspends adapter/model events.
|
|
1861
|
+
*
|
|
1862
|
+
* Behavior:
|
|
1863
|
+
* - Cancels any pending hide-timeout
|
|
1864
|
+
* - Instructs `ModelManager` to skip events
|
|
1865
|
+
* - Shows loading indicator (compact mode if there are visible items)
|
|
1866
|
+
* - Triggers a resize to accommodate layout changes
|
|
1370
1867
|
*/
|
|
1371
1868
|
async showLoading() {
|
|
1372
1869
|
if (!this.options || !this.loadingState || !this.optionHandle || !this.optionAdapter || !this.modelManager)
|
|
@@ -1376,15 +1873,15 @@ class Popup {
|
|
|
1376
1873
|
this.modelManager.skipEvent(true);
|
|
1377
1874
|
if (Libs.string2Boolean(this.options.loadingfield) === false)
|
|
1378
1875
|
return;
|
|
1379
|
-
// this.updateEmptyState({isEmpty: false, hasVisible: true});
|
|
1380
1876
|
this.emptyState.hide();
|
|
1381
1877
|
this.loadingState.show(this.optionAdapter.getVisibilityStats().hasVisible);
|
|
1382
|
-
// this.optionHandle.hide();
|
|
1383
1878
|
this.triggerResize();
|
|
1384
1879
|
}
|
|
1385
1880
|
/**
|
|
1386
|
-
* Hides the loading state after a
|
|
1387
|
-
*
|
|
1881
|
+
* Hides the loading state (after a configured delay), resumes events, syncs empty state,
|
|
1882
|
+
* and triggers a resize.
|
|
1883
|
+
*
|
|
1884
|
+
* Debounce: Uses `animationtime` as a short delay before hiding the loading indicator.
|
|
1388
1885
|
*/
|
|
1389
1886
|
async hideLoading() {
|
|
1390
1887
|
if (!this.options || !this.loadingState || !this.optionAdapter || !this.modelManager)
|
|
@@ -1400,8 +1897,10 @@ class Popup {
|
|
|
1400
1897
|
}, this.options.animationtime);
|
|
1401
1898
|
}
|
|
1402
1899
|
/**
|
|
1403
|
-
* Subscribes to adapter
|
|
1404
|
-
*
|
|
1900
|
+
* Subscribes to adapter events to keep the empty state synchronized.
|
|
1901
|
+
*
|
|
1902
|
+
* - `onVisibilityChanged`: updates empty/not-found visibility
|
|
1903
|
+
* - `onPropChanged('items')`: updates visibility after items are mutated
|
|
1405
1904
|
*/
|
|
1406
1905
|
setupEmptyStateLogic() {
|
|
1407
1906
|
if (!this.optionAdapter)
|
|
@@ -1416,10 +1915,14 @@ class Popup {
|
|
|
1416
1915
|
});
|
|
1417
1916
|
}
|
|
1418
1917
|
/**
|
|
1419
|
-
* Updates the empty state and
|
|
1420
|
-
*
|
|
1918
|
+
* Updates the empty/not-found state and the options container visibility.
|
|
1919
|
+
*
|
|
1920
|
+
* Rules:
|
|
1921
|
+
* - `isEmpty` → show "No data", hide options & handle
|
|
1922
|
+
* - `!hasVisible` → show "Not found", hide options & handle
|
|
1923
|
+
* - otherwise → show options, hide empty state, refresh handle
|
|
1421
1924
|
*
|
|
1422
|
-
* @param
|
|
1925
|
+
* @param stats - Optionally provide precomputed visibility stats.
|
|
1423
1926
|
*/
|
|
1424
1927
|
updateEmptyState(stats) {
|
|
1425
1928
|
if (!this.optionAdapter || !this.emptyState || !this.optionHandle || !this.optionsContainer)
|
|
@@ -1438,30 +1941,44 @@ class Popup {
|
|
|
1438
1941
|
else {
|
|
1439
1942
|
this.emptyState.hide();
|
|
1440
1943
|
this.optionsContainer.classList.remove("hide");
|
|
1441
|
-
this.optionHandle.
|
|
1944
|
+
this.optionHandle.update();
|
|
1442
1945
|
}
|
|
1443
1946
|
}
|
|
1444
1947
|
/**
|
|
1445
|
-
*
|
|
1948
|
+
* Subscribes to adapter property pre-change notifications.
|
|
1949
|
+
*
|
|
1950
|
+
* @param propName - Adapter property name to observe.
|
|
1951
|
+
* @param callback - Handler invoked before the property changes.
|
|
1446
1952
|
*/
|
|
1447
1953
|
onAdapterPropChanging(propName, callback) {
|
|
1448
1954
|
this.optionAdapter?.onPropChanging(propName, callback);
|
|
1449
1955
|
}
|
|
1450
1956
|
/**
|
|
1451
|
-
*
|
|
1957
|
+
* Subscribes to adapter property post-change notifications.
|
|
1958
|
+
*
|
|
1959
|
+
* @param propName - Adapter property name to observe.
|
|
1960
|
+
* @param callback - Handler invoked after the property changes.
|
|
1452
1961
|
*/
|
|
1453
1962
|
onAdapterPropChanged(propName, callback) {
|
|
1454
1963
|
this.optionAdapter?.onPropChanged(propName, callback);
|
|
1455
1964
|
}
|
|
1456
1965
|
/**
|
|
1457
|
-
* Injects an effector service used
|
|
1966
|
+
* Injects an effector service used for size measurement and animations.
|
|
1967
|
+
*
|
|
1968
|
+
* @param effectorSvc - Effector instance to bind to the popup element.
|
|
1458
1969
|
*/
|
|
1459
1970
|
setupEffector(effectorSvc) {
|
|
1460
1971
|
this.effSvc = effectorSvc;
|
|
1461
1972
|
}
|
|
1462
1973
|
/**
|
|
1463
|
-
* Opens the popup:
|
|
1464
|
-
*
|
|
1974
|
+
* Opens (expands) the popup:
|
|
1975
|
+
* - On first open: appends to `document.body`, sets up resize observer, and blocks outside mousedown
|
|
1976
|
+
* - Synchronizes the OptionHandle visibility and (optionally) the empty state
|
|
1977
|
+
* - Computes placement from the parent anchor and runs the expand animation
|
|
1978
|
+
* - Resumes recycler view after the animation completes
|
|
1979
|
+
*
|
|
1980
|
+
* @param callback - Optional callback invoked when the opening animation completes.
|
|
1981
|
+
* @param isShowEmptyState - If true, evaluates and applies empty/not-found state before animation.
|
|
1465
1982
|
*/
|
|
1466
1983
|
open(callback = null, isShowEmptyState) {
|
|
1467
1984
|
if (!this.node || !this.options || !this.optionHandle || !this.parent || !this.effSvc)
|
|
@@ -1471,12 +1988,13 @@ class Popup {
|
|
|
1471
1988
|
this.isCreated = true;
|
|
1472
1989
|
this.resizeObser = new ResizeObserverService();
|
|
1473
1990
|
this.effSvc.setElement(this.node);
|
|
1991
|
+
// Prevent the popup from closing when clicking inside
|
|
1474
1992
|
this.node.addEventListener("mousedown", (e) => {
|
|
1475
1993
|
e.stopPropagation();
|
|
1476
1994
|
e.preventDefault();
|
|
1477
1995
|
});
|
|
1478
1996
|
}
|
|
1479
|
-
this.optionHandle.
|
|
1997
|
+
this.optionHandle.update();
|
|
1480
1998
|
if (isShowEmptyState) {
|
|
1481
1999
|
this.updateEmptyState();
|
|
1482
2000
|
}
|
|
@@ -1507,8 +2025,14 @@ class Popup {
|
|
|
1507
2025
|
});
|
|
1508
2026
|
}
|
|
1509
2027
|
/**
|
|
1510
|
-
* Closes the popup:
|
|
1511
|
-
*
|
|
2028
|
+
* Closes (collapses) the popup:
|
|
2029
|
+
* - Suspends the recycler view
|
|
2030
|
+
* - Disconnects the resize observer
|
|
2031
|
+
* - Runs the collapse animation
|
|
2032
|
+
*
|
|
2033
|
+
* Safely no-ops when the popup has not been created.
|
|
2034
|
+
*
|
|
2035
|
+
* @param callback - Optional callback invoked when the closing animation completes.
|
|
1512
2036
|
*/
|
|
1513
2037
|
close(callback = null) {
|
|
1514
2038
|
if (!this.isCreated || !this.options || !this.resizeObser || !this.effSvc)
|
|
@@ -1522,19 +2046,20 @@ class Popup {
|
|
|
1522
2046
|
});
|
|
1523
2047
|
}
|
|
1524
2048
|
/**
|
|
1525
|
-
* Programmatically triggers a resize recalculation if
|
|
1526
|
-
* causing the
|
|
2049
|
+
* Programmatically triggers a resize recalculation (if created),
|
|
2050
|
+
* causing the popup to recompute placement and dimensions.
|
|
1527
2051
|
*/
|
|
1528
2052
|
triggerResize() {
|
|
1529
2053
|
if (this.isCreated)
|
|
1530
2054
|
this.resizeObser?.trigger();
|
|
1531
2055
|
}
|
|
1532
2056
|
/**
|
|
1533
|
-
* Enables infinite
|
|
1534
|
-
*
|
|
2057
|
+
* Enables infinite scrolling:
|
|
2058
|
+
* - Listens to scroll events on the popup container
|
|
2059
|
+
* - When within 100px of the bottom, attempts to load more items (if enabled and not already loading)
|
|
1535
2060
|
*
|
|
1536
|
-
* @param searchController - Provides pagination state and a method
|
|
1537
|
-
* @param _options - Optional SelectiveOptions (reserved for future
|
|
2061
|
+
* @param searchController - Provides pagination state and a `loadMore()` method.
|
|
2062
|
+
* @param _options - Optional SelectiveOptions (reserved for future tuning).
|
|
1538
2063
|
*/
|
|
1539
2064
|
setupInfiniteScroll(searchController, _options) {
|
|
1540
2065
|
if (!this.node)
|
|
@@ -1547,6 +2072,7 @@ class Popup {
|
|
|
1547
2072
|
const scrollTop = container.scrollTop;
|
|
1548
2073
|
const scrollHeight = container.scrollHeight;
|
|
1549
2074
|
const clientHeight = container.clientHeight;
|
|
2075
|
+
// Near-bottom threshold: 100px
|
|
1550
2076
|
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
1551
2077
|
if (!state.isLoading && state.hasMore) {
|
|
1552
2078
|
const result = await searchController.loadMore();
|
|
@@ -1560,18 +2086,24 @@ class Popup {
|
|
|
1560
2086
|
this.node.addEventListener("scroll", this.scrollListener);
|
|
1561
2087
|
}
|
|
1562
2088
|
/**
|
|
1563
|
-
* Completely
|
|
2089
|
+
* Completely tears down the popup and releases all resources.
|
|
1564
2090
|
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
2091
|
+
* Operations:
|
|
2092
|
+
* - Clear pending timeouts
|
|
2093
|
+
* - Remove event listeners (scroll, mousedown)
|
|
2094
|
+
* - Disconnect resize observer
|
|
2095
|
+
* - Unbind effector from the element
|
|
2096
|
+
* - Remove DOM node (replace-with-clone fallback)
|
|
2097
|
+
* - Reset ModelManager event skipping
|
|
2098
|
+
* - Clear recycler view, adapter, and child components
|
|
2099
|
+
* - Null out references to avoid leaks and mark as not created
|
|
1571
2100
|
*
|
|
1572
|
-
*
|
|
2101
|
+
* Idempotent: safe to call multiple times.
|
|
1573
2102
|
*/
|
|
1574
|
-
|
|
2103
|
+
destroy() {
|
|
2104
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
1575
2107
|
if (this.hideLoadHandle) {
|
|
1576
2108
|
clearTimeout(this.hideLoadHandle);
|
|
1577
2109
|
this.hideLoadHandle = null;
|
|
@@ -1580,6 +2112,10 @@ class Popup {
|
|
|
1580
2112
|
this.node.removeEventListener("scroll", this.scrollListener);
|
|
1581
2113
|
this.scrollListener = null;
|
|
1582
2114
|
}
|
|
2115
|
+
this.emptyState.destroy();
|
|
2116
|
+
this.loadingState.destroy();
|
|
2117
|
+
this.optionHandle.destroy();
|
|
2118
|
+
this.recyclerView.destroy();
|
|
1583
2119
|
try {
|
|
1584
2120
|
this.resizeObser?.disconnect();
|
|
1585
2121
|
}
|
|
@@ -1607,6 +2143,7 @@ class Popup {
|
|
|
1607
2143
|
this.recyclerView?.clear?.();
|
|
1608
2144
|
this.recyclerView = null;
|
|
1609
2145
|
this.optionAdapter = null;
|
|
2146
|
+
// Original behavior kept intentionally.
|
|
1610
2147
|
this.node.remove();
|
|
1611
2148
|
}
|
|
1612
2149
|
catch (_) { }
|
|
@@ -1617,10 +2154,11 @@ class Popup {
|
|
|
1617
2154
|
this.parent = null;
|
|
1618
2155
|
this.options = null;
|
|
1619
2156
|
this.isCreated = false;
|
|
2157
|
+
super.destroy();
|
|
1620
2158
|
}
|
|
1621
2159
|
/**
|
|
1622
|
-
* Computes the parent panel's location and box metrics
|
|
1623
|
-
*
|
|
2160
|
+
* Computes the parent panel's location and box metrics
|
|
2161
|
+
* (size, position, padding, border). Accounts for iOS visual viewport offsets.
|
|
1624
2162
|
*/
|
|
1625
2163
|
getParentLocation() {
|
|
1626
2164
|
const viewPanel = this.parent.container.tags.ViewPanel;
|
|
@@ -1646,8 +2184,12 @@ class Popup {
|
|
|
1646
2184
|
};
|
|
1647
2185
|
}
|
|
1648
2186
|
/**
|
|
1649
|
-
* Determines popup placement (top/bottom) and height constraints based on
|
|
1650
|
-
*
|
|
2187
|
+
* Determines popup placement (top/bottom) and height constraints based on:
|
|
2188
|
+
* - Available viewport space above/below the anchor
|
|
2189
|
+
* - Content size from effector's hidden measurement
|
|
2190
|
+
* - Configured min/max heights
|
|
2191
|
+
*
|
|
2192
|
+
* Returns the final placement, top offset, and computed heights.
|
|
1651
2193
|
*/
|
|
1652
2194
|
calculatePosition(location) {
|
|
1653
2195
|
const vv = window.visualViewport;
|
|
@@ -1691,7 +2233,7 @@ class Popup {
|
|
|
1691
2233
|
return { position, top, maxHeight, realHeight, contentHeight };
|
|
1692
2234
|
}
|
|
1693
2235
|
/**
|
|
1694
|
-
* Handles parent resize
|
|
2236
|
+
* Handles parent resize by recalculating placement and dimensions,
|
|
1695
2237
|
* then animates the popup to the new size and position.
|
|
1696
2238
|
*/
|
|
1697
2239
|
handleResize(location) {
|
|
@@ -1711,33 +2253,66 @@ class Popup {
|
|
|
1711
2253
|
}
|
|
1712
2254
|
}
|
|
1713
2255
|
|
|
1714
|
-
|
|
2256
|
+
/**
|
|
2257
|
+
* Searchable input component for the Select UI.
|
|
2258
|
+
*
|
|
2259
|
+
* Responsibilities:
|
|
2260
|
+
* - Render a search input field with proper ARIA attributes
|
|
2261
|
+
* - Dispatch typed events for search, navigation, Enter, and Escape
|
|
2262
|
+
* - Support showing/hiding and dynamic placeholder updates
|
|
2263
|
+
*
|
|
2264
|
+
* Lifecycle:
|
|
2265
|
+
* - Constructed with optional options → initialized → `init()`
|
|
2266
|
+
* - Consumers wire callbacks (`onSearch`, `onNavigate`, `onEnter`, `onEsc`)
|
|
2267
|
+
*
|
|
2268
|
+
* @extends Lifecycle
|
|
2269
|
+
*/
|
|
2270
|
+
class SearchBox extends Lifecycle {
|
|
1715
2271
|
/**
|
|
1716
2272
|
* Creates a searchable input box component with optional configuration
|
|
1717
2273
|
* and initializes it if options are provided.
|
|
1718
2274
|
*
|
|
1719
|
-
* @param
|
|
2275
|
+
* @param options - Configuration (e.g., placeholder, accessibility IDs).
|
|
1720
2276
|
*/
|
|
1721
2277
|
constructor(options = null) {
|
|
2278
|
+
super();
|
|
2279
|
+
/** Internal reference to the mounted node structure with typed tags. */
|
|
1722
2280
|
this.nodeMounted = null;
|
|
2281
|
+
/** Root container element for the search box component. */
|
|
1723
2282
|
this.node = null;
|
|
2283
|
+
/** Reference to the input element (`type="search"`). */
|
|
1724
2284
|
this.SearchInput = null;
|
|
2285
|
+
/** Callback fired on input changes (when not a control key event). */
|
|
1725
2286
|
this.onSearch = null;
|
|
2287
|
+
/** Current configuration options (placeholder, IDs, searchable flag, etc.). */
|
|
1726
2288
|
this.options = null;
|
|
2289
|
+
/** Callback to handle list navigation: +1 for next, -1 for previous. */
|
|
1727
2290
|
this.onNavigate = null;
|
|
2291
|
+
/** Callback fired on Enter. Typically used to confirm a selection. */
|
|
1728
2292
|
this.onEnter = null;
|
|
2293
|
+
/** Callback fired on Escape. Typically used to dismiss a popup. */
|
|
1729
2294
|
this.onEsc = null;
|
|
1730
2295
|
this.options = options;
|
|
1731
2296
|
if (options)
|
|
1732
|
-
this.
|
|
2297
|
+
this.initialize(options);
|
|
1733
2298
|
}
|
|
1734
2299
|
/**
|
|
1735
2300
|
* Initializes the search box DOM, sets ARIA attributes, and wires keyboard/mouse/input events.
|
|
1736
|
-
* Supports navigation (ArrowUp/ArrowDown/Tab), Enter, and Escape through callbacks.
|
|
1737
2301
|
*
|
|
1738
|
-
*
|
|
2302
|
+
* Accessibility:
|
|
2303
|
+
* - `role="searchbox"`
|
|
2304
|
+
* - `aria-controls` references the listbox container by ID
|
|
2305
|
+
* - `aria-autocomplete="list"` indicates list-based suggestions/results
|
|
2306
|
+
*
|
|
2307
|
+
* Keyboard support:
|
|
2308
|
+
* - ArrowDown / Tab → navigate forward
|
|
2309
|
+
* - ArrowUp → navigate backward
|
|
2310
|
+
* - Enter → confirm action
|
|
2311
|
+
* - Escape → cancel/close action
|
|
2312
|
+
*
|
|
2313
|
+
* @param options - Configuration including placeholder and `SEID_LIST` for `aria-controls`.
|
|
1739
2314
|
*/
|
|
1740
|
-
|
|
2315
|
+
initialize(options) {
|
|
1741
2316
|
this.nodeMounted = Libs.mountNode({
|
|
1742
2317
|
SearchBox: {
|
|
1743
2318
|
tag: { node: "div", classList: ["selective-ui-searchbox", "hide"] },
|
|
@@ -1761,12 +2336,14 @@ class SearchBox {
|
|
|
1761
2336
|
this.SearchInput = this.nodeMounted.tags.SearchInput;
|
|
1762
2337
|
let isControlKey = false;
|
|
1763
2338
|
const inputEl = this.nodeMounted.tags.SearchInput;
|
|
2339
|
+
// Prevent parent listeners from intercepting mouse interactions.
|
|
1764
2340
|
inputEl.addEventListener("mousedown", (e) => {
|
|
1765
2341
|
e.stopPropagation();
|
|
1766
2342
|
});
|
|
1767
2343
|
inputEl.addEventListener("mouseup", (e) => {
|
|
1768
2344
|
e.stopPropagation();
|
|
1769
2345
|
});
|
|
2346
|
+
// Keyboard handling: navigation, submit, and cancel.
|
|
1770
2347
|
inputEl.addEventListener("keydown", (e) => {
|
|
1771
2348
|
isControlKey = false;
|
|
1772
2349
|
if (e.key === "ArrowDown" || e.key === "Tab") {
|
|
@@ -1793,16 +2370,19 @@ class SearchBox {
|
|
|
1793
2370
|
isControlKey = true;
|
|
1794
2371
|
this.onEsc?.();
|
|
1795
2372
|
}
|
|
2373
|
+
// Ensure events don't bubble to container-level listeners.
|
|
1796
2374
|
e.stopPropagation();
|
|
1797
2375
|
});
|
|
2376
|
+
// Text input changes (ignore control-key initiated sequences).
|
|
1798
2377
|
inputEl.addEventListener("input", () => {
|
|
1799
2378
|
if (isControlKey)
|
|
1800
2379
|
return;
|
|
1801
2380
|
this.onSearch?.(inputEl.value, true);
|
|
1802
2381
|
});
|
|
2382
|
+
this.init();
|
|
1803
2383
|
}
|
|
1804
2384
|
/**
|
|
1805
|
-
* Shows the search box, toggles
|
|
2385
|
+
* Shows the search box, toggles `readOnly` based on `options.searchable`,
|
|
1806
2386
|
* and focuses the input when searchable.
|
|
1807
2387
|
*/
|
|
1808
2388
|
show() {
|
|
@@ -1817,7 +2397,7 @@ class SearchBox {
|
|
|
1817
2397
|
}
|
|
1818
2398
|
}
|
|
1819
2399
|
/**
|
|
1820
|
-
* Hides the search box by adding the
|
|
2400
|
+
* Hides the search box by adding the `hide` class.
|
|
1821
2401
|
*/
|
|
1822
2402
|
hide() {
|
|
1823
2403
|
if (!this.node)
|
|
@@ -1825,9 +2405,9 @@ class SearchBox {
|
|
|
1825
2405
|
this.node.classList.add("hide");
|
|
1826
2406
|
}
|
|
1827
2407
|
/**
|
|
1828
|
-
* Clears the current search value and optionally triggers the onSearch callback.
|
|
2408
|
+
* Clears the current search value and optionally triggers the `onSearch` callback.
|
|
1829
2409
|
*
|
|
1830
|
-
* @param
|
|
2410
|
+
* @param isTrigger - Whether to invoke `onSearch` with an empty string. Defaults to `true`.
|
|
1831
2411
|
*/
|
|
1832
2412
|
clear(isTrigger = true) {
|
|
1833
2413
|
if (!this.nodeMounted)
|
|
@@ -1838,7 +2418,7 @@ class SearchBox {
|
|
|
1838
2418
|
/**
|
|
1839
2419
|
* Updates the input's placeholder text, stripping any HTML for safety.
|
|
1840
2420
|
*
|
|
1841
|
-
* @param
|
|
2421
|
+
* @param value - The new placeholder text.
|
|
1842
2422
|
*/
|
|
1843
2423
|
setPlaceHolder(value) {
|
|
1844
2424
|
if (!this.SearchInput)
|
|
@@ -1846,15 +2426,37 @@ class SearchBox {
|
|
|
1846
2426
|
this.SearchInput.placeholder = Libs.stripHtml(value);
|
|
1847
2427
|
}
|
|
1848
2428
|
/**
|
|
1849
|
-
* Sets the active descendant for ARIA to indicate
|
|
2429
|
+
* Sets the active descendant for ARIA to indicate the currently highlighted option.
|
|
1850
2430
|
*
|
|
1851
|
-
* @param
|
|
2431
|
+
* @param id - The DOM id of the active option element.
|
|
1852
2432
|
*/
|
|
1853
2433
|
setActiveDescendant(id) {
|
|
1854
2434
|
if (!this.SearchInput)
|
|
1855
2435
|
return;
|
|
1856
2436
|
this.SearchInput.setAttribute("aria-activedescendant", id);
|
|
1857
2437
|
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Destroys the search box and releases resources.
|
|
2440
|
+
*
|
|
2441
|
+
* - Removes the root DOM node
|
|
2442
|
+
* - Clears references to DOM and callbacks
|
|
2443
|
+
* - Ends the lifecycle
|
|
2444
|
+
*/
|
|
2445
|
+
destroy() {
|
|
2446
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
this.node?.remove();
|
|
2450
|
+
this.nodeMounted = null;
|
|
2451
|
+
this.node = null;
|
|
2452
|
+
this.SearchInput = null;
|
|
2453
|
+
this.onSearch = null;
|
|
2454
|
+
this.options = null;
|
|
2455
|
+
this.onNavigate = null;
|
|
2456
|
+
this.onEnter = null;
|
|
2457
|
+
this.onEsc = null;
|
|
2458
|
+
super.destroy();
|
|
2459
|
+
}
|
|
1858
2460
|
}
|
|
1859
2461
|
|
|
1860
2462
|
/**
|
|
@@ -2160,66 +2762,85 @@ class EffectorImpl {
|
|
|
2160
2762
|
}
|
|
2161
2763
|
|
|
2162
2764
|
/**
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2165
|
-
*
|
|
2765
|
+
* Base Model class that connects a target DOM element with its corresponding View.
|
|
2766
|
+
* Handles lifecycle, state, and synchronization between model, view, and DOM.
|
|
2767
|
+
*
|
|
2768
|
+
* @template TTarget - The HTML element this model is bound to
|
|
2769
|
+
* @template TTags - A map of named HTML elements used by the view
|
|
2770
|
+
* @template TView - The view implementation associated with this model
|
|
2771
|
+
* @template TOptions - Configuration options for the model
|
|
2772
|
+
*
|
|
2166
2773
|
* @implements {ModelContract<TTarget, TView>}
|
|
2167
2774
|
*/
|
|
2168
|
-
class Model {
|
|
2775
|
+
class Model extends Lifecycle {
|
|
2169
2776
|
/**
|
|
2170
|
-
* Returns the current value
|
|
2171
|
-
*
|
|
2777
|
+
* Returns the current value of the bound target element.
|
|
2778
|
+
*
|
|
2779
|
+
* - For single-value elements, this is usually a string
|
|
2780
|
+
* - For multi-value elements, this may be an array depending on usage
|
|
2172
2781
|
*/
|
|
2173
2782
|
get value() {
|
|
2174
2783
|
return this.targetElement?.getAttribute("value") ?? null;
|
|
2175
2784
|
}
|
|
2176
2785
|
/**
|
|
2177
|
-
*
|
|
2178
|
-
* Stores references for later updates and rendering.
|
|
2786
|
+
* Creates a new Model instance.
|
|
2179
2787
|
*
|
|
2180
|
-
*
|
|
2181
|
-
*
|
|
2182
|
-
*
|
|
2788
|
+
* Initializes the model with configuration options and optionally binds
|
|
2789
|
+
* it to a target DOM element and a view.
|
|
2790
|
+
*
|
|
2791
|
+
* @param options - Configuration options for the model
|
|
2792
|
+
* @param targetElement - Optional DOM element to bind to this model
|
|
2793
|
+
* @param view - Optional view responsible for rendering the model
|
|
2183
2794
|
*/
|
|
2184
2795
|
constructor(options, targetElement = null, view = null) {
|
|
2796
|
+
super();
|
|
2797
|
+
/** The underlying DOM element associated with this model */
|
|
2185
2798
|
this.targetElement = null;
|
|
2799
|
+
/** View instance responsible for rendering this model */
|
|
2186
2800
|
this.view = null;
|
|
2801
|
+
/** Position index of the model (used for ordering or tracking) */
|
|
2187
2802
|
this.position = -1;
|
|
2803
|
+
/** Indicates whether the model has been initialized */
|
|
2188
2804
|
this.isInit = false;
|
|
2805
|
+
/** Indicates whether the model has been destroyed/removed */
|
|
2189
2806
|
this.isRemoved = false;
|
|
2190
2807
|
this.options = options;
|
|
2191
2808
|
this.targetElement = targetElement;
|
|
2192
2809
|
this.view = view;
|
|
2810
|
+
this.init();
|
|
2193
2811
|
}
|
|
2194
2812
|
/**
|
|
2195
|
-
* Updates the bound target element
|
|
2813
|
+
* Updates the bound target element and triggers the update lifecycle.
|
|
2196
2814
|
*
|
|
2197
|
-
* @param
|
|
2815
|
+
* @param targetElement - The new DOM element to associate with this model
|
|
2198
2816
|
*/
|
|
2199
|
-
|
|
2817
|
+
updateTarget(targetElement) {
|
|
2200
2818
|
this.targetElement = targetElement;
|
|
2201
|
-
this.
|
|
2819
|
+
this.update();
|
|
2202
2820
|
}
|
|
2203
2821
|
/**
|
|
2204
|
-
*
|
|
2822
|
+
* Hook executed when the model is updated.
|
|
2823
|
+
* Intended to be overridden by subclasses.
|
|
2205
2824
|
*/
|
|
2206
|
-
|
|
2825
|
+
onUpdate() { }
|
|
2826
|
+
/**
|
|
2827
|
+
* Destroys the model and releases all references.
|
|
2828
|
+
*
|
|
2829
|
+
* - Unbinds the target element
|
|
2830
|
+
* - Removes the associated view from the DOM
|
|
2831
|
+
* - Marks the model as removed
|
|
2832
|
+
* - Triggers lifecycle cleanup
|
|
2833
|
+
*/
|
|
2834
|
+
destroy() {
|
|
2835
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2207
2838
|
this.targetElement = null;
|
|
2208
|
-
this.view?.
|
|
2839
|
+
this.view?.destroy();
|
|
2209
2840
|
this.view = null;
|
|
2210
2841
|
this.isRemoved = true;
|
|
2211
|
-
|
|
2842
|
+
super.destroy();
|
|
2212
2843
|
}
|
|
2213
|
-
/**
|
|
2214
|
-
* Hook invoked whenever the target element changes.
|
|
2215
|
-
* Override in subclasses to react to attribute/content updates (e.g., text, disabled state).
|
|
2216
|
-
*/
|
|
2217
|
-
onTargetChanged() { }
|
|
2218
|
-
/**
|
|
2219
|
-
* Hook invoked whenever the target element is removed.
|
|
2220
|
-
* Override in subclasses to react to removal of the element.
|
|
2221
|
-
*/
|
|
2222
|
-
onRemove() { }
|
|
2223
2844
|
}
|
|
2224
2845
|
|
|
2225
2846
|
/**
|
|
@@ -2239,10 +2860,14 @@ class GroupModel extends Model {
|
|
|
2239
2860
|
this.items = [];
|
|
2240
2861
|
this.collapsed = false;
|
|
2241
2862
|
this.privOnCollapsedChanged = [];
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2863
|
+
}
|
|
2864
|
+
init() {
|
|
2865
|
+
if (this.targetElement) {
|
|
2866
|
+
this.label = this.targetElement.label;
|
|
2867
|
+
this.collapsed = Libs.string2Boolean(this.targetElement.dataset?.collapsed);
|
|
2245
2868
|
}
|
|
2869
|
+
super.init();
|
|
2870
|
+
this.mount();
|
|
2246
2871
|
}
|
|
2247
2872
|
/**
|
|
2248
2873
|
* Returns the array of values from all option items within the group.
|
|
@@ -2281,19 +2906,31 @@ class GroupModel extends Model {
|
|
|
2281
2906
|
*
|
|
2282
2907
|
* @param {HTMLOptGroupElement} targetElement - The updated <optgroup> element.
|
|
2283
2908
|
*/
|
|
2284
|
-
|
|
2909
|
+
updateTarget(targetElement) {
|
|
2285
2910
|
this.label = targetElement.label;
|
|
2286
2911
|
this.view?.updateLabel(this.label);
|
|
2912
|
+
this.update();
|
|
2287
2913
|
}
|
|
2288
2914
|
/**
|
|
2289
2915
|
* Hook invoked when the target element reference changes.
|
|
2290
2916
|
* Updates the view's label and collapsed state to keep UI in sync.
|
|
2291
2917
|
*/
|
|
2292
|
-
|
|
2918
|
+
update() {
|
|
2293
2919
|
if (this.view) {
|
|
2294
2920
|
this.view.updateLabel(this.label);
|
|
2295
2921
|
this.view.setCollapsed(this.collapsed);
|
|
2296
2922
|
}
|
|
2923
|
+
super.update();
|
|
2924
|
+
}
|
|
2925
|
+
destroy() {
|
|
2926
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
this.items.forEach(item => {
|
|
2930
|
+
item.destroy();
|
|
2931
|
+
});
|
|
2932
|
+
this.items = [];
|
|
2933
|
+
super.destroy();
|
|
2297
2934
|
}
|
|
2298
2935
|
/**
|
|
2299
2936
|
* Registers a callback to be invoked when the group's collapsed state changes.
|
|
@@ -2361,9 +2998,11 @@ class OptionModel extends Model {
|
|
|
2361
2998
|
this._visible = true;
|
|
2362
2999
|
this._highlighted = false;
|
|
2363
3000
|
this.group = null;
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
3001
|
+
}
|
|
3002
|
+
init() {
|
|
3003
|
+
this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
|
|
3004
|
+
super.init();
|
|
3005
|
+
this.mount();
|
|
2367
3006
|
}
|
|
2368
3007
|
/**
|
|
2369
3008
|
* Returns the image source from dataset (imgsrc or image), or an empty string if absent.
|
|
@@ -2536,10 +3175,12 @@ class OptionModel extends Model {
|
|
|
2536
3175
|
* Updates label content (HTML or text), image src/alt if present,
|
|
2537
3176
|
* and synchronizes initial selected state to the view.
|
|
2538
3177
|
*/
|
|
2539
|
-
|
|
3178
|
+
update() {
|
|
2540
3179
|
this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
|
|
2541
|
-
if (!this.view)
|
|
3180
|
+
if (!this.view) {
|
|
3181
|
+
super.update();
|
|
2542
3182
|
return;
|
|
3183
|
+
}
|
|
2543
3184
|
const labelContent = this.view.view.tags.LabelContent;
|
|
2544
3185
|
if (labelContent) {
|
|
2545
3186
|
if (this.options.allowHtml) {
|
|
@@ -2556,6 +3197,18 @@ class OptionModel extends Model {
|
|
|
2556
3197
|
}
|
|
2557
3198
|
if (this.targetElement)
|
|
2558
3199
|
this.selectedNonTrigger = this.targetElement.selected;
|
|
3200
|
+
super.update();
|
|
3201
|
+
}
|
|
3202
|
+
destroy() {
|
|
3203
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
this.privOnSelected = [];
|
|
3207
|
+
this.privOnInternalSelected = [];
|
|
3208
|
+
this.privOnVisibilityChanged = [];
|
|
3209
|
+
this.group = null;
|
|
3210
|
+
this.textToFind = null;
|
|
3211
|
+
super.destroy();
|
|
2559
3212
|
}
|
|
2560
3213
|
}
|
|
2561
3214
|
|
|
@@ -2735,7 +3388,7 @@ class ModelManager {
|
|
|
2735
3388
|
// Label is used as key; keep original behavior.
|
|
2736
3389
|
const hasLabelChange = existingGroup.label !== dataVset.label;
|
|
2737
3390
|
if (hasLabelChange) {
|
|
2738
|
-
existingGroup.
|
|
3391
|
+
existingGroup.updateTarget(dataVset);
|
|
2739
3392
|
}
|
|
2740
3393
|
existingGroup.position = position;
|
|
2741
3394
|
existingGroup.items = [];
|
|
@@ -2755,7 +3408,7 @@ class ModelManager {
|
|
|
2755
3408
|
const key = `${dataVset.value}::${dataVset.text}`;
|
|
2756
3409
|
const existingOption = oldOptionMap.get(key);
|
|
2757
3410
|
if (existingOption) {
|
|
2758
|
-
existingOption.
|
|
3411
|
+
existingOption.updateTarget(dataVset);
|
|
2759
3412
|
existingOption.position = position;
|
|
2760
3413
|
const parentGroup = dataVset["__parentGroup"];
|
|
2761
3414
|
if (parentGroup && currentGroup) {
|
|
@@ -2790,11 +3443,11 @@ class ModelManager {
|
|
|
2790
3443
|
this.oldPosition = position;
|
|
2791
3444
|
oldGroupMap.forEach((removedGroup) => {
|
|
2792
3445
|
isUpdate = false;
|
|
2793
|
-
removedGroup.
|
|
3446
|
+
removedGroup.destroy();
|
|
2794
3447
|
});
|
|
2795
3448
|
oldOptionMap.forEach((removedOption) => {
|
|
2796
3449
|
isUpdate = false;
|
|
2797
|
-
removedOption.
|
|
3450
|
+
removedOption.destroy();
|
|
2798
3451
|
});
|
|
2799
3452
|
this.privModelList = newModels;
|
|
2800
3453
|
if (this.privAdapterHandle) {
|
|
@@ -2857,34 +3510,45 @@ class ModelManager {
|
|
|
2857
3510
|
}
|
|
2858
3511
|
|
|
2859
3512
|
/**
|
|
2860
|
-
*
|
|
2861
|
-
*
|
|
3513
|
+
* RecyclerView renders models provided by an Adapter into a container element.
|
|
3514
|
+
*
|
|
3515
|
+
* Responsibilities:
|
|
3516
|
+
* - Maintain a root container (`viewElement`) where item views are rendered
|
|
3517
|
+
* - Attach an Adapter and wire item-change lifecycle:
|
|
3518
|
+
* - `onPropChanging('items')` → clear container before items change
|
|
3519
|
+
* - `onPropChanged('items')` → re-render after items change
|
|
3520
|
+
* - Expose rendering utilities: `render()`, `clear()`, `refresh()`
|
|
3521
|
+
* - Participate in the standard lifecycle (`init` → `mount` → `update` → `destroy`)
|
|
3522
|
+
*
|
|
3523
|
+
* @template TItem - The model type handled by the adapter.
|
|
3524
|
+
* @template TAdapter - The adapter type that manages items and updates the view.
|
|
3525
|
+
*
|
|
2862
3526
|
* @implements {RecyclerViewContract<TAdapter>}
|
|
2863
3527
|
*/
|
|
2864
|
-
class RecyclerView {
|
|
3528
|
+
class RecyclerView extends Lifecycle {
|
|
2865
3529
|
/**
|
|
2866
3530
|
* Constructs a RecyclerView with an optional container element that will host rendered item views.
|
|
2867
3531
|
*
|
|
2868
3532
|
* @param {HTMLDivElement|null} [viewElement=null] - The root element where the adapter will render items.
|
|
2869
3533
|
*/
|
|
2870
3534
|
constructor(viewElement = null) {
|
|
3535
|
+
super();
|
|
3536
|
+
/** Root container that hosts rendered item views. */
|
|
2871
3537
|
this.viewElement = null;
|
|
3538
|
+
/** The adapter that manages models and updates the RecyclerView on changes. */
|
|
2872
3539
|
this.adapter = null;
|
|
2873
3540
|
this.viewElement = viewElement;
|
|
2874
|
-
|
|
2875
|
-
/**
|
|
2876
|
-
* Sets or updates the container element used to render the adapter's item views.
|
|
2877
|
-
*
|
|
2878
|
-
* @param {HTMLDivElement} viewElement - The root element for rendering.
|
|
2879
|
-
*/
|
|
2880
|
-
setView(viewElement) {
|
|
2881
|
-
this.viewElement = viewElement;
|
|
3541
|
+
this.init();
|
|
2882
3542
|
}
|
|
2883
3543
|
/**
|
|
2884
3544
|
* Attaches an adapter to the RecyclerView and wires item-change lifecycle:
|
|
2885
|
-
* - onPropChanging(
|
|
2886
|
-
* - onPropChanged(
|
|
2887
|
-
*
|
|
3545
|
+
* - `onPropChanging('items')`: clears the container before items change
|
|
3546
|
+
* - `onPropChanged('items')`: re-renders after items change
|
|
3547
|
+
*
|
|
3548
|
+
* Then performs:
|
|
3549
|
+
* - `adapter.mount()` to initialize the adapter
|
|
3550
|
+
* - `this.mount()` to mark the RecyclerView as mounted
|
|
3551
|
+
* - An initial `render()` to sync the UI
|
|
2888
3552
|
*
|
|
2889
3553
|
* @param {TAdapter} adapter - The adapter managing models and their views.
|
|
2890
3554
|
*/
|
|
@@ -2896,11 +3560,13 @@ class RecyclerView {
|
|
|
2896
3560
|
adapter.onPropChanged("items", () => {
|
|
2897
3561
|
this.render();
|
|
2898
3562
|
});
|
|
3563
|
+
adapter.mount();
|
|
3564
|
+
this.mount();
|
|
2899
3565
|
this.render();
|
|
2900
3566
|
}
|
|
2901
3567
|
/**
|
|
2902
3568
|
* Removes all child nodes from the rendering container, if present.
|
|
2903
|
-
*
|
|
3569
|
+
* Typically used right before re-rendering or when items are about to change.
|
|
2904
3570
|
*/
|
|
2905
3571
|
clear() {
|
|
2906
3572
|
if (!this.viewElement)
|
|
@@ -2910,77 +3576,155 @@ class RecyclerView {
|
|
|
2910
3576
|
/**
|
|
2911
3577
|
* Renders the current adapter contents into the container.
|
|
2912
3578
|
* No-ops if either the adapter or the container is not set.
|
|
3579
|
+
* Emits the `update` lifecycle after delegating rendering to the adapter.
|
|
2913
3580
|
*/
|
|
2914
3581
|
render() {
|
|
2915
3582
|
if (!this.adapter || !this.viewElement)
|
|
2916
3583
|
return;
|
|
2917
3584
|
this.adapter.updateRecyclerView(this.viewElement);
|
|
3585
|
+
this.update();
|
|
2918
3586
|
}
|
|
2919
3587
|
/**
|
|
2920
3588
|
* Forces a re-render of the current adapter state into the container.
|
|
2921
3589
|
* Useful when visual updates are required without changing the data.
|
|
2922
3590
|
*
|
|
2923
|
-
* @param isUpdate - Indicates if this refresh
|
|
3591
|
+
* @param {boolean} isUpdate - Indicates if this refresh originates from an update operation.
|
|
3592
|
+
* (Reserved for future use; no impact on logic.)
|
|
2924
3593
|
*/
|
|
2925
3594
|
refresh(isUpdate) {
|
|
2926
3595
|
this.render();
|
|
2927
3596
|
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Destroys the RecyclerView, detaching from its adapter and container.
|
|
3599
|
+
*
|
|
3600
|
+
* - Delegates teardown to the adapter
|
|
3601
|
+
* - Clears strong references (adapter, viewElement)
|
|
3602
|
+
* - Ends the lifecycle
|
|
3603
|
+
*/
|
|
3604
|
+
destroy() {
|
|
3605
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
this.adapter?.destroy?.();
|
|
3609
|
+
this.viewElement = null;
|
|
3610
|
+
this.adapter = null;
|
|
3611
|
+
super.destroy();
|
|
3612
|
+
}
|
|
2928
3613
|
}
|
|
2929
3614
|
|
|
2930
3615
|
/**
|
|
2931
|
-
*
|
|
3616
|
+
* Accessory box that displays “selected chips” for multi-select mode.
|
|
3617
|
+
*
|
|
3618
|
+
* Responsibilities:
|
|
3619
|
+
* - Create and position a lightweight container near the Select UI mask
|
|
3620
|
+
* - Render current selections as removable “accessory items” (chips)
|
|
3621
|
+
* - Dispatch selection changes back to the ModelManager
|
|
3622
|
+
* - Show/hide based on configuration and current selection count
|
|
3623
|
+
*
|
|
3624
|
+
* Lifecycle:
|
|
3625
|
+
* - Constructed with optional options → `initialize()` → `init()`
|
|
3626
|
+
* - `setRoot()` binds to the Select UI mask and calls `mount()`
|
|
3627
|
+
* - `setModelData()` re-renders chips and calls `update()`
|
|
3628
|
+
* - `destroy()` removes the DOM node and clears references
|
|
3629
|
+
*
|
|
3630
|
+
* @extends Lifecycle
|
|
2932
3631
|
*/
|
|
2933
|
-
class AccessoryBox {
|
|
3632
|
+
class AccessoryBox extends Lifecycle {
|
|
2934
3633
|
/**
|
|
2935
|
-
*
|
|
3634
|
+
* Creates an AccessoryBox and (optionally) initializes it with configuration.
|
|
2936
3635
|
*
|
|
2937
|
-
* @param
|
|
3636
|
+
* @param options - Configuration options (e.g., placement via `accessoryStyle`,
|
|
3637
|
+
* visibility via `accessoryVisible`, texts, etc.).
|
|
2938
3638
|
*/
|
|
2939
3639
|
constructor(options = null) {
|
|
3640
|
+
super();
|
|
3641
|
+
/** Internal reference to the mounted node structure. */
|
|
2940
3642
|
this.nodeMounted = null;
|
|
3643
|
+
/** Root DOM element of the accessory box (hidden by default). */
|
|
2941
3644
|
this.node = null;
|
|
3645
|
+
/** Configuration (texts, behavior, placement style). */
|
|
2942
3646
|
this.options = null;
|
|
3647
|
+
/** The overlay/mask element belonging to the main Select UI. */
|
|
2943
3648
|
this.selectUIMask = null;
|
|
3649
|
+
/** Parent element hosting both the Select UI mask and the accessory box. */
|
|
2944
3650
|
this.parentMask = null;
|
|
3651
|
+
/** ModelManager used to trigger selection state changes. */
|
|
2945
3652
|
this.modelManager = null;
|
|
3653
|
+
/** Current list of selected option models rendered as “chips”. */
|
|
2946
3654
|
this.modelDatas = [];
|
|
2947
3655
|
if (options)
|
|
2948
|
-
this.
|
|
3656
|
+
this.initialize(options);
|
|
2949
3657
|
}
|
|
2950
3658
|
/**
|
|
2951
|
-
*
|
|
2952
|
-
* The node is initially hidden and stops mouseup events from bubbling.
|
|
3659
|
+
* Stores options and starts the lifecycle.
|
|
2953
3660
|
*
|
|
2954
|
-
*
|
|
3661
|
+
* Does not attach the node yet; the DOM structure is created in `init()`.
|
|
3662
|
+
*
|
|
3663
|
+
* @param options - Configuration object for the accessory box.
|
|
3664
|
+
*/
|
|
3665
|
+
initialize(options) {
|
|
3666
|
+
this.options = options;
|
|
3667
|
+
this.init(); // Trigger lifecycle initialization
|
|
3668
|
+
}
|
|
3669
|
+
/**
|
|
3670
|
+
* Initializes the accessory box DOM structure.
|
|
3671
|
+
*
|
|
3672
|
+
* - Creates the root node (hidden by default)
|
|
3673
|
+
* - Stops mouseup events from propagating to parent containers
|
|
3674
|
+
* - Completes the lifecycle initialization
|
|
2955
3675
|
*/
|
|
2956
|
-
init(
|
|
3676
|
+
init() {
|
|
3677
|
+
if (this.state !== LifecycleState.NEW)
|
|
3678
|
+
return;
|
|
2957
3679
|
this.nodeMounted = Libs.mountNode({
|
|
2958
3680
|
AccessoryBox: {
|
|
2959
3681
|
tag: {
|
|
2960
3682
|
node: "div",
|
|
2961
3683
|
classList: ["selective-ui-accessorybox", "hide"],
|
|
2962
3684
|
onmouseup: (evt) => {
|
|
3685
|
+
// Prevent outside listeners from reacting to chip clicks
|
|
2963
3686
|
evt.stopPropagation();
|
|
2964
3687
|
},
|
|
2965
3688
|
},
|
|
2966
3689
|
},
|
|
2967
3690
|
});
|
|
2968
3691
|
this.node = this.nodeMounted.view;
|
|
2969
|
-
|
|
3692
|
+
super.init(); // Mark as INITIALIZED
|
|
2970
3693
|
}
|
|
2971
3694
|
/**
|
|
2972
|
-
*
|
|
3695
|
+
* Binds to the Select UI mask and positions the accessory box relative to it.
|
|
3696
|
+
*
|
|
3697
|
+
* You should call this after the Select UI mask is available. This method
|
|
3698
|
+
* will insert the accessory box either before or after the mask based on
|
|
3699
|
+
* `options.accessoryStyle` and then call `mount()`.
|
|
2973
3700
|
*
|
|
2974
|
-
* @param
|
|
3701
|
+
* @param selectUIMask - The overlay/mask element of the main Select UI.
|
|
2975
3702
|
*/
|
|
2976
3703
|
setRoot(selectUIMask) {
|
|
2977
3704
|
this.selectUIMask = selectUIMask;
|
|
2978
3705
|
this.parentMask = selectUIMask.parentElement;
|
|
2979
3706
|
this.refreshLocation();
|
|
3707
|
+
this.mount();
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* Lifecycle mount override (no-op guard).
|
|
3711
|
+
*
|
|
3712
|
+
* Ensures mount is only applied once the component has been initialized.
|
|
3713
|
+
*/
|
|
3714
|
+
mount() {
|
|
3715
|
+
if (!this.is(LifecycleState.INITIALIZED)) {
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
super.mount();
|
|
2980
3719
|
}
|
|
2981
3720
|
/**
|
|
2982
|
-
*
|
|
2983
|
-
*
|
|
3721
|
+
* Positions the accessory box relative to the Select UI mask.
|
|
3722
|
+
*
|
|
3723
|
+
* Placement rules:
|
|
3724
|
+
* - `accessoryStyle === "top"` → insert before the mask
|
|
3725
|
+
* - otherwise → insert after the mask
|
|
3726
|
+
*
|
|
3727
|
+
* Also keeps the accessory box aligned under the same parent container.
|
|
2984
3728
|
*/
|
|
2985
3729
|
refreshLocation() {
|
|
2986
3730
|
if (!this.parentMask ||
|
|
@@ -2994,18 +3738,30 @@ class AccessoryBox {
|
|
|
2994
3738
|
this.parentMask.insertBefore(this.node, ref);
|
|
2995
3739
|
}
|
|
2996
3740
|
/**
|
|
2997
|
-
* Assigns
|
|
3741
|
+
* Assigns the `ModelManager` instance used to trigger selection changes.
|
|
2998
3742
|
*
|
|
2999
|
-
* @param
|
|
3743
|
+
* @param modelManager - The model manager controlling option state.
|
|
3000
3744
|
*/
|
|
3001
3745
|
setModelManager(modelManager) {
|
|
3002
3746
|
this.modelManager = modelManager;
|
|
3003
3747
|
}
|
|
3004
3748
|
/**
|
|
3005
|
-
* Renders accessory items for the
|
|
3006
|
-
*
|
|
3749
|
+
* Renders accessory items (“chips”) for the provided selected options.
|
|
3750
|
+
*
|
|
3751
|
+
* Behavior:
|
|
3752
|
+
* - Clears the current container
|
|
3753
|
+
* - For multi-select mode with non-empty data, mounts each chip:
|
|
3754
|
+
* - A “button” (span with role="button") to deselect the option
|
|
3755
|
+
* - A content span showing the option’s text (HTML is preserved as provided)
|
|
3756
|
+
* - When the button is clicked:
|
|
3757
|
+
* - Calls `modelManager.triggerChanging("select")`
|
|
3758
|
+
* - Sets `modelData.selected = false`
|
|
3759
|
+
*
|
|
3760
|
+
* Finally:
|
|
3761
|
+
* - Updates visibility based on config and chip count
|
|
3762
|
+
* - Emits lifecycle `update()` and a window `"resize"` event
|
|
3007
3763
|
*
|
|
3008
|
-
* @param
|
|
3764
|
+
* @param modelDatas - List of option models representing current selections.
|
|
3009
3765
|
*/
|
|
3010
3766
|
setModelData(modelDatas) {
|
|
3011
3767
|
if (!this.node || !this.options)
|
|
@@ -3048,38 +3804,107 @@ class AccessoryBox {
|
|
|
3048
3804
|
}
|
|
3049
3805
|
this.modelDatas = modelDatas;
|
|
3050
3806
|
this.refreshDisplay();
|
|
3807
|
+
this.update(); // lifecycle UPDATE
|
|
3051
3808
|
iEvents.trigger(window, "resize");
|
|
3052
3809
|
}
|
|
3810
|
+
/**
|
|
3811
|
+
* Lifecycle update override (no-op guard).
|
|
3812
|
+
*
|
|
3813
|
+
* Ensures update is only emitted after the component is mounted.
|
|
3814
|
+
*/
|
|
3815
|
+
update() {
|
|
3816
|
+
if (this.state !== LifecycleState.MOUNTED)
|
|
3817
|
+
return;
|
|
3818
|
+
super.update();
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* Applies visibility rules based on configuration and chip count.
|
|
3822
|
+
*
|
|
3823
|
+
* Visible when:
|
|
3824
|
+
* - `accessoryVisible` is truthy
|
|
3825
|
+
* - There is at least one selected item
|
|
3826
|
+
* - The Select is in multiple mode
|
|
3827
|
+
*/
|
|
3053
3828
|
refreshDisplay() {
|
|
3054
|
-
if (this.options?.accessoryVisible &&
|
|
3829
|
+
if (this.options?.accessoryVisible &&
|
|
3830
|
+
this.modelDatas.length > 0 &&
|
|
3831
|
+
this.options.multiple) {
|
|
3055
3832
|
this.show();
|
|
3056
3833
|
}
|
|
3057
3834
|
else {
|
|
3058
3835
|
this.hide();
|
|
3059
3836
|
}
|
|
3060
3837
|
}
|
|
3838
|
+
/** Shows the accessory box. */
|
|
3061
3839
|
show() {
|
|
3062
|
-
this.node
|
|
3840
|
+
this.node?.classList.remove("hide");
|
|
3063
3841
|
}
|
|
3842
|
+
/** Hides the accessory box. */
|
|
3064
3843
|
hide() {
|
|
3065
|
-
this.node
|
|
3844
|
+
this.node?.classList.add("hide");
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Destroys the accessory box and releases resources.
|
|
3848
|
+
*
|
|
3849
|
+
* - Removes the root DOM node
|
|
3850
|
+
* - Clears references to mounted structures, options, masks, and ModelManager
|
|
3851
|
+
* - Resets internal model data
|
|
3852
|
+
* - Ends the lifecycle
|
|
3853
|
+
*/
|
|
3854
|
+
destroy() {
|
|
3855
|
+
if (this.state === LifecycleState.DESTROYED)
|
|
3856
|
+
return;
|
|
3857
|
+
// Clean up DOM
|
|
3858
|
+
this.node?.remove();
|
|
3859
|
+
// Clear references
|
|
3860
|
+
this.nodeMounted = null;
|
|
3861
|
+
this.node = null;
|
|
3862
|
+
this.options = null;
|
|
3863
|
+
this.selectUIMask = null;
|
|
3864
|
+
this.parentMask = null;
|
|
3865
|
+
this.modelManager = null;
|
|
3866
|
+
this.modelDatas = [];
|
|
3867
|
+
super.destroy();
|
|
3066
3868
|
}
|
|
3067
3869
|
}
|
|
3068
3870
|
|
|
3069
|
-
|
|
3871
|
+
/**
|
|
3872
|
+
* Controller responsible for orchestrating search behavior across the Select UI.
|
|
3873
|
+
*
|
|
3874
|
+
* Responsibilities:
|
|
3875
|
+
* - Manage local (in-memory) filtering and remote (AJAX) searching
|
|
3876
|
+
* - Normalize heterogeneous server responses into a common structure
|
|
3877
|
+
* - Maintain pagination state (page, totals, loading, hasMore)
|
|
3878
|
+
* - Apply remote results back to the underlying <select> element
|
|
3879
|
+
* - Coordinate UI updates via Popup (loading indicator, resize, empty/not-found states)
|
|
3880
|
+
*
|
|
3881
|
+
* Lifecycle:
|
|
3882
|
+
* - Constructed with references to the native <select>, ModelManager, and SelectBox
|
|
3883
|
+
* - `init()` runs immediately via `initialize()`
|
|
3884
|
+
* - Methods `search()`, `loadMore()`, `clear()` are invoked by higher-level components
|
|
3885
|
+
*
|
|
3886
|
+
* @extends Lifecycle
|
|
3887
|
+
*/
|
|
3888
|
+
class SearchController extends Lifecycle {
|
|
3070
3889
|
/**
|
|
3071
3890
|
* Initializes the SearchController with a source <select> element and a ModelManager
|
|
3072
3891
|
* to manage option models and search results.
|
|
3073
3892
|
*
|
|
3074
|
-
* @param
|
|
3075
|
-
* @param
|
|
3076
|
-
* @param
|
|
3893
|
+
* @param selectElement - The native select element that provides context and data source.
|
|
3894
|
+
* @param modelManager - Manager responsible for models and rendering updates.
|
|
3895
|
+
* @param selectBox - SelectBox handle.
|
|
3077
3896
|
*/
|
|
3078
3897
|
constructor(selectElement, modelManager, selectBox) {
|
|
3898
|
+
super();
|
|
3899
|
+
/** Current AJAX configuration; when absent, local search is used. */
|
|
3079
3900
|
this.ajaxConfig = null;
|
|
3901
|
+
/** Used to cancel in-flight AJAX requests when a new search starts. */
|
|
3080
3902
|
this.abortController = null;
|
|
3903
|
+
/** Popup instance to reflect UI states (loading, empty/not-found), and sizing. */
|
|
3081
3904
|
this.popup = null;
|
|
3905
|
+
/** SelectBox handle (used by custom data builders). */
|
|
3082
3906
|
this.selectBox = null;
|
|
3907
|
+
/** Current pagination and loading state for remote searches. */
|
|
3083
3908
|
this.paginationState = {
|
|
3084
3909
|
currentPage: 0,
|
|
3085
3910
|
totalPages: 1,
|
|
@@ -3088,22 +3913,39 @@ class SearchController {
|
|
|
3088
3913
|
currentKeyword: "",
|
|
3089
3914
|
isPaginationEnabled: false,
|
|
3090
3915
|
};
|
|
3916
|
+
this.initialize(selectElement, modelManager, selectBox);
|
|
3917
|
+
}
|
|
3918
|
+
/**
|
|
3919
|
+
* Internal initializer that captures dependencies and starts the lifecycle.
|
|
3920
|
+
*
|
|
3921
|
+
* @param selectElement - The native select element that provides context and data source.
|
|
3922
|
+
* @param modelManager - Manager responsible for models and rendering updates.
|
|
3923
|
+
* @param selectBox - SelectBox handle.
|
|
3924
|
+
*/
|
|
3925
|
+
initialize(selectElement, modelManager, selectBox) {
|
|
3091
3926
|
this.select = selectElement;
|
|
3092
3927
|
this.modelManager = modelManager;
|
|
3093
3928
|
this.selectBox = selectBox;
|
|
3929
|
+
this.init();
|
|
3094
3930
|
}
|
|
3095
3931
|
/**
|
|
3096
3932
|
* Indicates whether AJAX-based search is configured.
|
|
3097
3933
|
*
|
|
3098
|
-
* @returns
|
|
3934
|
+
* @returns True if AJAX config is present; false otherwise.
|
|
3099
3935
|
*/
|
|
3100
3936
|
isAjax() {
|
|
3101
3937
|
return !!this.ajaxConfig;
|
|
3102
3938
|
}
|
|
3103
3939
|
/**
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3940
|
+
* Loads specific options by their values from the server.
|
|
3941
|
+
*
|
|
3942
|
+
* Behavior:
|
|
3943
|
+
* - Uses `ajaxConfig.dataByValues` if provided; otherwise builds a default payload.
|
|
3944
|
+
* - Supports GET/POST according to `ajaxConfig.method` (defaults to GET).
|
|
3945
|
+
* - Normalizes the response via `parseResponse()` and returns normalized items.
|
|
3946
|
+
*
|
|
3947
|
+
* @param values - Value or list of values to load.
|
|
3948
|
+
* @returns Promise resolving with `{ success, items, message? }`.
|
|
3107
3949
|
*/
|
|
3108
3950
|
async loadByValues(values) {
|
|
3109
3951
|
if (!this.ajaxConfig) {
|
|
@@ -3122,7 +3964,9 @@ class SearchController {
|
|
|
3122
3964
|
payload = {
|
|
3123
3965
|
values: valuesArray.join(","),
|
|
3124
3966
|
load_by_values: "1",
|
|
3125
|
-
...(typeof cfg.data === "function"
|
|
3967
|
+
...(typeof cfg.data === "function"
|
|
3968
|
+
? cfg.data.bind(this.selectBox.Selective.find(this.selectBox.container.targetElement))("", 0)
|
|
3969
|
+
: cfg.data ?? {}),
|
|
3126
3970
|
};
|
|
3127
3971
|
}
|
|
3128
3972
|
let response;
|
|
@@ -3143,6 +3987,7 @@ class SearchController {
|
|
|
3143
3987
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
3144
3988
|
const data = await response.json();
|
|
3145
3989
|
const result = this.parseResponse(data);
|
|
3990
|
+
this.update();
|
|
3146
3991
|
return { success: true, items: result.items };
|
|
3147
3992
|
}
|
|
3148
3993
|
catch (error) {
|
|
@@ -3151,9 +3996,10 @@ class SearchController {
|
|
|
3151
3996
|
}
|
|
3152
3997
|
}
|
|
3153
3998
|
/**
|
|
3154
|
-
*
|
|
3155
|
-
*
|
|
3156
|
-
* @
|
|
3999
|
+
* Checks whether the provided values already exist in the current <select> options.
|
|
4000
|
+
*
|
|
4001
|
+
* @param values - Values to check for presence.
|
|
4002
|
+
* @returns Partitioned result: `{ existing, missing }`.
|
|
3157
4003
|
*/
|
|
3158
4004
|
checkMissingValues(values) {
|
|
3159
4005
|
const allOptions = Array.from(this.select.options);
|
|
@@ -3165,7 +4011,7 @@ class SearchController {
|
|
|
3165
4011
|
/**
|
|
3166
4012
|
* Configures AJAX settings used for remote searching and pagination.
|
|
3167
4013
|
*
|
|
3168
|
-
* @param
|
|
4014
|
+
* @param config - AJAX configuration (endpoint, method, data builders, etc.).
|
|
3169
4015
|
*/
|
|
3170
4016
|
setAjax(config) {
|
|
3171
4017
|
this.ajaxConfig = config;
|
|
@@ -3173,13 +4019,15 @@ class SearchController {
|
|
|
3173
4019
|
/**
|
|
3174
4020
|
* Attaches a Popup instance to allow UI updates during search (e.g., loading, resize).
|
|
3175
4021
|
*
|
|
3176
|
-
* @param
|
|
4022
|
+
* @param popupInstance - The popup used to display search results and loading state.
|
|
3177
4023
|
*/
|
|
3178
4024
|
setPopup(popupInstance) {
|
|
3179
4025
|
this.popup = popupInstance;
|
|
3180
4026
|
}
|
|
3181
4027
|
/**
|
|
3182
4028
|
* Returns a shallow copy of the current pagination state used for search/infinite scroll.
|
|
4029
|
+
*
|
|
4030
|
+
* @returns Pagination state snapshot.
|
|
3183
4031
|
*/
|
|
3184
4032
|
getPaginationState() {
|
|
3185
4033
|
return { ...this.paginationState };
|
|
@@ -3200,6 +4048,8 @@ class SearchController {
|
|
|
3200
4048
|
}
|
|
3201
4049
|
/**
|
|
3202
4050
|
* Clears the current keyword and makes all options visible (local reset).
|
|
4051
|
+
*
|
|
4052
|
+
* No network requests are made; operates on the current model set.
|
|
3203
4053
|
*/
|
|
3204
4054
|
clear() {
|
|
3205
4055
|
this.paginationState.currentKeyword = "";
|
|
@@ -3217,14 +4067,20 @@ class SearchController {
|
|
|
3217
4067
|
}
|
|
3218
4068
|
/**
|
|
3219
4069
|
* Performs a search with either AJAX or local filtering depending on configuration.
|
|
4070
|
+
*
|
|
4071
|
+
* @param keyword - The search term to apply.
|
|
4072
|
+
* @param append - For AJAX mode: whether to append results (next page). Defaults to false.
|
|
4073
|
+
* @returns An implementation-specific result object.
|
|
3220
4074
|
*/
|
|
3221
4075
|
async search(keyword, append = false) {
|
|
3222
4076
|
if (this.ajaxConfig)
|
|
3223
|
-
return this.
|
|
3224
|
-
return this.
|
|
4077
|
+
return this.ajaxSearch(keyword, append);
|
|
4078
|
+
return this.localSearch(keyword);
|
|
3225
4079
|
}
|
|
3226
4080
|
/**
|
|
3227
4081
|
* Loads the next page for AJAX pagination if enabled and not already loading.
|
|
4082
|
+
*
|
|
4083
|
+
* @returns Result of the paginated AJAX request, or an error when not applicable.
|
|
3228
4084
|
*/
|
|
3229
4085
|
async loadMore() {
|
|
3230
4086
|
if (!this.ajaxConfig)
|
|
@@ -3236,13 +4092,16 @@ class SearchController {
|
|
|
3236
4092
|
if (!this.paginationState.hasMore)
|
|
3237
4093
|
return { success: false, message: "No more data" };
|
|
3238
4094
|
this.paginationState.currentPage++;
|
|
3239
|
-
return this.
|
|
4095
|
+
return this.ajaxSearch(this.paginationState.currentKeyword, true);
|
|
3240
4096
|
}
|
|
3241
4097
|
/**
|
|
3242
4098
|
* Executes a local (in-memory) search by normalizing the keyword (lowercase, non-accent)
|
|
3243
|
-
* and toggling each option's visibility based on text match.
|
|
4099
|
+
* and toggling each option's visibility based on text match.
|
|
4100
|
+
*
|
|
4101
|
+
* @param keyword - Keyword to filter against local options.
|
|
4102
|
+
* @returns Summary flags: `{ success, hasResults, isEmpty }`.
|
|
3244
4103
|
*/
|
|
3245
|
-
async
|
|
4104
|
+
async localSearch(keyword) {
|
|
3246
4105
|
if (this.compareSearchTrigger(keyword))
|
|
3247
4106
|
this.paginationState.currentKeyword = keyword;
|
|
3248
4107
|
const lower = String(keyword ?? "").toLowerCase();
|
|
@@ -3262,6 +4121,7 @@ class SearchController {
|
|
|
3262
4121
|
if (isVisible)
|
|
3263
4122
|
hasVisibleItems = true;
|
|
3264
4123
|
});
|
|
4124
|
+
this.update();
|
|
3265
4125
|
return {
|
|
3266
4126
|
success: true,
|
|
3267
4127
|
hasResults: hasVisibleItems,
|
|
@@ -3271,14 +4131,28 @@ class SearchController {
|
|
|
3271
4131
|
/**
|
|
3272
4132
|
* Checks whether the provided keyword differs from the current one,
|
|
3273
4133
|
* to determine if a new search should be triggered.
|
|
4134
|
+
*
|
|
4135
|
+
* @param keyword - The keyword to compare with the current search term.
|
|
4136
|
+
* @returns True if a new search should be triggered; otherwise false.
|
|
3274
4137
|
*/
|
|
3275
4138
|
compareSearchTrigger(keyword) {
|
|
3276
4139
|
return keyword !== this.paginationState.currentKeyword;
|
|
3277
4140
|
}
|
|
3278
4141
|
/**
|
|
3279
4142
|
* Executes an AJAX-based search with optional appending.
|
|
4143
|
+
*
|
|
4144
|
+
* Behavior:
|
|
4145
|
+
* - Aborts any in-flight request before starting a new search
|
|
4146
|
+
* - Shows loading in the popup (if available)
|
|
4147
|
+
* - Supports GET/POST with data built from config or function
|
|
4148
|
+
* - Applies normalized results to the <select>, respecting `keepSelected`
|
|
4149
|
+
* - Updates pagination state when server response includes pagination info
|
|
4150
|
+
*
|
|
4151
|
+
* @param keyword - Search keyword.
|
|
4152
|
+
* @param append - Whether to append results (true = next page); defaults to false.
|
|
4153
|
+
* @returns An implementation-specific result object with success and pagination flags.
|
|
3280
4154
|
*/
|
|
3281
|
-
async
|
|
4155
|
+
async ajaxSearch(keyword, append = false) {
|
|
3282
4156
|
const cfg = this.ajaxConfig;
|
|
3283
4157
|
if (this.compareSearchTrigger(keyword)) {
|
|
3284
4158
|
this.resetPagination();
|
|
@@ -3332,6 +4206,7 @@ class SearchController {
|
|
|
3332
4206
|
this.applyAjaxResult(result.items, !!cfg.keepSelected, append);
|
|
3333
4207
|
this.paginationState.isLoading = false;
|
|
3334
4208
|
this.popup?.hideLoading();
|
|
4209
|
+
this.update();
|
|
3335
4210
|
return {
|
|
3336
4211
|
success: true,
|
|
3337
4212
|
hasResults: result.items.length > 0,
|
|
@@ -3352,7 +4227,20 @@ class SearchController {
|
|
|
3352
4227
|
}
|
|
3353
4228
|
}
|
|
3354
4229
|
/**
|
|
3355
|
-
*
|
|
4230
|
+
* Normalizes various server response shapes into a standard structure for options and groups.
|
|
4231
|
+
*
|
|
4232
|
+
* Supported shapes (examples):
|
|
4233
|
+
* - `{ object: [...], page?, totalPages?, hasMore? }`
|
|
4234
|
+
* - `{ data: [...], page?, totalPages?, hasMore? }`
|
|
4235
|
+
* - `{ items: [...], pagination: { page, totalPages, hasMore } }`
|
|
4236
|
+
* - `[...]` (array of items)
|
|
4237
|
+
*
|
|
4238
|
+
* Each item can represent either:
|
|
4239
|
+
* - An option: `{ type: "option", value, text, selected?, data? }`
|
|
4240
|
+
* - A group: `{ type: "optgroup", label, data?, options: [...] }`
|
|
4241
|
+
*
|
|
4242
|
+
* @param data - Server response (any shape).
|
|
4243
|
+
* @returns `{ items, hasPagination, page, totalPages, hasMore }`
|
|
3356
4244
|
*/
|
|
3357
4245
|
parseResponse(data) {
|
|
3358
4246
|
let items = [];
|
|
@@ -3419,6 +4307,15 @@ class SearchController {
|
|
|
3419
4307
|
}
|
|
3420
4308
|
/**
|
|
3421
4309
|
* Applies normalized AJAX results to the underlying <select> element.
|
|
4310
|
+
*
|
|
4311
|
+
* Behavior:
|
|
4312
|
+
* - Optionally preserves existing selections (`keepSelected`)
|
|
4313
|
+
* - Clears existing children unless `append` is true
|
|
4314
|
+
* - Supports adding either normalized items or raw HTMLOption/HTMLOptGroup elements
|
|
4315
|
+
*
|
|
4316
|
+
* @param items - Normalized items (or raw HTMLOption/HTMLOptGroup).
|
|
4317
|
+
* @param keepSelected - Preserve previously selected options.
|
|
4318
|
+
* @param append - Append to existing options instead of replacing.
|
|
3422
4319
|
*/
|
|
3423
4320
|
applyAjaxResult(items, keepSelected, append = false) {
|
|
3424
4321
|
const select = this.select;
|
|
@@ -3428,7 +4325,7 @@ class SearchController {
|
|
|
3428
4325
|
if (!append)
|
|
3429
4326
|
select.innerHTML = "";
|
|
3430
4327
|
items.forEach((item) => {
|
|
3431
|
-
// Skip empty item
|
|
4328
|
+
// Skip empty item (defensive guard)
|
|
3432
4329
|
if ((item["type"] === "option" || !item["type"]) && item["value"] === "" && item["text"] === "")
|
|
3433
4330
|
return;
|
|
3434
4331
|
if (item instanceof HTMLOptionElement || item instanceof HTMLOptGroupElement) {
|
|
@@ -3477,6 +4374,25 @@ class SearchController {
|
|
|
3477
4374
|
}
|
|
3478
4375
|
});
|
|
3479
4376
|
}
|
|
4377
|
+
/**
|
|
4378
|
+
* Destroys the controller and clears references.
|
|
4379
|
+
*
|
|
4380
|
+
* Notes:
|
|
4381
|
+
* - In-flight requests are not aborted here; consumers should abort if needed before destroy.
|
|
4382
|
+
* - The linked Popup/ModelManager exist outside this controller and are not destroyed here.
|
|
4383
|
+
*/
|
|
4384
|
+
destroy() {
|
|
4385
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
this.select = null;
|
|
4389
|
+
this.modelManager = null;
|
|
4390
|
+
this.ajaxConfig = null;
|
|
4391
|
+
this.abortController = null;
|
|
4392
|
+
this.popup = null;
|
|
4393
|
+
this.selectBox = null;
|
|
4394
|
+
super.destroy();
|
|
4395
|
+
}
|
|
3480
4396
|
}
|
|
3481
4397
|
|
|
3482
4398
|
/**
|
|
@@ -3640,36 +4556,54 @@ class DatasetObserver {
|
|
|
3640
4556
|
}
|
|
3641
4557
|
|
|
3642
4558
|
/**
|
|
3643
|
-
*
|
|
3644
|
-
*
|
|
4559
|
+
* Base adapter that manages a list of model items and their corresponding views.
|
|
4560
|
+
*
|
|
4561
|
+
* Responsibilities:
|
|
4562
|
+
* - Hold and manage an item list (`items`)
|
|
4563
|
+
* - Create/bind item views (via `viewHolder()` and `onViewHolder()`)
|
|
4564
|
+
* - Expose a property-change event pipeline (`onPropChanging` / `onPropChanged`)
|
|
4565
|
+
* - Cooperate with a RecyclerView through `updateRecyclerView(parent)`
|
|
4566
|
+
* - Participate in the standard lifecycle (`init` → `mount` → `update` → `destroy`)
|
|
4567
|
+
*
|
|
4568
|
+
* Notes:
|
|
4569
|
+
* - Items are expected to embed a `view` reference and an `isInit` flag to track first render.
|
|
4570
|
+
* - Subclasses should override `viewHolder()` to return a concrete `TViewer`.
|
|
4571
|
+
*
|
|
4572
|
+
* @template TItem - Model type the adapter operates on. Must contain `{ view: TViewer | null; isInit: boolean }`.
|
|
4573
|
+
* @template TViewer - View type associated with each item (must implement `ViewContract`).
|
|
4574
|
+
*
|
|
3645
4575
|
* @implements {AdapterContract<TItem>}
|
|
3646
4576
|
*/
|
|
3647
|
-
class Adapter {
|
|
4577
|
+
class Adapter extends Lifecycle {
|
|
3648
4578
|
/**
|
|
3649
|
-
* Initializes the adapter with an optional array of items and
|
|
3650
|
-
*
|
|
4579
|
+
* Initializes the adapter with an optional array of items and starts its lifecycle.
|
|
4580
|
+
* Subclasses may override the lifecycle hooks (e.g., `onInit`) for custom setup.
|
|
3651
4581
|
*
|
|
3652
4582
|
* @param {TItem[]} [items=[]] - Initial items to be managed by the adapter.
|
|
3653
4583
|
*/
|
|
3654
4584
|
constructor(items = []) {
|
|
4585
|
+
super();
|
|
4586
|
+
/** Current list of items managed by the adapter. */
|
|
3655
4587
|
this.items = [];
|
|
4588
|
+
/** Unique key for this adapter instance (used to namespace callback pipelines). */
|
|
3656
4589
|
this.adapterKey = Libs.randomString(12);
|
|
4590
|
+
/** When true, suppresses certain event emissions (reserved for external coordination). */
|
|
3657
4591
|
this.isSkipEvent = false;
|
|
3658
4592
|
this.items = items;
|
|
3659
|
-
this.
|
|
4593
|
+
this.init();
|
|
3660
4594
|
}
|
|
3661
4595
|
/**
|
|
3662
|
-
*
|
|
3663
|
-
*
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
*
|
|
3668
|
-
*
|
|
4596
|
+
* Binds an item model to its viewer at a given position.
|
|
4597
|
+
*
|
|
4598
|
+
* Behavior:
|
|
4599
|
+
* - If the item has been initialized (`isInit = true`), calls `viewer.update()`
|
|
4600
|
+
* - Otherwise, calls `viewer.mount()` to perform the initial render
|
|
4601
|
+
*
|
|
4602
|
+
* Subclasses may override to customize bind logic (animations, diffing, etc.).
|
|
3669
4603
|
*
|
|
3670
4604
|
* @param {TItem} item - The model instance to bind to the view.
|
|
3671
|
-
* @param {TViewer|null} viewer - The view
|
|
3672
|
-
* @param {number} position -
|
|
4605
|
+
* @param {TViewer|null} viewer - The view responsible for rendering the model.
|
|
4606
|
+
* @param {number} position - Index of the item within the adapter.
|
|
3673
4607
|
*/
|
|
3674
4608
|
onViewHolder(item, viewer, position) {
|
|
3675
4609
|
const v = viewer;
|
|
@@ -3677,56 +4611,68 @@ class Adapter {
|
|
|
3677
4611
|
v?.update?.();
|
|
3678
4612
|
}
|
|
3679
4613
|
else {
|
|
3680
|
-
v?.
|
|
4614
|
+
v?.mount?.();
|
|
3681
4615
|
}
|
|
3682
4616
|
}
|
|
3683
4617
|
/**
|
|
3684
|
-
* Registers a pre-change
|
|
3685
|
-
* The callback is scheduled with a minimal delay to batch rapid updates.
|
|
4618
|
+
* Registers a **pre-change** callback for a property pipeline (e.g., `"items"`).
|
|
3686
4619
|
*
|
|
3687
|
-
*
|
|
4620
|
+
* Execution semantics:
|
|
4621
|
+
* - Scheduled with minimal debounce (0ms) by the global callback scheduler
|
|
4622
|
+
* - Runs **before** the property value is updated
|
|
4623
|
+
* - Can be used to clear UI or allocate resources
|
|
4624
|
+
*
|
|
4625
|
+
* @param {string} propName - Property name to observe (e.g., `"items"`).
|
|
3688
4626
|
* @param {Function} callback - Function to execute before the property changes.
|
|
3689
4627
|
*/
|
|
3690
4628
|
onPropChanging(propName, callback) {
|
|
3691
4629
|
Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce: 0 });
|
|
3692
4630
|
}
|
|
3693
4631
|
/**
|
|
3694
|
-
* Registers a post-change callback for a property
|
|
3695
|
-
*
|
|
4632
|
+
* Registers a **post-change** callback for a property pipeline (e.g., `"items"`).
|
|
4633
|
+
*
|
|
4634
|
+
* Execution semantics:
|
|
4635
|
+
* - Scheduled with minimal debounce (0ms) by the global callback scheduler
|
|
4636
|
+
* - Runs **after** the property value is updated
|
|
4637
|
+
* - Can be used to re-render or refresh UI
|
|
3696
4638
|
*
|
|
3697
|
-
* @param {string} propName -
|
|
4639
|
+
* @param {string} propName - Property name to observe (e.g., `"items"`).
|
|
3698
4640
|
* @param {Function} callback - Function to execute after the property changes.
|
|
3699
4641
|
*/
|
|
3700
4642
|
onPropChanged(propName, callback) {
|
|
3701
4643
|
Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback, { debounce: 0 });
|
|
3702
4644
|
}
|
|
3703
4645
|
/**
|
|
3704
|
-
* Triggers the post-change pipeline for a given property
|
|
3705
|
-
*
|
|
4646
|
+
* Triggers the **post-change** pipeline for a given property.
|
|
4647
|
+
* Use this **after** mutating the adapter state.
|
|
3706
4648
|
*
|
|
3707
|
-
* @param {string} propName -
|
|
4649
|
+
* @param {string} propName - Property name to emit (e.g., `"items"`).
|
|
3708
4650
|
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
4651
|
+
* @returns {Promise<void>} - Resolves when all callbacks complete.
|
|
3709
4652
|
*/
|
|
3710
4653
|
changeProp(propName, ...params) {
|
|
3711
4654
|
return Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params);
|
|
3712
4655
|
}
|
|
3713
4656
|
/**
|
|
3714
|
-
* Triggers the pre-change pipeline for a given property
|
|
3715
|
-
*
|
|
4657
|
+
* Triggers the **pre-change** pipeline for a given property.
|
|
4658
|
+
* Use this **before** mutating the adapter state.
|
|
3716
4659
|
*
|
|
3717
|
-
* @param {string} propName -
|
|
4660
|
+
* @param {string} propName - Property name to emit (e.g., `"items"`).
|
|
3718
4661
|
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
4662
|
+
* @returns {Promise<void>} - Resolves when all callbacks complete.
|
|
3719
4663
|
*/
|
|
3720
4664
|
changingProp(propName, ...params) {
|
|
3721
4665
|
return Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params);
|
|
3722
4666
|
}
|
|
3723
4667
|
/**
|
|
3724
|
-
*
|
|
3725
|
-
*
|
|
4668
|
+
* Factory method that creates a viewer instance for the given item,
|
|
4669
|
+
* attached to the specified parent container.
|
|
3726
4670
|
*
|
|
3727
|
-
*
|
|
3728
|
-
*
|
|
3729
|
-
* @
|
|
4671
|
+
* Subclasses **must** override this to return a concrete viewer.
|
|
4672
|
+
*
|
|
4673
|
+
* @param {HTMLElement} parent - Container element that will host the viewer.
|
|
4674
|
+
* @param {TItem} item - The model for which the viewer is created.
|
|
4675
|
+
* @returns {TViewer|null} - The created viewer instance; `null` by default.
|
|
3730
4676
|
*/
|
|
3731
4677
|
viewHolder(parent, item) {
|
|
3732
4678
|
return null;
|
|
@@ -3740,8 +4686,16 @@ class Adapter {
|
|
|
3740
4686
|
return this.items.length;
|
|
3741
4687
|
}
|
|
3742
4688
|
/**
|
|
3743
|
-
* Replaces the adapter's items with a new collection
|
|
3744
|
-
*
|
|
4689
|
+
* Replaces the adapter's items with a new collection.
|
|
4690
|
+
*
|
|
4691
|
+
* Event flow:
|
|
4692
|
+
* 1) Emit `changingProp('items')` (pre-change)
|
|
4693
|
+
* 2) Assign the new items array
|
|
4694
|
+
* 3) Emit `changeProp('items')` (post-change)
|
|
4695
|
+
* 4) Emit lifecycle `update()`
|
|
4696
|
+
*
|
|
4697
|
+
* Note: This does **not** directly render to the DOM.
|
|
4698
|
+
* Call `updateRecyclerView()` (typically via RecyclerView) to apply.
|
|
3745
4699
|
*
|
|
3746
4700
|
* @param {TItem[]} items - The new list of items to set.
|
|
3747
4701
|
*/
|
|
@@ -3749,10 +4703,10 @@ class Adapter {
|
|
|
3749
4703
|
await this.changingProp("items", items);
|
|
3750
4704
|
this.items = items;
|
|
3751
4705
|
await this.changeProp("items", items);
|
|
4706
|
+
this.update();
|
|
3752
4707
|
}
|
|
3753
4708
|
/**
|
|
3754
|
-
* Synchronizes adapter items from an external source by delegating to setItems()
|
|
3755
|
-
* Useful for keeping adapter state aligned with another data store.
|
|
4709
|
+
* Synchronizes adapter items from an external source by delegating to `setItems()`.
|
|
3756
4710
|
*
|
|
3757
4711
|
* @param {TItem[]} items - The source list of items to synchronize.
|
|
3758
4712
|
*/
|
|
@@ -3760,10 +4714,16 @@ class Adapter {
|
|
|
3760
4714
|
await this.setItems(items);
|
|
3761
4715
|
}
|
|
3762
4716
|
/**
|
|
3763
|
-
*
|
|
3764
|
-
*
|
|
4717
|
+
* Ensures each item has a viewer, then binds it through `onViewHolder()`.
|
|
4718
|
+
*
|
|
4719
|
+
* Flow:
|
|
4720
|
+
* - Iterate items in order
|
|
4721
|
+
* - If an item is not initialized:
|
|
4722
|
+
* - Create a viewer via `viewHolder(parent, item)` and assign to `item.view`
|
|
4723
|
+
* - Call `onViewHolder(item, viewer, index)` to mount/update accordingly
|
|
4724
|
+
* - Mark item as initialized (`isInit = true`)
|
|
3765
4725
|
*
|
|
3766
|
-
* @param {HTMLElement} parent - The container
|
|
4726
|
+
* @param {HTMLElement} parent - The container in which item viewers are rendered.
|
|
3767
4727
|
*/
|
|
3768
4728
|
updateRecyclerView(parent) {
|
|
3769
4729
|
for (let index = 0; index < this.itemCount(); index++) {
|
|
@@ -3778,65 +4738,127 @@ class Adapter {
|
|
|
3778
4738
|
}
|
|
3779
4739
|
}
|
|
3780
4740
|
/**
|
|
3781
|
-
*
|
|
3782
|
-
*
|
|
4741
|
+
* Hook for updating adapter data without performing default actions.
|
|
4742
|
+
* Subclasses can override to implement custom refresh logic.
|
|
3783
4743
|
*
|
|
3784
|
-
* @param {TItem[]} items -
|
|
4744
|
+
* @param {TItem[]} items - Incoming data to apply to the adapter.
|
|
3785
4745
|
*/
|
|
3786
4746
|
updateData(items) {
|
|
3787
4747
|
}
|
|
4748
|
+
/**
|
|
4749
|
+
* Destroys the adapter and releases references.
|
|
4750
|
+
*
|
|
4751
|
+
* - Clears the RecyclerView reference (if any)
|
|
4752
|
+
* - Empties the item array
|
|
4753
|
+
* - (Subclasses may override to destroy item views if needed)
|
|
4754
|
+
*/
|
|
4755
|
+
destroy() {
|
|
4756
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
4757
|
+
return;
|
|
4758
|
+
}
|
|
4759
|
+
this.recyclerView = null;
|
|
4760
|
+
this.items.forEach(item => {
|
|
4761
|
+
item?.destroy?.();
|
|
4762
|
+
});
|
|
4763
|
+
this.items = [];
|
|
4764
|
+
}
|
|
3788
4765
|
}
|
|
3789
4766
|
|
|
3790
4767
|
/**
|
|
3791
|
-
*
|
|
4768
|
+
* Base View class that anchors a mounted DOM structure into a parent container.
|
|
4769
|
+
*
|
|
4770
|
+
* Responsibilities:
|
|
4771
|
+
* - Hold a reference to the parent container (`parent`)
|
|
4772
|
+
* - Store the mounted structure (`view`) returned by a mounting helper
|
|
4773
|
+
* - Provide a safe getter for the root element (`getView()`)
|
|
4774
|
+
* - Participate in the standard lifecycle (`init` → `mount` → `update` → `destroy`)
|
|
4775
|
+
*
|
|
4776
|
+
* Typical usage:
|
|
4777
|
+
* - Subclasses set `this.view` inside `onMount()` using a mounting utility
|
|
4778
|
+
* - Then call `this.parent!.appendChild(this.view.view)`
|
|
4779
|
+
*
|
|
4780
|
+
* @template TTags - A map of tag names to their corresponding HTMLElement instances.
|
|
3792
4781
|
* @implements {ViewContract<TTags>}
|
|
3793
4782
|
*/
|
|
3794
|
-
class View {
|
|
4783
|
+
class View extends Lifecycle {
|
|
3795
4784
|
/**
|
|
3796
|
-
*
|
|
4785
|
+
* Creates a View bound to the specified parent container.
|
|
4786
|
+
*
|
|
4787
|
+
* Note: Subclasses should assign `this.view` during `onMount()` before
|
|
4788
|
+
* attempting to access `getView()` or manipulate the root element.
|
|
3797
4789
|
*
|
|
3798
|
-
* @param
|
|
4790
|
+
* @param parent - The parent element into which this view will render.
|
|
3799
4791
|
*/
|
|
3800
4792
|
constructor(parent) {
|
|
4793
|
+
super();
|
|
4794
|
+
/** The parent DOM element into which this view is rendered. */
|
|
3801
4795
|
this.parent = null;
|
|
4796
|
+
/**
|
|
4797
|
+
* Mounted result containing:
|
|
4798
|
+
* - `view`: the root element of this view
|
|
4799
|
+
* - `tags`: a strongly-typed map of child elements
|
|
4800
|
+
*/
|
|
3802
4801
|
this.view = null;
|
|
3803
4802
|
this.parent = parent;
|
|
4803
|
+
this.init();
|
|
3804
4804
|
}
|
|
3805
|
-
/**
|
|
3806
|
-
* Renders the view into the parent container.
|
|
3807
|
-
* Override in subclasses to create DOM structure and mount tags.
|
|
3808
|
-
*/
|
|
3809
|
-
render() { }
|
|
3810
|
-
/**
|
|
3811
|
-
* Updates the view to reflect model or state changes.
|
|
3812
|
-
* Override in subclasses to patch DOM nodes without full re-render.
|
|
3813
|
-
*/
|
|
3814
|
-
update() { }
|
|
3815
4805
|
/**
|
|
3816
4806
|
* Returns the root HTMLElement of the mounted view.
|
|
3817
4807
|
*
|
|
3818
|
-
* @returns
|
|
4808
|
+
* @returns The root element produced by the mounting helper.
|
|
4809
|
+
* @throws {Error} If the view has not been mounted or `this.view` is not set.
|
|
3819
4810
|
*/
|
|
3820
4811
|
getView() {
|
|
3821
|
-
if (!this.view?.view)
|
|
4812
|
+
if (!this.view?.view) {
|
|
3822
4813
|
throw new Error("View is not mounted. Did you forget to set this.view?");
|
|
4814
|
+
}
|
|
3823
4815
|
return this.view.view;
|
|
3824
4816
|
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Destroys the view and releases DOM references.
|
|
4819
|
+
*
|
|
4820
|
+
* - Removes the root element from the DOM (if present)
|
|
4821
|
+
* - Clears references to `parent` and `view`
|
|
4822
|
+
* - Ends the lifecycle via `super.destroy()`
|
|
4823
|
+
*/
|
|
4824
|
+
destroy() {
|
|
4825
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
this.getView()?.remove?.();
|
|
4829
|
+
this.parent = null;
|
|
4830
|
+
this.view = null;
|
|
4831
|
+
super.destroy();
|
|
4832
|
+
}
|
|
3825
4833
|
}
|
|
3826
4834
|
|
|
3827
4835
|
/**
|
|
3828
|
-
*
|
|
4836
|
+
* View implementation responsible for rendering and managing
|
|
4837
|
+
* a grouped collection of selectable items.
|
|
4838
|
+
*
|
|
4839
|
+
* The group consists of:
|
|
4840
|
+
* - A header element (label)
|
|
4841
|
+
* - A container holding child item views
|
|
4842
|
+
*
|
|
4843
|
+
* @extends View<GroupViewTags>
|
|
3829
4844
|
*/
|
|
3830
4845
|
class GroupView extends View {
|
|
3831
4846
|
constructor() {
|
|
3832
4847
|
super(...arguments);
|
|
4848
|
+
/**
|
|
4849
|
+
* Strongly-typed reference to the mounted group view structure.
|
|
4850
|
+
* Will be null until the view has been mounted.
|
|
4851
|
+
*/
|
|
3833
4852
|
this.view = null;
|
|
3834
4853
|
}
|
|
3835
4854
|
/**
|
|
3836
|
-
*
|
|
3837
|
-
*
|
|
4855
|
+
* Mounts the group view into the DOM.
|
|
4856
|
+
*
|
|
4857
|
+
* Creates the group container, header, and items wrapper,
|
|
4858
|
+
* applies required ARIA attributes, and appends the root
|
|
4859
|
+
* element to the parent container.
|
|
3838
4860
|
*/
|
|
3839
|
-
|
|
4861
|
+
mount() {
|
|
3840
4862
|
const group_id = Libs.randomString(7);
|
|
3841
4863
|
this.view = Libs.mountView({
|
|
3842
4864
|
GroupView: {
|
|
@@ -3866,53 +4888,68 @@ class GroupView extends View {
|
|
|
3866
4888
|
},
|
|
3867
4889
|
},
|
|
3868
4890
|
});
|
|
3869
|
-
// Parent is guaranteed by base constructor.
|
|
4891
|
+
// Parent is guaranteed to exist by the base View constructor.
|
|
3870
4892
|
this.parent.appendChild(this.view.view);
|
|
4893
|
+
super.mount();
|
|
3871
4894
|
}
|
|
3872
4895
|
/**
|
|
3873
|
-
*
|
|
4896
|
+
* Called when the view needs to be updated.
|
|
4897
|
+
*
|
|
4898
|
+
* Currently performs a lightweight refresh by updating
|
|
4899
|
+
* the group header label.
|
|
3874
4900
|
*/
|
|
3875
4901
|
update() {
|
|
3876
4902
|
this.updateLabel();
|
|
4903
|
+
super.update();
|
|
3877
4904
|
}
|
|
3878
4905
|
/**
|
|
3879
|
-
* Updates the
|
|
4906
|
+
* Updates the text content of the group header.
|
|
3880
4907
|
*
|
|
3881
|
-
* @param
|
|
4908
|
+
* @param label - The new label to display.
|
|
4909
|
+
* If null, the existing label is preserved.
|
|
3882
4910
|
*/
|
|
3883
4911
|
updateLabel(label = null) {
|
|
3884
4912
|
if (!this.view)
|
|
3885
4913
|
return;
|
|
3886
4914
|
const headerEl = this.view.tags.GroupHeader;
|
|
3887
|
-
if (label !== null)
|
|
4915
|
+
if (label !== null) {
|
|
3888
4916
|
headerEl.textContent = label;
|
|
4917
|
+
}
|
|
3889
4918
|
}
|
|
3890
4919
|
/**
|
|
3891
|
-
* Returns the container element that holds all option
|
|
4920
|
+
* Returns the container element that holds all item/option views
|
|
4921
|
+
* belonging to this group.
|
|
3892
4922
|
*
|
|
3893
|
-
* @returns
|
|
4923
|
+
* @returns The HTMLDivElement used as the items container.
|
|
4924
|
+
* @throws {Error} If the view has not been mounted yet.
|
|
3894
4925
|
*/
|
|
3895
4926
|
getItemsContainer() {
|
|
3896
|
-
if (!this.view)
|
|
3897
|
-
throw new Error("GroupView
|
|
4927
|
+
if (!this.view) {
|
|
4928
|
+
throw new Error("GroupView has not been rendered.");
|
|
4929
|
+
}
|
|
3898
4930
|
return this.view.tags.GroupItems;
|
|
3899
4931
|
}
|
|
3900
4932
|
/**
|
|
3901
|
-
*
|
|
3902
|
-
*
|
|
4933
|
+
* Updates the visibility of the group based on its children.
|
|
4934
|
+
*
|
|
4935
|
+
* If all child items are hidden, the entire group
|
|
4936
|
+
* will be hidden as well.
|
|
3903
4937
|
*/
|
|
3904
4938
|
updateVisibility() {
|
|
3905
4939
|
if (!this.view)
|
|
3906
4940
|
return;
|
|
3907
4941
|
const items = this.view.tags.GroupItems;
|
|
3908
|
-
const visibleItems = Array.from(items.children).filter(
|
|
3909
|
-
|
|
3910
|
-
this.view.view.classList.toggle("hide", !isVisible);
|
|
4942
|
+
const visibleItems = Array.from(items.children).filter(child => !child.classList.contains("hide"));
|
|
4943
|
+
this.view.view.classList.toggle("hide", visibleItems.length === 0);
|
|
3911
4944
|
}
|
|
3912
4945
|
/**
|
|
3913
|
-
* Sets the collapsed state
|
|
4946
|
+
* Sets the collapsed/expanded state of the group.
|
|
3914
4947
|
*
|
|
3915
|
-
*
|
|
4948
|
+
* This updates both:
|
|
4949
|
+
* - Visual state (CSS classes)
|
|
4950
|
+
* - Accessibility state (ARIA attributes)
|
|
4951
|
+
*
|
|
4952
|
+
* @param collapsed - True to collapse the group, false to expand it.
|
|
3916
4953
|
*/
|
|
3917
4954
|
setCollapsed(collapsed) {
|
|
3918
4955
|
if (!this.view)
|
|
@@ -3923,30 +4960,62 @@ class GroupView extends View {
|
|
|
3923
4960
|
}
|
|
3924
4961
|
|
|
3925
4962
|
/**
|
|
3926
|
-
*
|
|
4963
|
+
* View implementation for a single selectable option.
|
|
4964
|
+
*
|
|
4965
|
+
* An option may consist of:
|
|
4966
|
+
* - An input element (radio or checkbox)
|
|
4967
|
+
* - An optional image
|
|
4968
|
+
* - A label container
|
|
4969
|
+
*
|
|
4970
|
+
* This view supports reactive configuration changes via a Proxy,
|
|
4971
|
+
* allowing partial DOM updates without fully re-rendering the view.
|
|
4972
|
+
*
|
|
4973
|
+
* @extends View<OptionViewTags>
|
|
3927
4974
|
*/
|
|
3928
4975
|
class OptionView extends View {
|
|
3929
4976
|
/**
|
|
3930
|
-
*
|
|
3931
|
-
* The proxy enables partial DOM updates when config properties change after initial render.
|
|
4977
|
+
* Creates a new OptionView bound to the given parent element.
|
|
3932
4978
|
*
|
|
3933
|
-
*
|
|
4979
|
+
* Initializes the internal configuration and sets up
|
|
4980
|
+
* a reactive Proxy to track and apply configuration changes.
|
|
4981
|
+
*
|
|
4982
|
+
* @param parent - The container element that will host this option view.
|
|
3934
4983
|
*/
|
|
3935
4984
|
constructor(parent) {
|
|
3936
4985
|
super(parent);
|
|
4986
|
+
/**
|
|
4987
|
+
* Reference to the mounted option view.
|
|
4988
|
+
* Set during `onMount`; null before render.
|
|
4989
|
+
*/
|
|
3937
4990
|
this.view = null;
|
|
4991
|
+
/**
|
|
4992
|
+
* Internal configuration state used as the Proxy target.
|
|
4993
|
+
* Should not be mutated directly.
|
|
4994
|
+
*/
|
|
3938
4995
|
this.config = null;
|
|
4996
|
+
/**
|
|
4997
|
+
* Proxy wrapper around `config`.
|
|
4998
|
+
* Assigning properties on this object triggers incremental
|
|
4999
|
+
* DOM updates once the view has been rendered.
|
|
5000
|
+
*/
|
|
3939
5001
|
this.configProxy = null;
|
|
5002
|
+
/**
|
|
5003
|
+
* Indicates whether the initial render has been completed.
|
|
5004
|
+
* Partial DOM updates are skipped until this becomes true.
|
|
5005
|
+
*/
|
|
3940
5006
|
this.isRendered = false;
|
|
3941
|
-
this.
|
|
5007
|
+
this.initialize();
|
|
3942
5008
|
}
|
|
3943
5009
|
/**
|
|
3944
|
-
*
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
3947
|
-
*
|
|
5010
|
+
* Initializes the default configuration object and wraps it in a Proxy.
|
|
5011
|
+
*
|
|
5012
|
+
* The Proxy intercepts property assignments and:
|
|
5013
|
+
* - Updates internal state
|
|
5014
|
+
* - Applies targeted DOM changes when the view is already rendered
|
|
5015
|
+
*
|
|
5016
|
+
* No DOM mutations occur before the initial render.
|
|
3948
5017
|
*/
|
|
3949
|
-
|
|
5018
|
+
initialize() {
|
|
3950
5019
|
const self = this;
|
|
3951
5020
|
this.config = {
|
|
3952
5021
|
isMultiple: false,
|
|
@@ -3973,54 +5042,59 @@ class OptionView extends View {
|
|
|
3973
5042
|
return true;
|
|
3974
5043
|
},
|
|
3975
5044
|
});
|
|
5045
|
+
this.init();
|
|
3976
5046
|
}
|
|
3977
5047
|
/**
|
|
3978
|
-
* Indicates whether the option supports multiple selection
|
|
5048
|
+
* Indicates whether the option supports multiple selection.
|
|
3979
5049
|
*
|
|
3980
|
-
*
|
|
5050
|
+
* - `false`: single selection (radio)
|
|
5051
|
+
* - `true`: multiple selection (checkbox)
|
|
3981
5052
|
*/
|
|
3982
5053
|
get isMultiple() {
|
|
3983
5054
|
return this.config.isMultiple;
|
|
3984
5055
|
}
|
|
3985
5056
|
/**
|
|
3986
5057
|
* Enables or disables multiple selection mode.
|
|
3987
|
-
* When rendered, toggles the root CSS class and switches the input type between 'checkbox' and 'radio'.
|
|
3988
5058
|
*
|
|
3989
|
-
*
|
|
5059
|
+
* When rendered:
|
|
5060
|
+
* - Toggles the `multiple` CSS class on the root element
|
|
5061
|
+
* - Switches the input type between `radio` and `checkbox`
|
|
3990
5062
|
*/
|
|
3991
5063
|
set isMultiple(value) {
|
|
3992
5064
|
this.configProxy.isMultiple = !!value;
|
|
3993
5065
|
}
|
|
3994
5066
|
/**
|
|
3995
|
-
* Indicates whether the option
|
|
3996
|
-
*
|
|
3997
|
-
* @returns {boolean} True if an image is displayed; otherwise false.
|
|
5067
|
+
* Indicates whether the option displays an image next to the label.
|
|
3998
5068
|
*/
|
|
3999
5069
|
get hasImage() {
|
|
4000
5070
|
return this.config.hasImage;
|
|
4001
5071
|
}
|
|
4002
5072
|
/**
|
|
4003
|
-
* Shows or hides the image block
|
|
4004
|
-
* When rendered, toggles related CSS classes and creates/removes the image element accordingly.
|
|
5073
|
+
* Shows or hides the image block.
|
|
4005
5074
|
*
|
|
4006
|
-
*
|
|
5075
|
+
* When rendered:
|
|
5076
|
+
* - Toggles related CSS classes
|
|
5077
|
+
* - Creates or removes the `<img>` element dynamically
|
|
4007
5078
|
*/
|
|
4008
5079
|
set hasImage(value) {
|
|
4009
5080
|
this.configProxy.hasImage = !!value;
|
|
4010
5081
|
}
|
|
4011
5082
|
/**
|
|
4012
|
-
* Provides reactive access to the
|
|
4013
|
-
* Mutating properties on this object will trigger partial DOM updates when rendered.
|
|
5083
|
+
* Provides reactive access to the full option configuration.
|
|
4014
5084
|
*
|
|
4015
|
-
*
|
|
5085
|
+
* Mutating properties on this object triggers incremental
|
|
5086
|
+
* DOM updates when the view has already been rendered.
|
|
4016
5087
|
*/
|
|
4017
5088
|
get optionConfig() {
|
|
4018
5089
|
return this.configProxy;
|
|
4019
5090
|
}
|
|
4020
5091
|
/**
|
|
4021
|
-
* Applies a
|
|
4022
|
-
*
|
|
4023
|
-
*
|
|
5092
|
+
* Applies a batch of configuration changes.
|
|
5093
|
+
*
|
|
5094
|
+
* Only properties whose values differ from the current state
|
|
5095
|
+
* are assigned to the Proxy and processed.
|
|
5096
|
+
*
|
|
5097
|
+
* @param config - Partial configuration patch
|
|
4024
5098
|
*/
|
|
4025
5099
|
set optionConfig(config) {
|
|
4026
5100
|
if (!config || !this.configProxy || !this.config)
|
|
@@ -4038,24 +5112,25 @@ class OptionView extends View {
|
|
|
4038
5112
|
changes.labelValign = config.labelValign;
|
|
4039
5113
|
if (config.labelHalign !== undefined && config.labelHalign !== this.config.labelHalign)
|
|
4040
5114
|
changes.labelHalign = config.labelHalign;
|
|
4041
|
-
if (Object.keys(changes).length > 0)
|
|
5115
|
+
if (Object.keys(changes).length > 0) {
|
|
4042
5116
|
Object.assign(this.configProxy, changes);
|
|
5117
|
+
}
|
|
4043
5118
|
}
|
|
4044
5119
|
/**
|
|
4045
|
-
*
|
|
4046
|
-
*
|
|
4047
|
-
*
|
|
4048
|
-
*
|
|
5120
|
+
* Performs the initial render of the option view.
|
|
5121
|
+
*
|
|
5122
|
+
* Builds the DOM structure based on the current configuration,
|
|
5123
|
+
* assigns CSS classes and ARIA attributes, mounts the view via
|
|
5124
|
+
* `Libs.mountView`, and enables reactive updates afterward.
|
|
4049
5125
|
*/
|
|
4050
|
-
|
|
5126
|
+
mount() {
|
|
4051
5127
|
const viewClass = ["selective-ui-option-view"];
|
|
4052
5128
|
const opt_id = Libs.randomString(7);
|
|
4053
5129
|
const inputID = `option_${opt_id}`;
|
|
4054
5130
|
if (this.config.isMultiple)
|
|
4055
5131
|
viewClass.push("multiple");
|
|
4056
5132
|
if (this.config.hasImage) {
|
|
4057
|
-
viewClass.push("has-image");
|
|
4058
|
-
viewClass.push(`image-${this.config.imagePosition}`);
|
|
5133
|
+
viewClass.push("has-image", `image-${this.config.imagePosition}`);
|
|
4059
5134
|
}
|
|
4060
5135
|
const childStructure = {
|
|
4061
5136
|
OptionInput: {
|
|
@@ -4108,10 +5183,14 @@ class OptionView extends View {
|
|
|
4108
5183
|
});
|
|
4109
5184
|
this.parent.appendChild(this.view.view);
|
|
4110
5185
|
this.isRendered = true;
|
|
5186
|
+
super.mount();
|
|
4111
5187
|
}
|
|
4112
5188
|
/**
|
|
4113
|
-
* Applies a targeted DOM update for a single configuration
|
|
4114
|
-
*
|
|
5189
|
+
* Applies a targeted DOM update for a single configuration change.
|
|
5190
|
+
*
|
|
5191
|
+
* Updates only the affected parts of the view
|
|
5192
|
+
* (classes, attributes, styles, or child nodes)
|
|
5193
|
+
* without triggering a full re-render.
|
|
4115
5194
|
*/
|
|
4116
5195
|
applyPartialChange(prop, newValue, oldValue) {
|
|
4117
5196
|
const v = this.view;
|
|
@@ -4137,18 +5216,20 @@ class OptionView extends View {
|
|
|
4137
5216
|
this.createImage();
|
|
4138
5217
|
}
|
|
4139
5218
|
else {
|
|
4140
|
-
root.className = root.className
|
|
5219
|
+
root.className = root.className
|
|
5220
|
+
.replace(/image-(top|right|bottom|left)/g, "")
|
|
5221
|
+
.trim();
|
|
4141
5222
|
const img = v.tags?.OptionImage;
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
v.tags.OptionImage = null;
|
|
4145
|
-
}
|
|
5223
|
+
img?.remove();
|
|
5224
|
+
v.tags.OptionImage = null;
|
|
4146
5225
|
}
|
|
4147
5226
|
break;
|
|
4148
5227
|
}
|
|
4149
5228
|
case "imagePosition": {
|
|
4150
5229
|
if (this.config.hasImage) {
|
|
4151
|
-
root.className = root.className
|
|
5230
|
+
root.className = root.className
|
|
5231
|
+
.replace(/image-(top|right|bottom|left)/g, "")
|
|
5232
|
+
.trim();
|
|
4152
5233
|
root.classList.add(`image-${String(newValue)}`);
|
|
4153
5234
|
}
|
|
4154
5235
|
break;
|
|
@@ -4161,35 +5242,33 @@ class OptionView extends View {
|
|
|
4161
5242
|
const styleProp = prop === "imageWidth" ? "width" :
|
|
4162
5243
|
prop === "imageHeight" ? "height" :
|
|
4163
5244
|
"borderRadius";
|
|
4164
|
-
|
|
4165
|
-
if (img.style[styleProp] !== val)
|
|
4166
|
-
img.style[styleProp] = val;
|
|
5245
|
+
img.style[styleProp] = String(newValue);
|
|
4167
5246
|
}
|
|
4168
5247
|
break;
|
|
4169
5248
|
}
|
|
4170
5249
|
case "labelValign":
|
|
4171
5250
|
case "labelHalign": {
|
|
4172
5251
|
if (label) {
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
5252
|
+
label.className =
|
|
5253
|
+
`align-vertical-${this.config.labelValign} ` +
|
|
5254
|
+
`align-horizontal-${this.config.labelHalign}`;
|
|
4176
5255
|
}
|
|
4177
5256
|
break;
|
|
4178
5257
|
}
|
|
4179
5258
|
}
|
|
4180
5259
|
}
|
|
4181
5260
|
/**
|
|
4182
|
-
* Creates the
|
|
4183
|
-
*
|
|
4184
|
-
* The image
|
|
4185
|
-
* before the label
|
|
5261
|
+
* Creates and inserts the `<img>` element for the option on demand.
|
|
5262
|
+
*
|
|
5263
|
+
* The image is styled using the current configuration and is inserted
|
|
5264
|
+
* before the label when possible. If an image already exists, no action
|
|
5265
|
+
* is taken.
|
|
4186
5266
|
*/
|
|
4187
5267
|
createImage() {
|
|
4188
5268
|
const v = this.view;
|
|
4189
5269
|
if (!v || !v.view)
|
|
4190
5270
|
return;
|
|
4191
|
-
|
|
4192
|
-
if (existing)
|
|
5271
|
+
if (v.tags?.OptionImage)
|
|
4193
5272
|
return;
|
|
4194
5273
|
const root = v.view;
|
|
4195
5274
|
const label = v.tags?.OptionLabel;
|
|
@@ -4198,27 +5277,64 @@ class OptionView extends View {
|
|
|
4198
5277
|
image.style.width = this.config.imageWidth;
|
|
4199
5278
|
image.style.height = this.config.imageHeight;
|
|
4200
5279
|
image.style.borderRadius = this.config.imageBorderRadius;
|
|
4201
|
-
if (label
|
|
5280
|
+
if (label?.parentElement) {
|
|
4202
5281
|
root.insertBefore(image, label);
|
|
4203
|
-
|
|
5282
|
+
}
|
|
5283
|
+
else {
|
|
4204
5284
|
root.appendChild(image);
|
|
5285
|
+
}
|
|
4205
5286
|
v.tags.OptionImage = image;
|
|
4206
5287
|
}
|
|
4207
5288
|
}
|
|
4208
5289
|
|
|
4209
5290
|
/**
|
|
4210
|
-
*
|
|
5291
|
+
* Adapter that can render a heterogeneous list composed of groups and options.
|
|
5292
|
+
*
|
|
5293
|
+
* Responsibilities:
|
|
5294
|
+
* - Build and maintain a flat list of options for navigation and visibility stats
|
|
5295
|
+
* - Create proper views based on item type (GroupView / OptionView)
|
|
5296
|
+
* - Bind view logic (events, image/label rendering, collapsed/expanded state)
|
|
5297
|
+
* - Track selection for both single and multiple-select modes
|
|
5298
|
+
* - Emit visibility-change statistics to subscribed listeners
|
|
5299
|
+
*
|
|
5300
|
+
* Lifecycle / Events:
|
|
5301
|
+
* - On `init()`: schedule a debounced visibility aggregation dispatcher
|
|
5302
|
+
* - On item changes: rebuild the flat structure and notify observers
|
|
5303
|
+
* - Delegates selection updates via the underlying `OptionModel` event hooks
|
|
5304
|
+
*
|
|
5305
|
+
* @extends Adapter
|
|
4211
5306
|
*/
|
|
4212
5307
|
class MixedAdapter extends Adapter {
|
|
5308
|
+
/**
|
|
5309
|
+
* Creates a MixedAdapter with an optional initial list of items.
|
|
5310
|
+
* Builds an initial flat structure for fast navigation and stats.
|
|
5311
|
+
*
|
|
5312
|
+
* @param items - Initial items (groups and/or options).
|
|
5313
|
+
*/
|
|
4213
5314
|
constructor(items = []) {
|
|
4214
5315
|
super(items);
|
|
5316
|
+
/** Whether the adapter operates in multi-selection mode. */
|
|
4215
5317
|
this.isMultiple = false;
|
|
5318
|
+
/** Registered listeners for aggregated visibility statistics. */
|
|
4216
5319
|
this.visibilityChangedCallbacks = [];
|
|
5320
|
+
/** Index (in the flat list) of the currently highlighted option. */
|
|
4217
5321
|
this.currentHighlightIndex = -1;
|
|
5322
|
+
/** Cached pointer to the single selected item in single-select mode. */
|
|
4218
5323
|
this.selectedItemSingle = null;
|
|
5324
|
+
/** Top-level group models (if any). */
|
|
4219
5325
|
this.groups = [];
|
|
5326
|
+
/** Flat list of all option models (including those inside groups). */
|
|
4220
5327
|
this.flatOptions = [];
|
|
4221
5328
|
this.buildFlatStructure();
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* Initializes internal scheduling for visibility-change notifications,
|
|
5332
|
+
* then calls the base lifecycle and mounts immediately.
|
|
5333
|
+
*
|
|
5334
|
+
* - Debounced `sche_vis_${adapterKey}` computes visibility aggregates
|
|
5335
|
+
* and invokes all registered `visibilityChangedCallbacks`.
|
|
5336
|
+
*/
|
|
5337
|
+
init() {
|
|
4222
5338
|
Libs.callbackScheduler.on(`sche_vis_${this.adapterKey}`, () => {
|
|
4223
5339
|
const visibleCount = this.flatOptions.filter((item) => item.visible).length;
|
|
4224
5340
|
const totalCount = this.flatOptions.length;
|
|
@@ -4230,11 +5346,18 @@ class MixedAdapter extends Adapter {
|
|
|
4230
5346
|
isEmpty: totalCount === 0,
|
|
4231
5347
|
});
|
|
4232
5348
|
});
|
|
5349
|
+
// Proxy hook; allows other listeners to chain after visibility aggregation.
|
|
4233
5350
|
Libs.callbackScheduler.run(`sche_vis_proxy_${this.adapterKey}`);
|
|
4234
5351
|
}, { debounce: 10 });
|
|
5352
|
+
super.init();
|
|
5353
|
+
this.mount();
|
|
4235
5354
|
}
|
|
4236
5355
|
/**
|
|
4237
|
-
*
|
|
5356
|
+
* Builds / rebuilds a flat list of options and captures group references.
|
|
5357
|
+
*
|
|
5358
|
+
* The flat list is used for:
|
|
5359
|
+
* - navigation across visible options
|
|
5360
|
+
* - computing visibility statistics quickly
|
|
4238
5361
|
*/
|
|
4239
5362
|
buildFlatStructure() {
|
|
4240
5363
|
this.flatOptions = [];
|
|
@@ -4250,11 +5373,11 @@ class MixedAdapter extends Adapter {
|
|
|
4250
5373
|
});
|
|
4251
5374
|
}
|
|
4252
5375
|
/**
|
|
4253
|
-
*
|
|
5376
|
+
* Factory method returning the appropriate view implementation per item type.
|
|
4254
5377
|
*
|
|
4255
|
-
* @param
|
|
4256
|
-
* @param
|
|
4257
|
-
* @returns
|
|
5378
|
+
* @param parent - The container element where the view will be mounted.
|
|
5379
|
+
* @param item - The item to render (group or option).
|
|
5380
|
+
* @returns A new GroupView/OptionView instance based on the item type.
|
|
4258
5381
|
*/
|
|
4259
5382
|
viewHolder(parent, item) {
|
|
4260
5383
|
if (item instanceof GroupModel)
|
|
@@ -4262,12 +5385,12 @@ class MixedAdapter extends Adapter {
|
|
|
4262
5385
|
return new OptionView(parent);
|
|
4263
5386
|
}
|
|
4264
5387
|
/**
|
|
4265
|
-
* Binds a data model (
|
|
4266
|
-
*
|
|
5388
|
+
* Binds a data model (group or option) to its view and delegates rendering
|
|
5389
|
+
* to specialized handlers.
|
|
4267
5390
|
*
|
|
4268
|
-
* @param
|
|
4269
|
-
* @param
|
|
4270
|
-
* @param
|
|
5391
|
+
* @param item - GroupModel or OptionModel.
|
|
5392
|
+
* @param viewer - The view instance that will render the model.
|
|
5393
|
+
* @param position - Position of the item in the top-level list.
|
|
4271
5394
|
*/
|
|
4272
5395
|
onViewHolder(item, viewer, position) {
|
|
4273
5396
|
item.position = position;
|
|
@@ -4280,12 +5403,15 @@ class MixedAdapter extends Adapter {
|
|
|
4280
5403
|
item.isInit = true;
|
|
4281
5404
|
}
|
|
4282
5405
|
/**
|
|
4283
|
-
* Handles binding
|
|
4284
|
-
*
|
|
5406
|
+
* Handles binding/rendering for a group:
|
|
5407
|
+
* - Sets header text and click-to-toggle behavior
|
|
5408
|
+
* - Observes collapsed state to hide/show child options
|
|
5409
|
+
* - Ensures each child option is rendered and bound
|
|
5410
|
+
* - Syncs collapsed state and visibility
|
|
4285
5411
|
*
|
|
4286
|
-
* @param
|
|
4287
|
-
* @param
|
|
4288
|
-
* @param
|
|
5412
|
+
* @param groupModel - Group data model.
|
|
5413
|
+
* @param groupView - Group view instance.
|
|
5414
|
+
* @param position - Group index.
|
|
4289
5415
|
*/
|
|
4290
5416
|
handleGroupView(groupModel, groupView, position) {
|
|
4291
5417
|
super.onViewHolder(groupModel, groupView, position);
|
|
@@ -4319,12 +5445,15 @@ class MixedAdapter extends Adapter {
|
|
|
4319
5445
|
groupView.updateVisibility();
|
|
4320
5446
|
}
|
|
4321
5447
|
/**
|
|
4322
|
-
* Handles binding
|
|
4323
|
-
*
|
|
5448
|
+
* Handles binding/rendering for an option:
|
|
5449
|
+
* - Applies adapter-wide and model-specific visual configuration (image, label alignment, etc.)
|
|
5450
|
+
* - Wires click/hover listeners for selection and highlighting
|
|
5451
|
+
* - Syncs selection state (single vs multiple)
|
|
5452
|
+
* - Updates image source/alt and label HTML
|
|
4324
5453
|
*
|
|
4325
|
-
* @param
|
|
4326
|
-
* @param
|
|
4327
|
-
* @param
|
|
5454
|
+
* @param optionModel - Option data model.
|
|
5455
|
+
* @param optionViewer - Option view instance.
|
|
5456
|
+
* @param position - Option index within its group list.
|
|
4328
5457
|
*/
|
|
4329
5458
|
handleOptionView(optionModel, optionViewer, position) {
|
|
4330
5459
|
optionViewer.isMultiple = this.isMultiple;
|
|
@@ -4393,54 +5522,77 @@ class MixedAdapter extends Adapter {
|
|
|
4393
5522
|
}
|
|
4394
5523
|
}
|
|
4395
5524
|
/**
|
|
4396
|
-
*
|
|
5525
|
+
* Replaces items and rebuilds the internal flat structure.
|
|
5526
|
+
* Emits the standard pre/post change notifications and updates lifecycle.
|
|
4397
5527
|
*
|
|
4398
|
-
* @param
|
|
5528
|
+
* @param items - New mixed item collection (groups/options).
|
|
4399
5529
|
*/
|
|
4400
5530
|
async setItems(items) {
|
|
4401
5531
|
await this.changingProp("items", items);
|
|
4402
5532
|
this.items = items;
|
|
4403
5533
|
this.buildFlatStructure();
|
|
4404
5534
|
await this.changeProp("items", items);
|
|
5535
|
+
this.update();
|
|
4405
5536
|
}
|
|
4406
5537
|
/**
|
|
4407
|
-
* Synchronizes
|
|
5538
|
+
* Synchronizes items from an external source by delegating to `setItems()`.
|
|
4408
5539
|
*
|
|
4409
|
-
* @param
|
|
5540
|
+
* @param items - New mixed item collection (groups/options).
|
|
4410
5541
|
*/
|
|
4411
5542
|
async syncFromSource(items) {
|
|
4412
5543
|
await this.setItems(items);
|
|
4413
5544
|
}
|
|
4414
5545
|
/**
|
|
4415
|
-
* Updates
|
|
4416
|
-
* without triggering change notifications.
|
|
5546
|
+
* Updates items and rebuilds the flat structure **without** firing change notifications.
|
|
4417
5547
|
*
|
|
4418
|
-
* @param
|
|
5548
|
+
* @param items - New mixed item collection (groups/options).
|
|
4419
5549
|
*/
|
|
4420
5550
|
updateData(items) {
|
|
4421
5551
|
this.items = items;
|
|
4422
5552
|
this.buildFlatStructure();
|
|
5553
|
+
this.update();
|
|
5554
|
+
}
|
|
5555
|
+
/**
|
|
5556
|
+
* Destroys the adapter and cleans up:
|
|
5557
|
+
* - Clears visibility scheduler
|
|
5558
|
+
* - Destroys all groups (cascades to their items/views)
|
|
5559
|
+
* - Resets cached state and arrays
|
|
5560
|
+
*/
|
|
5561
|
+
destroy() {
|
|
5562
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
5563
|
+
return;
|
|
5564
|
+
}
|
|
5565
|
+
Libs.callbackScheduler.clear(`sche_vis_${this.adapterKey}`);
|
|
5566
|
+
this.groups.forEach(group => {
|
|
5567
|
+
group.destroy();
|
|
5568
|
+
});
|
|
5569
|
+
this.visibilityChangedCallbacks = [];
|
|
5570
|
+
this.currentHighlightIndex = -1;
|
|
5571
|
+
this.selectedItemSingle = null;
|
|
5572
|
+
this.groups = [];
|
|
5573
|
+
this.flatOptions = [];
|
|
5574
|
+
super.destroy();
|
|
4423
5575
|
}
|
|
4424
5576
|
/**
|
|
4425
|
-
* Returns all option items
|
|
5577
|
+
* Returns all currently selected option items.
|
|
4426
5578
|
*
|
|
4427
|
-
* @returns
|
|
5579
|
+
* @returns Array of selected options from the flat list.
|
|
4428
5580
|
*/
|
|
4429
5581
|
getSelectedItems() {
|
|
4430
5582
|
return this.flatOptions.filter((item) => item.selected);
|
|
4431
5583
|
}
|
|
4432
5584
|
/**
|
|
4433
|
-
* Returns the first selected option
|
|
5585
|
+
* Returns the first selected option (if any).
|
|
4434
5586
|
*
|
|
4435
|
-
* @returns
|
|
5587
|
+
* @returns The first selected option; `undefined` if none.
|
|
4436
5588
|
*/
|
|
4437
5589
|
getSelectedItem() {
|
|
4438
5590
|
return this.flatOptions.find((item) => item.selected);
|
|
4439
5591
|
}
|
|
4440
5592
|
/**
|
|
4441
|
-
* Checks
|
|
5593
|
+
* Checks/unchecks all options when in multiple selection mode.
|
|
4442
5594
|
*
|
|
4443
|
-
* @param
|
|
5595
|
+
* @param isChecked - `true` to select all; `false` to deselect all.
|
|
4444
5596
|
*/
|
|
4445
5597
|
checkAll(isChecked) {
|
|
4446
5598
|
if (!this.isMultiple)
|
|
@@ -4452,21 +5604,21 @@ class MixedAdapter extends Adapter {
|
|
|
4452
5604
|
/**
|
|
4453
5605
|
* Subscribes a callback to visibility changes across options.
|
|
4454
5606
|
*
|
|
4455
|
-
* @param
|
|
4456
|
-
* - Function to invoke when visibility stats change.
|
|
5607
|
+
* @param callback - Invoked with aggregated visibility stats.
|
|
4457
5608
|
*/
|
|
4458
5609
|
onVisibilityChanged(callback) {
|
|
4459
5610
|
this.visibilityChangedCallbacks.push(callback);
|
|
4460
5611
|
}
|
|
4461
5612
|
/**
|
|
4462
|
-
*
|
|
4463
|
-
* Computes visible and total counts, then emits aggregated state.
|
|
5613
|
+
* Schedules a visibility statistics recomputation and notifies subscribers.
|
|
4464
5614
|
*/
|
|
4465
5615
|
notifyVisibilityChanged() {
|
|
4466
5616
|
Libs.callbackScheduler.run(`sche_vis_${this.adapterKey}`);
|
|
4467
5617
|
}
|
|
4468
5618
|
/**
|
|
4469
5619
|
* Computes and returns current visibility statistics for options.
|
|
5620
|
+
*
|
|
5621
|
+
* @returns Aggregated stats: `{ visibleCount, totalCount, hasVisible, isEmpty }`.
|
|
4470
5622
|
*/
|
|
4471
5623
|
getVisibilityStats() {
|
|
4472
5624
|
const visibleCount = this.flatOptions.filter((item) => item.visible).length;
|
|
@@ -4479,16 +5631,16 @@ class MixedAdapter extends Adapter {
|
|
|
4479
5631
|
};
|
|
4480
5632
|
}
|
|
4481
5633
|
/**
|
|
4482
|
-
* Resets the highlight to the first visible option
|
|
5634
|
+
* Resets the highlight to the first visible option.
|
|
4483
5635
|
*/
|
|
4484
5636
|
resetHighlight() {
|
|
4485
5637
|
this.setHighlight(0);
|
|
4486
5638
|
}
|
|
4487
5639
|
/**
|
|
4488
|
-
* Moves the highlight
|
|
5640
|
+
* Moves the highlight among visible options and optionally scrolls into view.
|
|
4489
5641
|
*
|
|
4490
|
-
* @param
|
|
4491
|
-
* @param
|
|
5642
|
+
* @param direction - +1 to move forward; -1 to move backward.
|
|
5643
|
+
* @param isScrollToView - Whether to scroll the highlighted item into view. Defaults to `true`.
|
|
4492
5644
|
*/
|
|
4493
5645
|
navigate(direction, isScrollToView = true) {
|
|
4494
5646
|
const visibleOptions = this.flatOptions.filter((opt) => opt.visible);
|
|
@@ -4507,8 +5659,8 @@ class MixedAdapter extends Adapter {
|
|
|
4507
5659
|
this.setHighlight(flatIndex, isScrollToView);
|
|
4508
5660
|
}
|
|
4509
5661
|
/**
|
|
4510
|
-
* Triggers a click on the currently highlighted
|
|
4511
|
-
* No-
|
|
5662
|
+
* Triggers a click on the currently highlighted, visible option.
|
|
5663
|
+
* No-ops if nothing is highlighted or the item is hidden.
|
|
4512
5664
|
*/
|
|
4513
5665
|
selectHighlighted() {
|
|
4514
5666
|
if (this.currentHighlightIndex > -1 && this.flatOptions[this.currentHighlightIndex]) {
|
|
@@ -4521,11 +5673,11 @@ class MixedAdapter extends Adapter {
|
|
|
4521
5673
|
}
|
|
4522
5674
|
}
|
|
4523
5675
|
/**
|
|
4524
|
-
* Highlights a target option by flat index or model
|
|
4525
|
-
*
|
|
5676
|
+
* Highlights a target option (by flat index or model reference),
|
|
5677
|
+
* skipping invisible items and optionally scrolling into view.
|
|
4526
5678
|
*
|
|
4527
|
-
* @param
|
|
4528
|
-
* @param
|
|
5679
|
+
* @param target - Flat index or OptionModel instance to highlight.
|
|
5680
|
+
* @param isScrollToView - Whether to scroll the highlighted item into view. Defaults to `true`.
|
|
4529
5681
|
*/
|
|
4530
5682
|
setHighlight(target, isScrollToView = true) {
|
|
4531
5683
|
let index = 0;
|
|
@@ -4554,6 +5706,7 @@ class MixedAdapter extends Adapter {
|
|
|
4554
5706
|
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
4555
5707
|
}
|
|
4556
5708
|
else {
|
|
5709
|
+
// If virtualized, ensure the item is rendered before trying to scroll.
|
|
4557
5710
|
this.recyclerView?.ensureRendered?.(i, { scrollIntoView: true });
|
|
4558
5711
|
}
|
|
4559
5712
|
}
|
|
@@ -4562,66 +5715,134 @@ class MixedAdapter extends Adapter {
|
|
|
4562
5715
|
}
|
|
4563
5716
|
}
|
|
4564
5717
|
/**
|
|
4565
|
-
* Hook invoked whenever the
|
|
5718
|
+
* Hook invoked whenever the highlighted item changes.
|
|
4566
5719
|
* Override to handle UI side effects (e.g., ARIA announcement, focus sync).
|
|
5720
|
+
*
|
|
5721
|
+
* @param index - Flat index of the newly highlighted item.
|
|
5722
|
+
* @param id - Optional DOM id of the highlighted view element.
|
|
4567
5723
|
*/
|
|
4568
5724
|
onHighlightChange(index, id) { }
|
|
4569
5725
|
/**
|
|
4570
|
-
* Hook invoked
|
|
4571
|
-
* Override to handle side effects
|
|
5726
|
+
* Hook invoked whenever a group's collapsed state changes.
|
|
5727
|
+
* Override to handle side effects (e.g., analytics, layout adjustments).
|
|
5728
|
+
*
|
|
5729
|
+
* @param model - The group whose collapsed state changed.
|
|
5730
|
+
* @param collapsed - New collapsed state.
|
|
4572
5731
|
*/
|
|
4573
5732
|
onCollapsedChange(model, collapsed) { }
|
|
4574
5733
|
}
|
|
4575
5734
|
|
|
4576
5735
|
/**
|
|
4577
|
-
* Fenwick tree (Binary Indexed Tree) for efficient prefix
|
|
4578
|
-
*
|
|
4579
|
-
*
|
|
5736
|
+
* Fenwick tree (Binary Indexed Tree) for efficient prefix-sum queries.
|
|
5737
|
+
*
|
|
5738
|
+
* - Internally uses **1-based indexing** for all BIT operations.
|
|
5739
|
+
* - Supports **O(log n)** updates and prefix/range queries.
|
|
5740
|
+
* - Useful for cumulative height calculations in virtualized lists.
|
|
5741
|
+
*
|
|
5742
|
+
* @extends Lifecycle
|
|
4580
5743
|
*/
|
|
4581
|
-
class Fenwick {
|
|
4582
|
-
|
|
5744
|
+
class Fenwick extends Lifecycle {
|
|
5745
|
+
/**
|
|
5746
|
+
* Creates a Fenwick tree and initializes it with the provided size (optional).
|
|
5747
|
+
*
|
|
5748
|
+
* @param stackNum - Initial number of elements (all values start at 0). Defaults to 0.
|
|
5749
|
+
*/
|
|
5750
|
+
constructor(stackNum = 0) {
|
|
5751
|
+
super();
|
|
5752
|
+
/** Internal BIT array. Index 0 is unused; valid range: [1..stackNum]. */
|
|
4583
5753
|
this.bit = [];
|
|
4584
|
-
|
|
4585
|
-
this.
|
|
5754
|
+
/** Number of elements managed by the tree (logical size). */
|
|
5755
|
+
this.stackNum = 0;
|
|
5756
|
+
this.initialize(stackNum);
|
|
5757
|
+
}
|
|
5758
|
+
/**
|
|
5759
|
+
* Initializes lifecycle and resets the tree to the given size.
|
|
5760
|
+
*
|
|
5761
|
+
* @param stackNum - Number of elements to allocate (values cleared to 0).
|
|
5762
|
+
*/
|
|
5763
|
+
initialize(stackNum) {
|
|
5764
|
+
this.init();
|
|
5765
|
+
this.reset(stackNum);
|
|
4586
5766
|
}
|
|
4587
|
-
/**
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
5767
|
+
/**
|
|
5768
|
+
* Resets the tree to a new size and clears all values to 0.
|
|
5769
|
+
*
|
|
5770
|
+
* @param stackNum - New number of elements (valid 1-based indexes: 1..stackNum).
|
|
5771
|
+
*/
|
|
5772
|
+
reset(stackNum) {
|
|
5773
|
+
this.stackNum = stackNum;
|
|
5774
|
+
this.bit = new Array(stackNum + 1).fill(0);
|
|
4591
5775
|
}
|
|
4592
|
-
/**
|
|
5776
|
+
/**
|
|
5777
|
+
* Adds `delta` to the element at **1-based** index `i`.
|
|
5778
|
+
*
|
|
5779
|
+
* Complexity: **O(log n)**
|
|
5780
|
+
*
|
|
5781
|
+
* @param i - 1-based index of the element to update (1..stackNum).
|
|
5782
|
+
* @param delta - Value to add (can be negative).
|
|
5783
|
+
*/
|
|
4593
5784
|
add(i, delta) {
|
|
4594
|
-
for (let x = i; x <= this.
|
|
5785
|
+
for (let x = i; x <= this.stackNum; x += x & -x)
|
|
4595
5786
|
this.bit[x] += delta;
|
|
4596
5787
|
}
|
|
4597
|
-
/**
|
|
5788
|
+
/**
|
|
5789
|
+
* Returns the prefix sum for the range **[1..i]** (inclusive).
|
|
5790
|
+
*
|
|
5791
|
+
* Complexity: **O(log n)**
|
|
5792
|
+
*
|
|
5793
|
+
* @param i - 1-based index up to which the sum is calculated.
|
|
5794
|
+
* @returns The cumulative sum from 1 to i.
|
|
5795
|
+
*/
|
|
4598
5796
|
sum(i) {
|
|
4599
5797
|
let s = 0;
|
|
4600
5798
|
for (let x = i; x > 0; x -= x & -x)
|
|
4601
5799
|
s += this.bit[x];
|
|
4602
5800
|
return s;
|
|
4603
5801
|
}
|
|
4604
|
-
/**
|
|
5802
|
+
/**
|
|
5803
|
+
* Returns the sum in the range **[l..r]** (1-based, inclusive).
|
|
5804
|
+
*
|
|
5805
|
+
* Complexity: **O(log n)**
|
|
5806
|
+
*
|
|
5807
|
+
* @param l - Left index (inclusive).
|
|
5808
|
+
* @param r - Right index (inclusive).
|
|
5809
|
+
* @returns The sum in [l..r], or 0 if r < l.
|
|
5810
|
+
*/
|
|
4605
5811
|
rangeSum(l, r) {
|
|
4606
5812
|
return r < l ? 0 : this.sum(r) - this.sum(l - 1);
|
|
4607
5813
|
}
|
|
4608
|
-
/**
|
|
5814
|
+
/**
|
|
5815
|
+
* Builds the tree from a **0-based** array in **O(n log n)**.
|
|
5816
|
+
*
|
|
5817
|
+
* Each element `arr[i]` is added to index `i + 1`.
|
|
5818
|
+
*
|
|
5819
|
+
* @param arr - Source values (0-based).
|
|
5820
|
+
*/
|
|
4609
5821
|
buildFrom(arr) {
|
|
4610
5822
|
this.reset(arr.length);
|
|
4611
5823
|
arr.forEach((val, i) => this.add(i + 1, val));
|
|
4612
5824
|
}
|
|
4613
5825
|
/**
|
|
4614
|
-
*
|
|
4615
|
-
*
|
|
5826
|
+
* Finds the largest index `idx` such that `prefixSum(idx) <= target`.
|
|
5827
|
+
*
|
|
5828
|
+
* This is a classic Fenwick-tree lower-bound over prefix sums.
|
|
5829
|
+
* It effectively returns the **count of items** that fit within the target
|
|
5830
|
+
* cumulative value (e.g., number of items whose total height <= target).
|
|
5831
|
+
*
|
|
5832
|
+
* Complexity: **O(log n)**
|
|
5833
|
+
*
|
|
5834
|
+
* @param target - Target prefix sum.
|
|
5835
|
+
* @returns The largest index satisfying the condition (in range 0..stackNum).
|
|
5836
|
+
* Returns 0 if the first element already exceeds `target`.
|
|
4616
5837
|
*/
|
|
4617
5838
|
lowerBoundPrefix(target) {
|
|
4618
5839
|
let idx = 0, bitMask = 1;
|
|
4619
|
-
while (bitMask << 1 <= this.
|
|
5840
|
+
while (bitMask << 1 <= this.stackNum)
|
|
4620
5841
|
bitMask <<= 1;
|
|
4621
5842
|
let cur = 0;
|
|
4622
5843
|
for (let step = bitMask; step !== 0; step >>= 1) {
|
|
4623
5844
|
const next = idx + step;
|
|
4624
|
-
if (next <= this.
|
|
5845
|
+
if (next <= this.stackNum && cur + this.bit[next] <= target) {
|
|
4625
5846
|
idx = next;
|
|
4626
5847
|
cur += this.bit[next];
|
|
4627
5848
|
}
|
|
@@ -4630,19 +5851,38 @@ class Fenwick {
|
|
|
4630
5851
|
}
|
|
4631
5852
|
}
|
|
4632
5853
|
/**
|
|
4633
|
-
* Virtual RecyclerView with efficient windowing and dynamic
|
|
5854
|
+
* Virtual RecyclerView with efficient windowing and dynamic-height support.
|
|
4634
5855
|
*
|
|
4635
|
-
* Only renders items visible in viewport plus overscan buffer, using
|
|
4636
|
-
* elements to simulate scroll height. Supports variable
|
|
4637
|
-
* adaptive estimation and maintains scroll position during
|
|
5856
|
+
* Only renders items visible in the viewport plus an overscan buffer, using
|
|
5857
|
+
* top/bottom padding elements to simulate full scroll height. Supports variable
|
|
5858
|
+
* item heights with **adaptive estimation** and maintains scroll position during
|
|
5859
|
+
* height changes using an **anchor item** technique.
|
|
4638
5860
|
*
|
|
4639
|
-
*
|
|
4640
|
-
*
|
|
5861
|
+
* Key features:
|
|
5862
|
+
* - Virtual windowing with configurable `overscan`
|
|
5863
|
+
* - Dynamic heights with `ResizeObserver`-based measurement
|
|
5864
|
+
* - Adaptive height estimation (average of measured items)
|
|
5865
|
+
* - Efficient prefix sums via Fenwick tree (1-based) for O(log n) math
|
|
5866
|
+
* - Stable scroll position during re-measure via anchor correction
|
|
5867
|
+
*
|
|
5868
|
+
* @template TItem - Model type for list items.
|
|
5869
|
+
* @template TAdapter - Adapter managing item views.
|
|
5870
|
+
*
|
|
5871
|
+
* @extends RecyclerView
|
|
4641
5872
|
*/
|
|
4642
5873
|
class VirtualRecyclerView extends RecyclerView {
|
|
4643
|
-
/** Creates virtual recycler view with optional root element. */
|
|
5874
|
+
/** Creates a virtual recycler view with an optional root element. */
|
|
4644
5875
|
constructor(viewElement = null) {
|
|
4645
5876
|
super(viewElement);
|
|
5877
|
+
/**
|
|
5878
|
+
* Virtualization settings.
|
|
5879
|
+
*
|
|
5880
|
+
* - `scrollEl` : External scroll container (if omitted, inferred from DOM)
|
|
5881
|
+
* - `estimateItemHeight` : Initial/fallback item height in pixels
|
|
5882
|
+
* - `overscan` : Extra viewport height (in item multiples) rendered above/below
|
|
5883
|
+
* - `dynamicHeights` : Enable measuring items with ResizeObserver
|
|
5884
|
+
* - `adaptiveEstimate` : Use average of measured items as the running estimate
|
|
5885
|
+
*/
|
|
4646
5886
|
this.opts = {
|
|
4647
5887
|
scrollEl: undefined,
|
|
4648
5888
|
estimateItemHeight: 36,
|
|
@@ -4650,31 +5890,54 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4650
5890
|
dynamicHeights: true,
|
|
4651
5891
|
adaptiveEstimate: true,
|
|
4652
5892
|
};
|
|
5893
|
+
/** Cache of measured heights per item index (undefined when not measured). */
|
|
4653
5894
|
this.heightCache = [];
|
|
5895
|
+
/** Fenwick tree storing current height values (0 for invisible items). */
|
|
4654
5896
|
this.fenwick = new Fenwick(0);
|
|
5897
|
+
/** Map of currently created (mounted) DOM elements keyed by item index. */
|
|
4655
5898
|
this.created = new Map();
|
|
5899
|
+
/** Whether an initial height probe has been performed. */
|
|
4656
5900
|
this.firstMeasured = false;
|
|
5901
|
+
/** Current window bounds (inclusive) in flat item-space. */
|
|
4657
5902
|
this.start = 0;
|
|
5903
|
+
/** Current window end (inclusive). -1 means not initialized. */
|
|
4658
5904
|
this.end = -1;
|
|
5905
|
+
/** Pending animation frame ids for window and measurement. */
|
|
4659
5906
|
this.rafId = null;
|
|
4660
5907
|
this.measureRaf = null;
|
|
5908
|
+
/** Re-entrancy/suspension flags. */
|
|
4661
5909
|
this.updating = false;
|
|
4662
5910
|
this.suppressResize = false;
|
|
4663
5911
|
this.lastRenderCount = 0;
|
|
4664
5912
|
this.suspended = false;
|
|
4665
5913
|
this.resumeResizeAfter = false;
|
|
5914
|
+
/** Small cache for sticky header height (16ms TTL). */
|
|
4666
5915
|
this.stickyCacheTick = 0;
|
|
4667
5916
|
this.stickyCacheVal = 0;
|
|
5917
|
+
/** Stats for adaptive estimator. */
|
|
4668
5918
|
this.measuredSum = 0;
|
|
4669
5919
|
this.measuredCount = 0;
|
|
4670
5920
|
}
|
|
4671
|
-
/**
|
|
5921
|
+
/**
|
|
5922
|
+
* Updates virtualization settings (overscan, estimates, dynamic heights, etc.).
|
|
5923
|
+
*
|
|
5924
|
+
* @param opts - Partial configuration to merge with current options.
|
|
5925
|
+
*/
|
|
4672
5926
|
configure(opts) {
|
|
4673
5927
|
this.opts = { ...this.opts, ...opts };
|
|
4674
5928
|
}
|
|
4675
5929
|
/**
|
|
4676
|
-
* Binds adapter and initializes virtualization scaffold.
|
|
4677
|
-
*
|
|
5930
|
+
* Binds an adapter and initializes the virtualization scaffold.
|
|
5931
|
+
*
|
|
5932
|
+
* Flow:
|
|
5933
|
+
* 1) Dispose previous adapter/listeners if any
|
|
5934
|
+
* 2) Call `super.setAdapter(adapter)` to wire lifecycle
|
|
5935
|
+
* 3) Build the pad elements (top, host, bottom)
|
|
5936
|
+
* 4) Resolve `scrollEl` (from config or DOM)
|
|
5937
|
+
* 5) Attach scroll listener, refresh and attach resize observer
|
|
5938
|
+
* 6) Subscribe to adapter visibility changes to force a refresh
|
|
5939
|
+
*
|
|
5940
|
+
* @param adapter - The adapter managing models and item views.
|
|
4678
5941
|
*/
|
|
4679
5942
|
setAdapter(adapter) {
|
|
4680
5943
|
if (this.adapter)
|
|
@@ -4720,7 +5983,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4720
5983
|
}
|
|
4721
5984
|
/**
|
|
4722
5985
|
* Resumes scroll/resize processing after suspension.
|
|
4723
|
-
* Re-attaches listeners and schedules window update.
|
|
5986
|
+
* Re-attaches listeners and schedules a window update.
|
|
4724
5987
|
*/
|
|
4725
5988
|
resume() {
|
|
4726
5989
|
this.suspended = false;
|
|
@@ -4734,10 +5997,10 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4734
5997
|
this.scheduleUpdateWindow();
|
|
4735
5998
|
}
|
|
4736
5999
|
/**
|
|
4737
|
-
* Rebuilds internal state and schedules render update.
|
|
4738
|
-
* Probes initial item height on first run
|
|
6000
|
+
* Rebuilds internal state and schedules a render update.
|
|
6001
|
+
* Probes initial item height on first run and rebuilds the Fenwick tree.
|
|
4739
6002
|
*
|
|
4740
|
-
* @param isUpdate - True if called from data update
|
|
6003
|
+
* @param isUpdate - True if called from a data update; false on initial setup.
|
|
4741
6004
|
*/
|
|
4742
6005
|
refresh(isUpdate) {
|
|
4743
6006
|
if (!this.adapter || !this.viewElement)
|
|
@@ -4748,6 +6011,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4748
6011
|
this.lastRenderCount = count;
|
|
4749
6012
|
if (count === 0) {
|
|
4750
6013
|
this.resetState();
|
|
6014
|
+
this.update();
|
|
4751
6015
|
return;
|
|
4752
6016
|
}
|
|
4753
6017
|
this.heightCache.length = count;
|
|
@@ -4757,10 +6021,13 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4757
6021
|
}
|
|
4758
6022
|
this.rebuildFenwick(count);
|
|
4759
6023
|
this.scheduleUpdateWindow();
|
|
6024
|
+
this.update();
|
|
4760
6025
|
}
|
|
4761
6026
|
/**
|
|
4762
|
-
* Ensures item at index is rendered and optionally scrolls into view.
|
|
4763
|
-
*
|
|
6027
|
+
* Ensures the item at `index` is rendered and optionally scrolls it into view.
|
|
6028
|
+
*
|
|
6029
|
+
* @param index - Item index to ensure visible/mounted.
|
|
6030
|
+
* @param opt - Optional behavior: `{ scrollIntoView?: boolean }`.
|
|
4764
6031
|
*/
|
|
4765
6032
|
ensureRendered(index, opt) {
|
|
4766
6033
|
this.mountRange(index, index);
|
|
@@ -4768,8 +6035,10 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4768
6035
|
this.scrollToIndex(index);
|
|
4769
6036
|
}
|
|
4770
6037
|
/**
|
|
4771
|
-
* Scrolls container to make item at index visible.
|
|
6038
|
+
* Scrolls the container to make the item at `index` visible.
|
|
4772
6039
|
* Calculates target scroll position accounting for container offset.
|
|
6040
|
+
*
|
|
6041
|
+
* @param index - Item index to bring into view.
|
|
4773
6042
|
*/
|
|
4774
6043
|
scrollToIndex(index) {
|
|
4775
6044
|
const count = this.adapter?.itemCount?.() ?? 0;
|
|
@@ -4782,8 +6051,8 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4782
6051
|
this.scrollEl.scrollTop = Math.min(Math.max(0, target), maxScroll);
|
|
4783
6052
|
}
|
|
4784
6053
|
/**
|
|
4785
|
-
* Cleans up all resources: listeners, observers, DOM elements.
|
|
4786
|
-
* Call before removing component to prevent memory leaks.
|
|
6054
|
+
* Cleans up all resources: listeners, observers, and DOM elements.
|
|
6055
|
+
* Call before removing the component to prevent memory leaks.
|
|
4787
6056
|
*/
|
|
4788
6057
|
dispose() {
|
|
4789
6058
|
this.cancelFrames();
|
|
@@ -4795,9 +6064,31 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4795
6064
|
this.created.clear();
|
|
4796
6065
|
}
|
|
4797
6066
|
/**
|
|
4798
|
-
*
|
|
4799
|
-
*
|
|
4800
|
-
*
|
|
6067
|
+
* Destroys the virtual recycler view and releases resources.
|
|
6068
|
+
*
|
|
6069
|
+
* - Resets internal state and disposes observers/listeners
|
|
6070
|
+
* - Removes scaffold elements (PadTop, ItemsHost, PadBottom)
|
|
6071
|
+
* - Ends the lifecycle
|
|
6072
|
+
*/
|
|
6073
|
+
destroy() {
|
|
6074
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
6077
|
+
this.resetState();
|
|
6078
|
+
this.dispose();
|
|
6079
|
+
this.PadTop.remove();
|
|
6080
|
+
this.ItemsHost.remove();
|
|
6081
|
+
this.PadBottom.remove();
|
|
6082
|
+
this.PadTop = null;
|
|
6083
|
+
this.ItemsHost = null;
|
|
6084
|
+
this.PadBottom = null;
|
|
6085
|
+
super.destroy();
|
|
6086
|
+
}
|
|
6087
|
+
/**
|
|
6088
|
+
* Hard reset after large visibility changes (e.g., search/filter cleared).
|
|
6089
|
+
*
|
|
6090
|
+
* Rebuilds all height structures and remounts the visible window.
|
|
6091
|
+
* Essential for fixing padding calculations after bulk visibility updates.
|
|
4801
6092
|
*/
|
|
4802
6093
|
refreshItem() {
|
|
4803
6094
|
if (!this.adapter)
|
|
@@ -4825,7 +6116,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4825
6116
|
this.measureRaf = null;
|
|
4826
6117
|
}
|
|
4827
6118
|
}
|
|
4828
|
-
/** Resets all internal state: DOM, caches, measurements. */
|
|
6119
|
+
/** Resets all internal state: DOM, caches, and measurements. */
|
|
4829
6120
|
resetState() {
|
|
4830
6121
|
this.created.forEach(el => el.remove());
|
|
4831
6122
|
this.created.clear();
|
|
@@ -4838,8 +6129,8 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4838
6129
|
this.measuredCount = 0;
|
|
4839
6130
|
}
|
|
4840
6131
|
/**
|
|
4841
|
-
* Measures first item to set initial height estimate.
|
|
4842
|
-
*
|
|
6132
|
+
* Measures the first item to set an initial height estimate.
|
|
6133
|
+
* If dynamic heights are disabled, removes the probe element afterward.
|
|
4843
6134
|
*/
|
|
4844
6135
|
probeInitialHeight() {
|
|
4845
6136
|
const probe = 0;
|
|
@@ -4861,16 +6152,16 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4861
6152
|
}
|
|
4862
6153
|
}
|
|
4863
6154
|
/**
|
|
4864
|
-
*
|
|
4865
|
-
* Defaults to visible
|
|
6155
|
+
* Whether item at `index` is visible (not filtered/hidden).
|
|
6156
|
+
* Defaults to visible when the property is undefined.
|
|
4866
6157
|
*/
|
|
4867
6158
|
isIndexVisible(index) {
|
|
4868
6159
|
const item = this.adapter?.items?.[index];
|
|
4869
6160
|
return item?.visible ?? true;
|
|
4870
6161
|
}
|
|
4871
6162
|
/**
|
|
4872
|
-
* Finds next visible item index starting from
|
|
4873
|
-
* Returns -1 if no visible items found.
|
|
6163
|
+
* Finds the next visible item index starting from `index`.
|
|
6164
|
+
* Returns -1 if no visible items are found.
|
|
4874
6165
|
*/
|
|
4875
6166
|
nextVisibleFrom(index, count) {
|
|
4876
6167
|
for (let i = Math.max(0, index); i < count; i++) {
|
|
@@ -4881,7 +6172,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4881
6172
|
}
|
|
4882
6173
|
/**
|
|
4883
6174
|
* Recalculates total measured height and count from cache.
|
|
4884
|
-
* Only counts visible items for adaptive estimation.
|
|
6175
|
+
* Only counts **visible** items for adaptive estimation.
|
|
4885
6176
|
*/
|
|
4886
6177
|
recomputeMeasuredStats(count) {
|
|
4887
6178
|
this.measuredSum = 0;
|
|
@@ -4896,15 +6187,15 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4896
6187
|
}
|
|
4897
6188
|
}
|
|
4898
6189
|
}
|
|
4899
|
-
/** Returns view container's top offset relative to scroll container. */
|
|
6190
|
+
/** Returns view container's top offset relative to the scroll container. */
|
|
4900
6191
|
containerTopInScroll() {
|
|
4901
6192
|
const a = this.viewElement.getBoundingClientRect();
|
|
4902
6193
|
const b = this.scrollEl.getBoundingClientRect();
|
|
4903
6194
|
return Math.max(0, a.top - b.top + this.scrollEl.scrollTop);
|
|
4904
6195
|
}
|
|
4905
6196
|
/**
|
|
4906
|
-
* Returns sticky header height with 16ms cache to avoid
|
|
4907
|
-
* Used to adjust viewport
|
|
6197
|
+
* Returns sticky header height with ~16ms cache to avoid layout thrashing.
|
|
6198
|
+
* Used to adjust effective viewport height.
|
|
4908
6199
|
*/
|
|
4909
6200
|
stickyTopHeight() {
|
|
4910
6201
|
const now = performance.now();
|
|
@@ -4915,7 +6206,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4915
6206
|
this.stickyCacheTick = now;
|
|
4916
6207
|
return this.stickyCacheVal;
|
|
4917
6208
|
}
|
|
4918
|
-
/** Schedules window update on next frame if not already scheduled. */
|
|
6209
|
+
/** Schedules a window update on the next frame if not already scheduled. */
|
|
4919
6210
|
scheduleUpdateWindow() {
|
|
4920
6211
|
if (this.rafId != null || this.suspended)
|
|
4921
6212
|
return;
|
|
@@ -4925,8 +6216,10 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4925
6216
|
});
|
|
4926
6217
|
}
|
|
4927
6218
|
/**
|
|
4928
|
-
* Measures element's total height including margins.
|
|
4929
|
-
*
|
|
6219
|
+
* Measures an element's total height including vertical margins.
|
|
6220
|
+
*
|
|
6221
|
+
* @param el - Element to measure.
|
|
6222
|
+
* @returns Total outer height in pixels.
|
|
4930
6223
|
*/
|
|
4931
6224
|
measureOuterHeight(el) {
|
|
4932
6225
|
const rect = el.getBoundingClientRect();
|
|
@@ -4937,7 +6230,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4937
6230
|
}
|
|
4938
6231
|
/**
|
|
4939
6232
|
* Returns height estimate for unmeasured items.
|
|
4940
|
-
* Uses adaptive average if enabled, otherwise fixed estimate.
|
|
6233
|
+
* Uses adaptive average if enabled, otherwise the fixed estimate.
|
|
4941
6234
|
*/
|
|
4942
6235
|
getEstimate() {
|
|
4943
6236
|
if (this.opts.adaptiveEstimate && this.measuredCount > 0) {
|
|
@@ -4946,8 +6239,10 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4946
6239
|
return this.opts.estimateItemHeight;
|
|
4947
6240
|
}
|
|
4948
6241
|
/**
|
|
4949
|
-
* Rebuilds Fenwick tree with current heights and estimates.
|
|
4950
|
-
* Invisible items
|
|
6242
|
+
* Rebuilds the Fenwick tree with current heights and estimates.
|
|
6243
|
+
* Invisible items receive height 0; others use cached or estimated height.
|
|
6244
|
+
*
|
|
6245
|
+
* @param count - Total number of items.
|
|
4951
6246
|
*/
|
|
4952
6247
|
rebuildFenwick(count) {
|
|
4953
6248
|
const est = this.getEstimate();
|
|
@@ -4955,10 +6250,12 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4955
6250
|
this.fenwick.buildFrom(arr);
|
|
4956
6251
|
}
|
|
4957
6252
|
/**
|
|
4958
|
-
* Updates cached height at index and applies delta to Fenwick tree.
|
|
4959
|
-
*
|
|
6253
|
+
* Updates cached height at `index` and applies delta to the Fenwick tree.
|
|
6254
|
+
* Also updates running average for the adaptive estimator.
|
|
4960
6255
|
*
|
|
4961
|
-
* @
|
|
6256
|
+
* @param index - Item index to update.
|
|
6257
|
+
* @param newH - Newly measured outer height (px).
|
|
6258
|
+
* @returns True if the height changed beyond the epsilon threshold.
|
|
4962
6259
|
*/
|
|
4963
6260
|
updateHeightAt(index, newH) {
|
|
4964
6261
|
if (!this.isIndexVisible(index))
|
|
@@ -4980,8 +6277,11 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4980
6277
|
return true;
|
|
4981
6278
|
}
|
|
4982
6279
|
/**
|
|
4983
|
-
* Finds first visible item at or after scroll offset.
|
|
4984
|
-
* Uses Fenwick
|
|
6280
|
+
* Finds the first visible item at or after a scroll-relative offset.
|
|
6281
|
+
* Uses Fenwick lower-bound then adjusts forward to the next visible item.
|
|
6282
|
+
*
|
|
6283
|
+
* @param stRel - ScrollTop relative to the view container (px).
|
|
6284
|
+
* @param count - Total item count.
|
|
4985
6285
|
*/
|
|
4986
6286
|
findFirstVisibleIndex(stRel, count) {
|
|
4987
6287
|
const k = this.fenwick.lowerBoundPrefix(Math.max(0, stRel));
|
|
@@ -4990,8 +6290,11 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
4990
6290
|
return v === -1 ? Math.max(0, raw) : v;
|
|
4991
6291
|
}
|
|
4992
6292
|
/**
|
|
4993
|
-
* Inserts element into
|
|
4994
|
-
* Tries adjacent siblings first, then scans for insertion point.
|
|
6293
|
+
* Inserts an element into the host maintaining increasing index order.
|
|
6294
|
+
* Tries adjacent siblings first, then scans for the insertion point.
|
|
6295
|
+
*
|
|
6296
|
+
* @param index - Item index.
|
|
6297
|
+
* @param el - Element to insert.
|
|
4995
6298
|
*/
|
|
4996
6299
|
insertIntoHostByIndex(index, el) {
|
|
4997
6300
|
el.setAttribute(VirtualRecyclerView.ATTR_INDEX, String(index));
|
|
@@ -5016,8 +6319,11 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5016
6319
|
this.ItemsHost.appendChild(el);
|
|
5017
6320
|
}
|
|
5018
6321
|
/**
|
|
5019
|
-
* Ensures element is in correct DOM position for its index.
|
|
5020
|
-
* Reinserts
|
|
6322
|
+
* Ensures the element is in the correct DOM position for its index.
|
|
6323
|
+
* Reinserts when adjacent siblings indicate an out-of-order position.
|
|
6324
|
+
*
|
|
6325
|
+
* @param index - Item index.
|
|
6326
|
+
* @param el - Element to validate/reinsert.
|
|
5021
6327
|
*/
|
|
5022
6328
|
ensureDomOrder(index, el) {
|
|
5023
6329
|
if (el.parentElement !== this.ItemsHost) {
|
|
@@ -5035,8 +6341,8 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5035
6341
|
}
|
|
5036
6342
|
}
|
|
5037
6343
|
/**
|
|
5038
|
-
* Attaches ResizeObserver to measure items when they resize.
|
|
5039
|
-
* Singleton pattern
|
|
6344
|
+
* Attaches a ResizeObserver to measure items when they resize.
|
|
6345
|
+
* Singleton pattern — only creates once per instance.
|
|
5040
6346
|
*/
|
|
5041
6347
|
attachResizeObserverOnce() {
|
|
5042
6348
|
if (this.resizeObs)
|
|
@@ -5052,8 +6358,8 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5052
6358
|
this.resizeObs.observe(this.ItemsHost);
|
|
5053
6359
|
}
|
|
5054
6360
|
/**
|
|
5055
|
-
* Measures all currently rendered items and updates height cache.
|
|
5056
|
-
* Triggers window update
|
|
6361
|
+
* Measures all currently rendered items and updates the height cache.
|
|
6362
|
+
* Triggers a window update when any heights changed.
|
|
5057
6363
|
*/
|
|
5058
6364
|
measureVisibleAndUpdate() {
|
|
5059
6365
|
if (!this.adapter)
|
|
@@ -5079,20 +6385,21 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5079
6385
|
this.scheduleUpdateWindow();
|
|
5080
6386
|
}
|
|
5081
6387
|
}
|
|
5082
|
-
/** Scroll event handler
|
|
6388
|
+
/** Scroll event handler — schedules a render update. */
|
|
5083
6389
|
onScroll() {
|
|
5084
6390
|
this.scheduleUpdateWindow();
|
|
5085
6391
|
}
|
|
5086
6392
|
/**
|
|
5087
|
-
* Core rendering logic
|
|
6393
|
+
* Core rendering logic — calculates and updates the visible window.
|
|
5088
6394
|
*
|
|
5089
|
-
*
|
|
5090
|
-
*
|
|
5091
|
-
*
|
|
5092
|
-
*
|
|
5093
|
-
*
|
|
5094
|
-
*
|
|
5095
|
-
*
|
|
6395
|
+
* Steps:
|
|
6396
|
+
* 1) Calculate viewport bounds (account for sticky headers)
|
|
6397
|
+
* 2) Use an anchor item to prevent scroll jumping on height changes
|
|
6398
|
+
* 3) Determine start/end indices with overscan buffer
|
|
6399
|
+
* 4) Mount/unmount items as needed
|
|
6400
|
+
* 5) Measure visible items (if dynamic heights enabled)
|
|
6401
|
+
* 6) Update pad heights (top/bottom) to maintain total scroll span
|
|
6402
|
+
* 7) Adjust scroll position to keep the anchor item stable
|
|
5096
6403
|
*/
|
|
5097
6404
|
updateWindowInternal() {
|
|
5098
6405
|
if (this.updating || this.suspended)
|
|
@@ -5104,6 +6411,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5104
6411
|
const count = this.adapter.itemCount();
|
|
5105
6412
|
if (count <= 0)
|
|
5106
6413
|
return;
|
|
6414
|
+
// Handle item count changes (e.g., add/remove)
|
|
5107
6415
|
if (this.lastRenderCount !== count) {
|
|
5108
6416
|
this.lastRenderCount = count;
|
|
5109
6417
|
this.heightCache.length = count;
|
|
@@ -5145,6 +6453,7 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5145
6453
|
finally {
|
|
5146
6454
|
this.suppressResize = false;
|
|
5147
6455
|
}
|
|
6456
|
+
// Keep anchor item stable to prevent scroll jump
|
|
5148
6457
|
const anchorTopNew = this.offsetTopOf(anchorIndex);
|
|
5149
6458
|
const targetScroll = this.containerTopInScroll() + anchorTopNew - anchorDelta;
|
|
5150
6459
|
const maxScroll = Math.max(0, this.scrollEl.scrollHeight - this.scrollEl.clientHeight);
|
|
@@ -5159,14 +6468,16 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5159
6468
|
this.updating = false;
|
|
5160
6469
|
}
|
|
5161
6470
|
}
|
|
5162
|
-
/** Mounts all items in inclusive range [start..end]
|
|
6471
|
+
/** Mounts all items in the inclusive range `[start..end]`. */
|
|
5163
6472
|
mountRange(start, end) {
|
|
5164
6473
|
for (let i = start; i <= end; i++)
|
|
5165
6474
|
this.mountIndexOnce(i);
|
|
5166
6475
|
}
|
|
5167
6476
|
/**
|
|
5168
|
-
* Mounts single item, reusing existing element if available.
|
|
5169
|
-
* Creates view holder on first mount, rebinds on subsequent renders.
|
|
6477
|
+
* Mounts a single item, reusing an existing element if available.
|
|
6478
|
+
* Creates the view holder on first mount, or rebinds on subsequent renders.
|
|
6479
|
+
*
|
|
6480
|
+
* @param index - Item index to mount/rebind.
|
|
5170
6481
|
*/
|
|
5171
6482
|
mountIndexOnce(index) {
|
|
5172
6483
|
if (!this.isIndexVisible(index)) {
|
|
@@ -5206,7 +6517,9 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5206
6517
|
this.created.set(index, el);
|
|
5207
6518
|
}
|
|
5208
6519
|
}
|
|
5209
|
-
/**
|
|
6520
|
+
/**
|
|
6521
|
+
* Removes all mounted items outside the inclusive range `[start..end]`.
|
|
6522
|
+
*/
|
|
5210
6523
|
unmountOutside(start, end) {
|
|
5211
6524
|
this.created.forEach((el, idx) => {
|
|
5212
6525
|
if (idx < start || idx > end) {
|
|
@@ -5216,7 +6529,9 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5216
6529
|
}
|
|
5217
6530
|
});
|
|
5218
6531
|
}
|
|
5219
|
-
/**
|
|
6532
|
+
/**
|
|
6533
|
+
* Removes all items currently marked as invisible from the DOM.
|
|
6534
|
+
*/
|
|
5220
6535
|
cleanupInvisibleItems() {
|
|
5221
6536
|
this.created.forEach((el, idx) => {
|
|
5222
6537
|
if (!this.isIndexVisible(idx)) {
|
|
@@ -5226,26 +6541,44 @@ class VirtualRecyclerView extends RecyclerView {
|
|
|
5226
6541
|
}
|
|
5227
6542
|
});
|
|
5228
6543
|
}
|
|
5229
|
-
/**
|
|
6544
|
+
/**
|
|
6545
|
+
* Returns cumulative height from the start to the **top** of item at `index`.
|
|
6546
|
+
*
|
|
6547
|
+
* Internally uses Fenwick sum on the **inclusive** range [1..index],
|
|
6548
|
+
* which corresponds to offset-top in 0-based item space.
|
|
6549
|
+
*
|
|
6550
|
+
* @param index - Item index (0-based).
|
|
6551
|
+
*/
|
|
5230
6552
|
offsetTopOf(index) {
|
|
5231
6553
|
return this.fenwick.sum(index);
|
|
5232
6554
|
}
|
|
5233
|
-
/**
|
|
6555
|
+
/**
|
|
6556
|
+
* Returns the total height of items in inclusive range `[start..end]`.
|
|
6557
|
+
*
|
|
6558
|
+
* @param start - Start index (0-based).
|
|
6559
|
+
* @param end - End index (0-based).
|
|
6560
|
+
*/
|
|
5234
6561
|
windowHeight(start, end) {
|
|
5235
6562
|
return this.fenwick.rangeSum(start + 1, end + 1);
|
|
5236
6563
|
}
|
|
5237
|
-
/**
|
|
6564
|
+
/**
|
|
6565
|
+
* Returns the total scrollable height for all items.
|
|
6566
|
+
*
|
|
6567
|
+
* @param count - Total item count.
|
|
6568
|
+
*/
|
|
5238
6569
|
totalHeight(count) {
|
|
5239
6570
|
return this.fenwick.sum(count);
|
|
5240
6571
|
}
|
|
5241
6572
|
}
|
|
6573
|
+
/** Epsilon threshold for height-change significance. */
|
|
5242
6574
|
VirtualRecyclerView.EPS = 0.5;
|
|
6575
|
+
/** Attribute stored on each element indicating its item index. */
|
|
5243
6576
|
VirtualRecyclerView.ATTR_INDEX = "data-vindex";
|
|
5244
6577
|
|
|
5245
6578
|
/**
|
|
5246
6579
|
* @class
|
|
5247
6580
|
*/
|
|
5248
|
-
class SelectBox {
|
|
6581
|
+
class SelectBox extends Lifecycle {
|
|
5249
6582
|
/**
|
|
5250
6583
|
* Initializes a SelectBox instance and, if a source <select> and Selective context are provided,
|
|
5251
6584
|
* immediately calls init() to set up the enhanced UI and behavior.
|
|
@@ -5254,6 +6587,7 @@ class SelectBox {
|
|
|
5254
6587
|
* @param {any|null} [Selective=null] - The Selective framework/context used for configuration and services.
|
|
5255
6588
|
*/
|
|
5256
6589
|
constructor(select = null, Selective = null) {
|
|
6590
|
+
super();
|
|
5257
6591
|
this.container = {};
|
|
5258
6592
|
this.oldValue = null;
|
|
5259
6593
|
this.node = null;
|
|
@@ -5264,8 +6598,8 @@ class SelectBox {
|
|
|
5264
6598
|
this.isBeforeSearch = false;
|
|
5265
6599
|
/** Selective context (global helper) */
|
|
5266
6600
|
this.Selective = null;
|
|
5267
|
-
if (select)
|
|
5268
|
-
this.
|
|
6601
|
+
if (select && Selective)
|
|
6602
|
+
this.initialize(select, Selective);
|
|
5269
6603
|
}
|
|
5270
6604
|
/**
|
|
5271
6605
|
* Gets or sets the disabled state of the SelectBox.
|
|
@@ -5309,14 +6643,24 @@ class SelectBox {
|
|
|
5309
6643
|
this.node.classList.toggle("invisible", !value);
|
|
5310
6644
|
}
|
|
5311
6645
|
/**
|
|
5312
|
-
*
|
|
5313
|
-
*
|
|
5314
|
-
* @param {HTMLSelectElement} select - The native <select> element to enhance.
|
|
5315
|
-
* @param {any} Selective - The Selective framework/context for services and configuration.
|
|
6646
|
+
* Wrapper method to initialize with select element
|
|
5316
6647
|
*/
|
|
5317
|
-
|
|
6648
|
+
initialize(select, Selective) {
|
|
5318
6649
|
const bindedMap = Libs.getBinderMap(select);
|
|
5319
|
-
|
|
6650
|
+
this.options = bindedMap.options;
|
|
6651
|
+
this.Selective = Selective;
|
|
6652
|
+
this.init(select);
|
|
6653
|
+
}
|
|
6654
|
+
/**
|
|
6655
|
+
* Override lifecycle init - Creates all components and DOM structure
|
|
6656
|
+
*/
|
|
6657
|
+
init(select) {
|
|
6658
|
+
if (this.state !== LifecycleState.NEW)
|
|
6659
|
+
return;
|
|
6660
|
+
if (!select || !this.options)
|
|
6661
|
+
return;
|
|
6662
|
+
const options = this.options;
|
|
6663
|
+
// Create all components
|
|
5320
6664
|
const placeholder = new PlaceHolder(options);
|
|
5321
6665
|
const directive = new Directive();
|
|
5322
6666
|
const searchbox = new SearchBox(options);
|
|
@@ -5326,11 +6670,9 @@ class SelectBox {
|
|
|
5326
6670
|
const searchController = new SearchController(select, optionModelManager, this);
|
|
5327
6671
|
const selectObserver = new SelectObserver(select);
|
|
5328
6672
|
const datasetObserver = new DatasetObserver(select);
|
|
5329
|
-
this.Selective = Selective;
|
|
5330
|
-
this.options = options;
|
|
5331
6673
|
// ensure placeholder has id for aria-labelledby usage
|
|
5332
6674
|
if (placeholder.node)
|
|
5333
|
-
placeholder.node.id = String(
|
|
6675
|
+
placeholder.node.id = String(options.SEID_HOLDER ?? "");
|
|
5334
6676
|
const container = Libs.mountNode({
|
|
5335
6677
|
Container: {
|
|
5336
6678
|
tag: { node: "div", classList: "selective-ui-MAIN" },
|
|
@@ -5358,17 +6700,16 @@ class SelectBox {
|
|
|
5358
6700
|
}, null);
|
|
5359
6701
|
this.container = container;
|
|
5360
6702
|
this.node = container.view;
|
|
5361
|
-
//
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
container.
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
select.classList.add("init");
|
|
6703
|
+
// Store references on container
|
|
6704
|
+
container.searchController = searchController;
|
|
6705
|
+
container.placeholder = placeholder;
|
|
6706
|
+
container.directive = directive;
|
|
6707
|
+
container.searchbox = searchbox;
|
|
6708
|
+
container.effector = effector;
|
|
6709
|
+
container.targetElement = select;
|
|
6710
|
+
container.accessorybox = accessoryBox;
|
|
6711
|
+
container.selectObserver = selectObserver;
|
|
6712
|
+
container.datasetObserver = datasetObserver;
|
|
5372
6713
|
// ModelManager setup
|
|
5373
6714
|
optionModelManager.setupAdapter(MixedAdapter);
|
|
5374
6715
|
if (options.virtualScroll) {
|
|
@@ -5382,16 +6723,6 @@ class SelectBox {
|
|
|
5382
6723
|
container.popup?.triggerResize?.();
|
|
5383
6724
|
};
|
|
5384
6725
|
this.optionModelManager = optionModelManager;
|
|
5385
|
-
// Store references on container
|
|
5386
|
-
container.searchController = searchController;
|
|
5387
|
-
container.placeholder = placeholder;
|
|
5388
|
-
container.directive = directive;
|
|
5389
|
-
container.searchbox = searchbox;
|
|
5390
|
-
container.effector = effector;
|
|
5391
|
-
container.targetElement = select;
|
|
5392
|
-
container.accessorybox = accessoryBox;
|
|
5393
|
-
container.selectObserver = selectObserver;
|
|
5394
|
-
container.datasetObserver = datasetObserver;
|
|
5395
6726
|
// Popup
|
|
5396
6727
|
container.popup = new Popup(select, options, optionModelManager);
|
|
5397
6728
|
container.popup.setupEffector(effector);
|
|
@@ -5405,30 +6736,54 @@ class SelectBox {
|
|
|
5405
6736
|
container.popup.onAdapterPropChanging("select", () => {
|
|
5406
6737
|
this.oldValue = this.getAction()?.value ?? "";
|
|
5407
6738
|
});
|
|
6739
|
+
accessoryBox.setRoot(container.tags.ViewPanel);
|
|
6740
|
+
accessoryBox.setModelManager(optionModelManager);
|
|
6741
|
+
this.setupEventHandlers(select, container, options, searchController, searchbox);
|
|
6742
|
+
this.setupObservers(selectObserver, datasetObserver, select, optionModelManager);
|
|
6743
|
+
// Initial states
|
|
6744
|
+
this.isDisabled = Libs.string2Boolean(options.disabled);
|
|
6745
|
+
this.isReadOnly = Libs.string2Boolean(options.readonly);
|
|
6746
|
+
// Call parent lifecycle init
|
|
6747
|
+
super.init();
|
|
6748
|
+
}
|
|
6749
|
+
/**
|
|
6750
|
+
* Override lifecycle mount - Mounts component into DOM
|
|
6751
|
+
*/
|
|
6752
|
+
mount() {
|
|
6753
|
+
if (this.state !== LifecycleState.INITIALIZED)
|
|
6754
|
+
return;
|
|
6755
|
+
if (!this.node || !this.container.targetElement)
|
|
6756
|
+
return;
|
|
6757
|
+
const select = this.container.targetElement;
|
|
6758
|
+
const container = this.container;
|
|
6759
|
+
// Mount into DOM: wrapper before select, then move select inside
|
|
6760
|
+
select.parentNode?.insertBefore(this.node, select);
|
|
6761
|
+
this.node.insertBefore(select, container.tags.ViewPanel);
|
|
6762
|
+
container.tags.ViewPanel.addEventListener("mousedown", (e) => {
|
|
6763
|
+
e.stopPropagation();
|
|
6764
|
+
e.preventDefault();
|
|
6765
|
+
});
|
|
6766
|
+
Refresher.resizeBox(select, container.tags.ViewPanel);
|
|
6767
|
+
select.classList.add("init");
|
|
5408
6768
|
// initial mask
|
|
5409
6769
|
this.getAction()?.change(null, false);
|
|
5410
|
-
//
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
};
|
|
5428
|
-
// AJAX setup (if provided)
|
|
5429
|
-
if (options.ajax) {
|
|
5430
|
-
searchController.setAjax(options.ajax);
|
|
5431
|
-
}
|
|
6770
|
+
// Call parent lifecycle mount
|
|
6771
|
+
super.mount();
|
|
6772
|
+
}
|
|
6773
|
+
/**
|
|
6774
|
+
* Override lifecycle update - Called when data/state changes
|
|
6775
|
+
*/
|
|
6776
|
+
update() {
|
|
6777
|
+
if (this.state !== LifecycleState.MOUNTED)
|
|
6778
|
+
return;
|
|
6779
|
+
// Trigger any update logic here
|
|
6780
|
+
this.container.popup?.triggerResize?.();
|
|
6781
|
+
super.update();
|
|
6782
|
+
}
|
|
6783
|
+
/**
|
|
6784
|
+
* Setup event handlers (extracted from init for clarity)
|
|
6785
|
+
*/
|
|
6786
|
+
setupEventHandlers(select, container, options, searchController, searchbox) {
|
|
5432
6787
|
const optionAdapter = container.popup.optionAdapter;
|
|
5433
6788
|
let hightlightTimer = null;
|
|
5434
6789
|
const searchHandle = (keyword, isTrigger) => {
|
|
@@ -5494,9 +6849,32 @@ class SelectBox {
|
|
|
5494
6849
|
optionAdapter.onCollapsedChange = () => {
|
|
5495
6850
|
container.popup?.triggerResize?.();
|
|
5496
6851
|
};
|
|
5497
|
-
//
|
|
5498
|
-
|
|
5499
|
-
|
|
6852
|
+
// AJAX setup (if provided)
|
|
6853
|
+
if (options.ajax) {
|
|
6854
|
+
searchController.setAjax(options.ajax);
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
/**
|
|
6858
|
+
* Setup observers (extracted from init for clarity)
|
|
6859
|
+
*/
|
|
6860
|
+
setupObservers(selectObserver, datasetObserver, select, optionModelManager) {
|
|
6861
|
+
selectObserver.connect();
|
|
6862
|
+
selectObserver.onChanged = (sel) => {
|
|
6863
|
+
optionModelManager.update(Libs.parseSelectToArray(sel));
|
|
6864
|
+
this.getAction()?.refreshMask();
|
|
6865
|
+
};
|
|
6866
|
+
datasetObserver.connect();
|
|
6867
|
+
datasetObserver.onChanged = (dataset) => {
|
|
6868
|
+
if (Libs.string2Boolean(dataset.disabled) !== this.isDisabled) {
|
|
6869
|
+
this.isDisabled = Libs.string2Boolean(dataset.disabled);
|
|
6870
|
+
}
|
|
6871
|
+
if (Libs.string2Boolean(dataset.readonly) !== this.isReadOnly) {
|
|
6872
|
+
this.isReadOnly = Libs.string2Boolean(dataset.readonly);
|
|
6873
|
+
}
|
|
6874
|
+
if (Libs.string2Boolean(dataset.visible) !== this.isVisible) {
|
|
6875
|
+
this.isVisible = Libs.string2Boolean(dataset.visible ?? "1");
|
|
6876
|
+
}
|
|
6877
|
+
};
|
|
5500
6878
|
}
|
|
5501
6879
|
/**
|
|
5502
6880
|
* Disconnects observers associated with the SelectBox instance.
|
|
@@ -5509,6 +6887,38 @@ class SelectBox {
|
|
|
5509
6887
|
if (datasetObserver?.disconnect)
|
|
5510
6888
|
datasetObserver.disconnect();
|
|
5511
6889
|
}
|
|
6890
|
+
/**
|
|
6891
|
+
* Override lifecycle destroy - Complete cleanup
|
|
6892
|
+
*/
|
|
6893
|
+
destroy() {
|
|
6894
|
+
if (this.is(LifecycleState.DESTROYED)) {
|
|
6895
|
+
return;
|
|
6896
|
+
}
|
|
6897
|
+
// Disconnect observers
|
|
6898
|
+
this.deInit();
|
|
6899
|
+
// Destroy child components
|
|
6900
|
+
const container = this.container;
|
|
6901
|
+
container.searchController.destroy();
|
|
6902
|
+
container.directive.destroy();
|
|
6903
|
+
container.popup.destroy();
|
|
6904
|
+
container.accessorybox.destroy();
|
|
6905
|
+
container.placeholder.destroy();
|
|
6906
|
+
container.searchbox.destroy();
|
|
6907
|
+
// Remove from DOM
|
|
6908
|
+
this.node?.remove();
|
|
6909
|
+
// Clear all references
|
|
6910
|
+
this.container = {};
|
|
6911
|
+
this.node = null;
|
|
6912
|
+
this.options = null;
|
|
6913
|
+
this.optionModelManager = null;
|
|
6914
|
+
this.Selective = null;
|
|
6915
|
+
this.oldValue = null;
|
|
6916
|
+
this.isOpen = false;
|
|
6917
|
+
this.hasLoadedOnce = false;
|
|
6918
|
+
this.isBeforeSearch = false;
|
|
6919
|
+
// Call parent lifecycle destroy
|
|
6920
|
+
super.destroy();
|
|
6921
|
+
}
|
|
5512
6922
|
/**
|
|
5513
6923
|
* Returns an action API for controlling the SelectBox instance.
|
|
5514
6924
|
*/
|
|
@@ -5587,7 +6997,7 @@ class SelectBox {
|
|
|
5587
6997
|
},
|
|
5588
6998
|
valueDataset(_evtToken, strDataset = null, isArray = false) {
|
|
5589
6999
|
var item_list = [];
|
|
5590
|
-
superThis.getModelOption(true).forEach(m => {
|
|
7000
|
+
superThis.getModelOption(true).forEach((m) => {
|
|
5591
7001
|
item_list.push(strDataset ? m.dataset[strDataset] : m.dataset);
|
|
5592
7002
|
});
|
|
5593
7003
|
if (!isArray) {
|
|
@@ -5801,6 +7211,10 @@ class SelectBox {
|
|
|
5801
7211
|
if (superThis.options?.autoclose)
|
|
5802
7212
|
this.close();
|
|
5803
7213
|
}
|
|
7214
|
+
// Trigger update lifecycle
|
|
7215
|
+
if (superThis.is(LifecycleState.MOUNTED)) {
|
|
7216
|
+
superThis.update();
|
|
7217
|
+
}
|
|
5804
7218
|
},
|
|
5805
7219
|
refreshMask() {
|
|
5806
7220
|
let mask = bindedOptions.placeholder;
|
|
@@ -5841,7 +7255,7 @@ class SelectBox {
|
|
|
5841
7255
|
});
|
|
5842
7256
|
}
|
|
5843
7257
|
});
|
|
5844
|
-
}
|
|
7258
|
+
},
|
|
5845
7259
|
};
|
|
5846
7260
|
// mirror properties: disabled / readonly / visible
|
|
5847
7261
|
this.createSymProp(resp, "disabled", "isDisabled");
|
|
@@ -5890,9 +7304,6 @@ class SelectBox {
|
|
|
5890
7304
|
}
|
|
5891
7305
|
return flatOptions;
|
|
5892
7306
|
}
|
|
5893
|
-
detroy() {
|
|
5894
|
-
this.container.popup.detroy();
|
|
5895
|
-
}
|
|
5896
7307
|
}
|
|
5897
7308
|
|
|
5898
7309
|
/**
|
|
@@ -5919,12 +7330,12 @@ class ElementAdditionObserver {
|
|
|
5919
7330
|
this.actions = [];
|
|
5920
7331
|
}
|
|
5921
7332
|
/**
|
|
5922
|
-
*
|
|
7333
|
+
* connect observing the document for additions of elements matching the given tag.
|
|
5923
7334
|
* Detects both direct additions and nested matches within added subtrees.
|
|
5924
7335
|
*
|
|
5925
7336
|
* @param {string} tag - The tag name to watch for (e.g., "select", "div").
|
|
5926
7337
|
*/
|
|
5927
|
-
|
|
7338
|
+
connect(tag) {
|
|
5928
7339
|
if (this.isActive)
|
|
5929
7340
|
return;
|
|
5930
7341
|
this.isActive = true;
|
|
@@ -5953,7 +7364,7 @@ class ElementAdditionObserver {
|
|
|
5953
7364
|
* Stops observing for element additions and releases internal resources.
|
|
5954
7365
|
* No-ops if the observer is not active.
|
|
5955
7366
|
*/
|
|
5956
|
-
|
|
7367
|
+
disconnect() {
|
|
5957
7368
|
if (!this.isActive)
|
|
5958
7369
|
return;
|
|
5959
7370
|
this.isActive = false;
|
|
@@ -5970,9 +7381,21 @@ class ElementAdditionObserver {
|
|
|
5970
7381
|
}
|
|
5971
7382
|
}
|
|
5972
7383
|
|
|
5973
|
-
class Selective {
|
|
7384
|
+
class Selective extends Lifecycle {
|
|
5974
7385
|
constructor() {
|
|
7386
|
+
super();
|
|
7387
|
+
this.bindedQueries = new Map();
|
|
7388
|
+
this.init();
|
|
7389
|
+
}
|
|
7390
|
+
/**
|
|
7391
|
+
* Override lifecycle init - Initialize Selective system
|
|
7392
|
+
*/
|
|
7393
|
+
init() {
|
|
7394
|
+
if (!this.is(LifecycleState.NEW))
|
|
7395
|
+
return;
|
|
7396
|
+
// Initialize core properties
|
|
5975
7397
|
this.bindedQueries = new Map();
|
|
7398
|
+
super.init();
|
|
5976
7399
|
}
|
|
5977
7400
|
/**
|
|
5978
7401
|
* Binds Selective UI to all <select> elements matching the query.
|
|
@@ -5983,6 +7406,10 @@ class Selective {
|
|
|
5983
7406
|
* @param {object} options - Configuration overrides merged with defaults.
|
|
5984
7407
|
*/
|
|
5985
7408
|
bind(query, options) {
|
|
7409
|
+
// Auto-init if not initialized
|
|
7410
|
+
if (this.is(LifecycleState.NEW)) {
|
|
7411
|
+
this.init();
|
|
7412
|
+
}
|
|
5986
7413
|
const merged = Libs.mergeConfig(Libs.getDefaultConfig(), options);
|
|
5987
7414
|
// Ensure hooks exist
|
|
5988
7415
|
merged.on = merged.on ?? {};
|
|
@@ -5990,16 +7417,18 @@ class Selective {
|
|
|
5990
7417
|
this.bindedQueries.set(query, merged);
|
|
5991
7418
|
const doneToken = Libs.randomString();
|
|
5992
7419
|
Libs.callbackScheduler.on(doneToken, () => {
|
|
5993
|
-
iEvents.callEvent([this.find(query)], ...
|
|
7420
|
+
iEvents.callEvent([this.find(query)], ...merged.on.load);
|
|
5994
7421
|
Libs.callbackScheduler.clear(doneToken);
|
|
5995
7422
|
merged.on.load = [];
|
|
5996
7423
|
});
|
|
5997
7424
|
const selectElements = Libs.getElements(query);
|
|
7425
|
+
let hasAnyBound = false;
|
|
5998
7426
|
selectElements.forEach((item) => {
|
|
5999
7427
|
(async () => {
|
|
6000
7428
|
if (item.tagName === "SELECT") {
|
|
6001
7429
|
Libs.removeUnbinderMap(item);
|
|
6002
7430
|
if (this.applySelectBox(item, merged)) {
|
|
7431
|
+
hasAnyBound = true;
|
|
6003
7432
|
Libs.callbackScheduler.run(doneToken);
|
|
6004
7433
|
}
|
|
6005
7434
|
}
|
|
@@ -6008,6 +7437,30 @@ class Selective {
|
|
|
6008
7437
|
if (!Libs.getBindedCommand().includes(query)) {
|
|
6009
7438
|
Libs.getBindedCommand().push(query);
|
|
6010
7439
|
}
|
|
7440
|
+
// Mount if first bind and has elements
|
|
7441
|
+
if (this.is(LifecycleState.INITIALIZED) && hasAnyBound) {
|
|
7442
|
+
this.mount();
|
|
7443
|
+
}
|
|
7444
|
+
// Trigger update if already mounted
|
|
7445
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
7446
|
+
this.update();
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
/**
|
|
7450
|
+
* Override lifecycle mount - System is ready and has bound elements
|
|
7451
|
+
*/
|
|
7452
|
+
mount() {
|
|
7453
|
+
if (this.state !== LifecycleState.INITIALIZED)
|
|
7454
|
+
return;
|
|
7455
|
+
super.mount();
|
|
7456
|
+
}
|
|
7457
|
+
/**
|
|
7458
|
+
* Override lifecycle update - Called when bindings change
|
|
7459
|
+
*/
|
|
7460
|
+
update() {
|
|
7461
|
+
if (this.state !== LifecycleState.MOUNTED)
|
|
7462
|
+
return;
|
|
7463
|
+
super.update();
|
|
6011
7464
|
}
|
|
6012
7465
|
/**
|
|
6013
7466
|
* Finds the first bound SelectBox actions for a given query (or all bound queries if "*").
|
|
@@ -6052,20 +7505,26 @@ class Selective {
|
|
|
6052
7505
|
* Selective bindings automatically when they match previously bound queries.
|
|
6053
7506
|
*/
|
|
6054
7507
|
Observer() {
|
|
6055
|
-
this.EAObserver
|
|
6056
|
-
|
|
6057
|
-
this.
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
7508
|
+
if (!this.EAObserver) {
|
|
7509
|
+
this.EAObserver = new ElementAdditionObserver();
|
|
7510
|
+
this.EAObserver.onDetect((selectElement) => {
|
|
7511
|
+
this.bindedQueries.forEach((options, query) => {
|
|
7512
|
+
try {
|
|
7513
|
+
if (selectElement.matches(query)) {
|
|
7514
|
+
this.applySelectBox(selectElement, options);
|
|
7515
|
+
// Trigger update when new element is bound
|
|
7516
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
7517
|
+
this.update();
|
|
7518
|
+
}
|
|
7519
|
+
}
|
|
6061
7520
|
}
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
}
|
|
7521
|
+
catch (error) {
|
|
7522
|
+
console.warn(`Invalid selector: ${query}`, error);
|
|
7523
|
+
}
|
|
7524
|
+
});
|
|
6066
7525
|
});
|
|
6067
|
-
}
|
|
6068
|
-
this.EAObserver.
|
|
7526
|
+
}
|
|
7527
|
+
this.EAObserver.connect("select");
|
|
6069
7528
|
}
|
|
6070
7529
|
/**
|
|
6071
7530
|
* Destroys Selective instances. Supports:
|
|
@@ -6085,17 +7544,26 @@ class Selective {
|
|
|
6085
7544
|
else if (target instanceof HTMLSelectElement) {
|
|
6086
7545
|
this.destroyElement(target);
|
|
6087
7546
|
}
|
|
7547
|
+
// Trigger update after partial destroy
|
|
7548
|
+
if (target !== null && this.is(LifecycleState.MOUNTED)) {
|
|
7549
|
+
this.update();
|
|
7550
|
+
}
|
|
6088
7551
|
}
|
|
6089
7552
|
/**
|
|
6090
7553
|
* Destroys all bound Selective instances and clears bindings/state.
|
|
6091
7554
|
* Stops the ElementAdditionObserver.
|
|
7555
|
+
* Calls lifecycle destroy.
|
|
6092
7556
|
*/
|
|
6093
7557
|
destroyAll() {
|
|
7558
|
+
if (this.state === LifecycleState.DESTROYED)
|
|
7559
|
+
return;
|
|
6094
7560
|
const bindedCommands = Libs.getBindedCommand();
|
|
6095
7561
|
bindedCommands.forEach((query) => this.destroyByQuery(query));
|
|
6096
7562
|
this.bindedQueries.clear();
|
|
6097
7563
|
Libs.getBindedCommand().length = 0;
|
|
6098
|
-
this.EAObserver?.
|
|
7564
|
+
this.EAObserver?.disconnect();
|
|
7565
|
+
// Call parent lifecycle destroy
|
|
7566
|
+
super.destroy();
|
|
6099
7567
|
}
|
|
6100
7568
|
/**
|
|
6101
7569
|
* Destroys Selective instances bound to the specified query and removes
|
|
@@ -6125,17 +7593,17 @@ class Selective {
|
|
|
6125
7593
|
const bindMap = Libs.getBinderMap(selectElement);
|
|
6126
7594
|
if (!bindMap)
|
|
6127
7595
|
return;
|
|
6128
|
-
const
|
|
6129
|
-
popup?.detroy();
|
|
7596
|
+
const selfBox = bindMap.self;
|
|
6130
7597
|
Libs.setUnbinderMap(selectElement, bindMap);
|
|
6131
7598
|
const wasObserving = !!this.EAObserver;
|
|
6132
7599
|
if (wasObserving)
|
|
6133
|
-
this.EAObserver?.
|
|
7600
|
+
this.EAObserver?.disconnect();
|
|
6134
7601
|
try {
|
|
6135
7602
|
bindMap.self?.deInit?.();
|
|
6136
7603
|
}
|
|
6137
7604
|
catch (_) { }
|
|
6138
|
-
const wrapper = bindMap.container?.element ??
|
|
7605
|
+
const wrapper = bindMap.container?.element ??
|
|
7606
|
+
selectElement.parentElement;
|
|
6139
7607
|
selectElement.style.display = "";
|
|
6140
7608
|
selectElement.style.visibility = "";
|
|
6141
7609
|
selectElement.disabled = false;
|
|
@@ -6144,12 +7612,13 @@ class Selective {
|
|
|
6144
7612
|
wrapper.parentNode.replaceChild(selectElement, wrapper);
|
|
6145
7613
|
}
|
|
6146
7614
|
else {
|
|
6147
|
-
|
|
7615
|
+
selectElement.appendChild(selectElement);
|
|
6148
7616
|
}
|
|
6149
7617
|
Libs.removeBinderMap(selectElement);
|
|
6150
7618
|
if (wasObserving && this.bindedQueries.size > 0) {
|
|
6151
|
-
this.EAObserver?.
|
|
7619
|
+
this.EAObserver?.connect("select");
|
|
6152
7620
|
}
|
|
7621
|
+
selfBox?.destroy?.();
|
|
6153
7622
|
}
|
|
6154
7623
|
/**
|
|
6155
7624
|
* Rebinds a query by destroying existing instances and binding anew
|
|
@@ -6161,6 +7630,10 @@ class Selective {
|
|
|
6161
7630
|
rebind(query, options) {
|
|
6162
7631
|
this.destroyByQuery(query);
|
|
6163
7632
|
this.bind(query, options);
|
|
7633
|
+
// Trigger update after rebind
|
|
7634
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
7635
|
+
this.update();
|
|
7636
|
+
}
|
|
6164
7637
|
}
|
|
6165
7638
|
/**
|
|
6166
7639
|
* Applies SelectBox enhancement to a single <select> element:
|
|
@@ -6172,7 +7645,8 @@ class Selective {
|
|
|
6172
7645
|
* @returns {boolean} - False if already bound; true if successfully applied.
|
|
6173
7646
|
*/
|
|
6174
7647
|
applySelectBox(selectElement, options) {
|
|
6175
|
-
if (Libs.getBinderMap(selectElement) ||
|
|
7648
|
+
if (Libs.getBinderMap(selectElement) ||
|
|
7649
|
+
Libs.getUnbinderMap(selectElement)) {
|
|
6176
7650
|
return false;
|
|
6177
7651
|
}
|
|
6178
7652
|
const SEID = Libs.randomString(8);
|
|
@@ -6182,13 +7656,20 @@ class Selective {
|
|
|
6182
7656
|
options_cfg.SEID_HOLDER = `seui-${SEID}-placeholder`;
|
|
6183
7657
|
const bindMap = { options: options_cfg };
|
|
6184
7658
|
Libs.setBinderMap(selectElement, bindMap);
|
|
7659
|
+
// Create SelectBox with lifecycle
|
|
6185
7660
|
const selectBox = new SelectBox(selectElement, this);
|
|
7661
|
+
selectBox.on("onMount", () => {
|
|
7662
|
+
if (selectBox.container.view) {
|
|
7663
|
+
selectBox.container.view.addEventListener("mouseup", () => {
|
|
7664
|
+
bindMap.action?.toggle?.();
|
|
7665
|
+
});
|
|
7666
|
+
}
|
|
7667
|
+
});
|
|
7668
|
+
// Mount the SelectBox
|
|
7669
|
+
selectBox.mount();
|
|
6186
7670
|
bindMap.container = selectBox.container;
|
|
6187
7671
|
bindMap.action = selectBox.getAction();
|
|
6188
7672
|
bindMap.self = selectBox;
|
|
6189
|
-
selectBox.container.view.addEventListener("mouseup", () => {
|
|
6190
|
-
bindMap.action?.toggle?.();
|
|
6191
|
-
});
|
|
6192
7673
|
return true;
|
|
6193
7674
|
}
|
|
6194
7675
|
/**
|
|
@@ -6205,7 +7686,8 @@ class Selective {
|
|
|
6205
7686
|
getProperties(actionName, action) {
|
|
6206
7687
|
const descriptor = Object.getOwnPropertyDescriptor(action, actionName);
|
|
6207
7688
|
let type = "variable";
|
|
6208
|
-
if (descriptor?.get ||
|
|
7689
|
+
if (descriptor?.get ||
|
|
7690
|
+
(descriptor?.set && typeof action[actionName] !== "function")) {
|
|
6209
7691
|
type = "get-set";
|
|
6210
7692
|
}
|
|
6211
7693
|
else if (typeof action[actionName] === "function") {
|
|
@@ -6284,7 +7766,7 @@ const SECLASS = new Selective();
|
|
|
6284
7766
|
*
|
|
6285
7767
|
* Declared as `const` literal type to enable strict typing and easy tree-shaking.
|
|
6286
7768
|
*/
|
|
6287
|
-
const version = "1.2.
|
|
7769
|
+
const version = "1.2.4";
|
|
6288
7770
|
/**
|
|
6289
7771
|
* Library name identifier.
|
|
6290
7772
|
*
|