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.
Files changed (50) hide show
  1. package/dist/selective-ui.css.map +1 -1
  2. package/dist/selective-ui.esm.js +2106 -624
  3. package/dist/selective-ui.esm.js.map +1 -1
  4. package/dist/selective-ui.esm.min.js +2 -2
  5. package/dist/selective-ui.esm.min.js.br +0 -0
  6. package/dist/selective-ui.min.js +2 -2
  7. package/dist/selective-ui.min.js.br +0 -0
  8. package/dist/selective-ui.umd.js +2107 -625
  9. package/dist/selective-ui.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/ts/adapter/mixed-adapter.ts +156 -65
  12. package/src/ts/components/accessorybox.ts +153 -30
  13. package/src/ts/components/directive.ts +62 -11
  14. package/src/ts/components/option-handle.ts +124 -28
  15. package/src/ts/components/placeholder.ts +73 -16
  16. package/src/ts/components/popup/empty-state.ts +126 -0
  17. package/src/ts/components/popup/loading-state.ts +120 -0
  18. package/src/ts/components/{popup.ts → popup/popup.ts} +168 -71
  19. package/src/ts/components/searchbox.ts +82 -16
  20. package/src/ts/components/selectbox.ts +208 -109
  21. package/src/ts/core/base/adapter.ts +110 -44
  22. package/src/ts/core/base/lifecycle.ts +175 -0
  23. package/src/ts/core/base/model.ts +63 -32
  24. package/src/ts/core/base/recyclerview.ts +56 -18
  25. package/src/ts/core/base/view.ts +56 -19
  26. package/src/ts/core/base/virtual-recyclerview.ts +317 -126
  27. package/src/ts/core/model-manager.ts +4 -4
  28. package/src/ts/core/search-controller.ts +149 -24
  29. package/src/ts/global.ts +5 -5
  30. package/src/ts/index.ts +5 -5
  31. package/src/ts/models/group-model.ts +27 -6
  32. package/src/ts/models/option-model.ts +29 -6
  33. package/src/ts/services/ea-observer.ts +6 -6
  34. package/src/ts/types/components/searchbox.type.ts +1 -1
  35. package/src/ts/types/core/base/adapter.type.ts +2 -1
  36. package/src/ts/types/core/base/lifecycle.type.ts +62 -0
  37. package/src/ts/types/core/base/model.type.ts +3 -1
  38. package/src/ts/types/core/base/recyclerview.type.ts +2 -8
  39. package/src/ts/types/core/base/view.type.ts +36 -24
  40. package/src/ts/utils/istorage.ts +1 -1
  41. package/src/ts/utils/selective.ts +153 -36
  42. package/src/ts/views/group-view.ts +59 -21
  43. package/src/ts/views/option-view.ts +137 -68
  44. package/src/ts/components/empty-state.ts +0 -68
  45. package/src/ts/components/loading-state.ts +0 -66
  46. /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
  47. /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
  48. /package/src/css/components/{popup.css → popup/popup.css} +0 -0
  49. /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
  50. /package/src/css/{components/option.css → views/option-view.css} +0 -0
@@ -1,4 +1,4 @@
1
- /*! Selective UI v1.2.3 | MIT License */
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: true,
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
- * @class
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 PlaceHolder {
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
- * Represents a placeholder component for the Select UI, allowing dynamic updates to placeholder text.
838
- * Supports HTML content based on configuration and provides methods to get or set the placeholder value.
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.init(options);
1047
+ this.initialize(options);
845
1048
  }
846
1049
  /**
847
- * Initializes the placeholder element with provided options and renders its initial content.
1050
+ * Initializes the placeholder component.
848
1051
  *
849
- * @param {object} options - Configuration object containing placeholder text and HTML allowance.
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
- init(options) {
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
- * Retrieves the current placeholder text from the configuration.
1068
+ * Returns the current placeholder text from the configuration.
861
1069
  *
862
- * @returns {string} - The current placeholder text.
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 text and optionally saves it to the configuration.
869
- * Applies HTML sanitization based on the allowHtml setting.
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 {string} value - The new placeholder text.
872
- * @param {boolean} [isSave=true] - Whether to persist the new value in the configuration.
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 ? translated : Libs.stripHtml(translated);
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
- * @class
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
- this.node = this.init();
1136
+ super();
1137
+ this.init();
890
1138
  }
891
1139
  /**
892
- * Represents a directive button element used to toggle dropdown state.
893
- * Initializes a clickable node with appropriate ARIA attributes for accessibility.
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 is always an HTMLElement in practice.
897
- return Libs.nodeCreator({
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
- * Sets the dropdown state by toggling the "drop-down" CSS class on the directive node.
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 {boolean} value - If true, adds the "drop-down" class; otherwise removes it.
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
- * @class
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
- * Represents an option handle component that provides "Select All" and "Deselect All" actions
920
- * for multiple-selection lists. Includes methods to show/hide the handle, refresh its visibility,
921
- * and register callbacks for select/deselect events.
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.init(options);
1237
+ this.initialize(options);
931
1238
  }
932
1239
  /**
933
- * Initializes the option handle UI with "Select All" and "Deselect All" buttons,
934
- * wiring their click events to trigger registered callbacks.
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
- * @param {object} options - Configuration object containing text labels and feature flags.
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
- init(options) {
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
- * Determines whether the option handle should be available based on configuration.
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 {boolean} - True if multiple selection and select-all features are enabled.
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 of the option handle based on availability.
981
- * Shows the handle if available; hides it otherwise.
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
- refresh() {
984
- if (!this.node)
985
- return;
986
- if (this.available())
987
- this.show();
988
- else
989
- this.hide();
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 by removing the "hide" class.
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 by adding the "hide" class.
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 to be executed when "Select All" is clicked.
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 {Function|null} action - The function to call on select-all action.
1343
+ * @param action - Function to execute when "Select All" is triggered.
1011
1344
  */
1012
- OnSelectAll(action = null) {
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 to be executed when "Deselect All" is clicked.
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 {Function|null} action - The function to call on deselect-all action.
1356
+ * @param action - Function to execute when "Deselect All" is triggered.
1020
1357
  */
1021
- OnDeSelectAll(action = null) {
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
- * @class
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
- * Represents an empty state component that displays a message when no data or search results are available.
1033
- * Provides methods to show/hide the state and check its visibility.
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.init(options);
1415
+ this.initialize(options);
1040
1416
  }
1041
1417
  /**
1042
- * Initializes the empty state element with ARIA attributes for accessibility and stores configuration options.
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 {object} options - Configuration object containing text for "no data" and "not found" states.
1423
+ * @param options - Configuration object containing empty state messages.
1045
1424
  */
1046
- init(options) {
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 based on the provided type.
1436
+ * Displays the empty state message.
1057
1437
  *
1058
- * @param {"notfound" | "nodata"} [type="nodata"] - Determines which message to show:
1059
- * "notfound" for search results not found, "nodata" for no available data.
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" ? this.options.textNotFound : this.options.textNoData;
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 element by adding the "hide" class.
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 {boolean} - True if visible, false otherwise.
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
- * @class
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
- * Represents a loading state component that displays a loading message during data fetch or processing.
1092
- * Provides methods to show/hide the state and check its visibility.
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.init(options);
1521
+ this.initialize(options);
1099
1522
  }
1100
1523
  /**
1101
- * Initializes the loading state element with ARIA attributes for accessibility and stores configuration options.
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 {object} options - Configuration object containing text for the loading message.
1530
+ * @param options - Configuration object containing loading text.
1104
1531
  */
1105
- init(options) {
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 message and adjusts its size based on whether items are present.
1544
+ * Displays the loading state.
1117
1545
  *
1118
- * @param {boolean} hasItems - If true, applies a "small" style for compact display.
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 element by adding the "hide" class.
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 {boolean} - True if visible, false otherwise.
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
- * @class
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
- * Represents a popup component that manages rendering and interaction for a dropdown panel.
1276
- * Stores a reference to the ModelManager for handling option models and adapter logic.
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 {HTMLSelectElement|null} [select=null] - The source select element to bind.
1279
- * @param {object|null} [options=null] - Configuration options for the popup.
1280
- * @param {ModelManager|null} [modelManager=null] - The model manager instance for data handling.
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.init(select, options);
1790
+ this.initialize(select, options);
1305
1791
  }
1306
1792
  }
1307
1793
  /**
1308
- * Initializes the popup UI: creates DOM structure, wires OptionHandle, LoadingState, and EmptyState,
1309
- * binds ModelManager resources (adapter/recyclerView), and sets up empty-state logic.
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 {HTMLSelectElement} select - The source select element to bind.
1312
- * @param {object} options - Configuration for panel, IDs, multiple mode, and texts.
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
- init(select, options) {
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.OnSelectAll(() => {
1850
+ this.optionHandle.onSelectAll(() => {
1360
1851
  MMResources.adapter.checkAll(true);
1361
1852
  });
1362
- this.optionHandle.OnDeSelectAll(() => {
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 skips model events.
1369
- * Adjusts size based on current visibility stats and triggers a resize.
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 short delay, restores event handling,
1387
- * updates empty state based on adapter visibility stats, and triggers a resize.
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 visibility and item changes to keep the empty state in sync.
1404
- * Triggers resize when items change to reflect layout updates.
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 option container visibility based on aggregated stats.
1420
- * Shows "no data" when empty, "not found" when no visible items, otherwise shows options and handle.
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 {VisibilityStats|undefined} stats - Visibility stats; computed if omitted.
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.refresh();
1944
+ this.optionHandle.update();
1442
1945
  }
1443
1946
  }
1444
1947
  /**
1445
- * Registers a callback for adapter property pre-change notifications.
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
- * Registers a callback for adapter property post-change notifications.
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 to perform side effects (e.g., animations or external actions).
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: creates and attaches DOM if needed, initializes observers and effector,
1464
- * computes position and dimensions, and runs expand animation. Invokes callback on completion.
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.refresh();
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: disconnects the resize observer and runs collapse animation.
1511
- * Safely no-ops if the popup has not been created.
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 the popup is created,
1526
- * causing the layout to update based on the current parent dimensions.
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 scroll by listening to container scroll events and loading more data
1534
- * when nearing the bottom, respecting pagination state (enabled/loading/hasMore).
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 to load more items.
1537
- * @param _options - Optional SelectiveOptions (reserved for future behavior tuning).
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 tear down the popup instance and release all resources.
2089
+ * Completely tears down the popup and releases all resources.
1564
2090
  *
1565
- * Responsibilities:
1566
- * - Clear any pending timeouts and cancel animations/effects.
1567
- * - Remove event listeners (scroll, mousedown) and disconnect ResizeObserver.
1568
- * - Unmount and remove the DOM node; sever references to Effector/ModelManager.
1569
- * - Dispose adapter/recycler and child components (OptionHandle, EmptyState, LoadingState).
1570
- * - Reset flags and null out references to avoid memory leaks.
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
- * Safe to call multiple times; all operations are guarded via optional chaining.
2101
+ * Idempotent: safe to call multiple times.
1573
2102
  */
1574
- detroy() {
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, including size, position,
1623
- * padding, and border, accounting for iOS visual viewport offsets.
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 available viewport space,
1650
- * content size, and configured min/max heights; returns final position, top, and heights.
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 events by recalculating placement and dimensions,
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
- class SearchBox {
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 {object|null} [options=null] - Configuration (e.g., placeholder, accessibility IDs).
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.init(options);
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
- * @param {object} options - Configuration including placeholder and SEID_LIST for aria-controls.
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
- init(options) {
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 read-only based on `options.searchable`,
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 "hide" class.
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 {boolean} [isTrigger=true] - Whether to invoke onSearch with an empty string.
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 {string} value - The new placeholder text.
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 which option is currently highlighted.
2429
+ * Sets the active descendant for ARIA to indicate the currently highlighted option.
1850
2430
  *
1851
- * @param {string} id - The DOM id of the active option element.
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
- * @template TTarget
2164
- * @template TTags
2165
- * @template TView
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 from the underlying target element's "value" attribute.
2171
- * For single-select, this is typically a string; for multi-select, may be an array depending on usage.
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
- * Constructs a Model instance with configuration options and optional bindings to a target element and view.
2178
- * Stores references for later updates and rendering.
2786
+ * Creates a new Model instance.
2179
2787
  *
2180
- * @param {TOptions} options - Configuration options for the model.
2181
- * @param {TTarget|null} [targetElement=null] - The underlying element (e.g., <option> or group node).
2182
- * @param {TView|null} [view=null] - The associated view responsible for rendering the model.
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 reference and invokes the change hook.
2813
+ * Updates the bound target element and triggers the update lifecycle.
2196
2814
  *
2197
- * @param {TTarget|null} targetElement - The new target element to bind to the model.
2815
+ * @param targetElement - The new DOM element to associate with this model
2198
2816
  */
2199
- update(targetElement) {
2817
+ updateTarget(targetElement) {
2200
2818
  this.targetElement = targetElement;
2201
- this.onTargetChanged();
2819
+ this.update();
2202
2820
  }
2203
2821
  /**
2204
- * Cleans up references and invokes the removal hook when the model is no longer needed.
2822
+ * Hook executed when the model is updated.
2823
+ * Intended to be overridden by subclasses.
2205
2824
  */
2206
- remove() {
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?.getView()?.remove?.();
2839
+ this.view?.destroy();
2209
2840
  this.view = null;
2210
2841
  this.isRemoved = true;
2211
- this.onRemove();
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
- if (targetElement) {
2243
- this.label = targetElement.label;
2244
- this.collapsed = Libs.string2Boolean(targetElement.dataset?.collapsed);
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
- update(targetElement) {
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
- onTargetChanged() {
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
- (async () => {
2365
- this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
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
- onTargetChanged() {
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.update(dataVset);
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.update(dataVset);
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.remove();
3446
+ removedGroup.destroy();
2794
3447
  });
2795
3448
  oldOptionMap.forEach((removedOption) => {
2796
3449
  isUpdate = false;
2797
- removedOption.remove();
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
- * @template TItem
2861
- * @template TAdapter
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("items"): clears the container before items change,
2886
- * - onPropChanged("items"): re-renders after items change,
2887
- * then performs an initial render.
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
- * Used prior to re-rendering or when items are changing.
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 is due to an update operation.
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
- * @class
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
- * Initializes the accessory box with optional configuration and immediately calls init() if provided.
3634
+ * Creates an AccessoryBox and (optionally) initializes it with configuration.
2936
3635
  *
2937
- * @param {object|null} options - Configuration options for the accessory box (e.g., layout and behavior).
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.init(options);
3656
+ this.initialize(options);
2949
3657
  }
2950
3658
  /**
2951
- * Creates the accessory box DOM node and stores the provided options.
2952
- * The node is initially hidden and stops mouseup events from bubbling.
3659
+ * Stores options and starts the lifecycle.
2953
3660
  *
2954
- * @param {SelectiveOptions} options - Configuration object for the accessory box.
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(options) {
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
- this.options = options;
3692
+ super.init(); // Mark as INITIALIZED
2970
3693
  }
2971
3694
  /**
2972
- * Sets the root references for the accessory box (mask elements) and refreshes its location in the DOM.
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 {HTMLDivElement} selectUIMask - The overlay/mask element of the main Select UI.
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
- * Inserts the accessory box before or after the Select UI mask depending on the configured accessoryStyle.
2983
- * Keeps the accessory box aligned relative to the parent mask.
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 a ModelManager instance used to trigger and manage selection state changes.
3741
+ * Assigns the `ModelManager` instance used to trigger selection changes.
2998
3742
  *
2999
- * @param {ModelManager} modelManager - The model manager controlling option state.
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 currently selected options in multiple-select mode.
3006
- * Shows the accessory box when there are items; otherwise hides it. Triggers a window resize event.
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 {OptionModel[]} modelDatas - List of option models to render as accessory items.
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 && this.modelDatas.length > 0 && this.options.multiple) {
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.classList.remove("hide");
3840
+ this.node?.classList.remove("hide");
3063
3841
  }
3842
+ /** Hides the accessory box. */
3064
3843
  hide() {
3065
- this.node.classList.add("hide");
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
- class SearchController {
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 {HTMLSelectElement} selectElement - The native select element that provides context and data source.
3075
- * @param {ModelManager<MixedItem, any>} modelManager - Manager responsible for models and rendering updates.
3076
- * @param {SelectBox} selectBox - SelectBox handle.
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 {boolean} - True if AJAX config is present; false otherwise.
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
- * Load specific options by their values from server
3105
- * @param {string|string[]} values - Values to load
3106
- * @returns {Promise<{success: boolean, items: Array, message?: string}>}
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" ? cfg.data.bind(this.selectBox.Selective.find(this.selectBox.container.targetElement))("", 0) : cfg.data ?? {}),
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
- * Check if values exist in current options
3155
- * @param {string[]} values - Values to check
3156
- * @returns {{existing: string[], missing: string[]}}
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 {object} config - AJAX configuration object (e.g., endpoint, headers, query params).
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 {Popup} popupInstance - The popup used to display search results and loading state.
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._ajaxSearch(keyword, append);
3224
- return this._localSearch(keyword);
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._ajaxSearch(this.paginationState.currentKeyword, true);
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. Returns summary flags.
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 _localSearch(keyword) {
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 _ajaxSearch(keyword, append = false) {
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
- * Parses various server response shapes into a normalized structure for options and groups.
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
- * @template TItem
3644
- * @template TViewer
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 invokes onInit()
3650
- * to perform any subclass-specific setup. Accepts a generic list of models.
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.onInit();
4593
+ this.init();
3660
4594
  }
3661
4595
  /**
3662
- * Lifecycle hook called once after construction. Override in subclasses to
3663
- * perform setup tasks (e.g., event wiring, cache building).
3664
- */
3665
- onInit() { }
3666
- /**
3667
- * Binds an item model to its viewer at a given position. If the item has not
3668
- * been initialized yet, renders the viewer; otherwise triggers an update.
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 instance responsible for rendering the model.
3672
- * @param {number} position - The index of the item within the adapter.
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?.render?.();
4614
+ v?.mount?.();
3681
4615
  }
3682
4616
  }
3683
4617
  /**
3684
- * Registers a pre-change (debounced) callback for a property change pipeline.
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
- * @param {string} propName - The property name to observe (e.g., "items").
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 change pipeline.
3695
- * The callback is executed after the property is updated.
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 - The property name to observe (e.g., "items").
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, passing optional parameters
3705
- * to registered callbacks. Use this after mutating adapter state.
4646
+ * Triggers the **post-change** pipeline for a given property.
4647
+ * Use this **after** mutating the adapter state.
3706
4648
  *
3707
- * @param {string} propName - The property name to emit (e.g., "items").
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, passing optional parameters
3715
- * to registered callbacks. Use this before mutating adapter state.
4657
+ * Triggers the **pre-change** pipeline for a given property.
4658
+ * Use this **before** mutating the adapter state.
3716
4659
  *
3717
- * @param {string} propName - The property name to emit (e.g., "items").
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
- * Creates and returns a viewer instance for the given item within the specified parent container.
3725
- * Override in subclasses to return a concrete view implementation tailored to TItem.
4668
+ * Factory method that creates a viewer instance for the given item,
4669
+ * attached to the specified parent container.
3726
4670
  *
3727
- * @param {HTMLElement} parent - The container element that will host the viewer.
3728
- * @param {TItem} item - The model instance for which the viewer is created.
3729
- * @returns {TViewer|null} - The created viewer instance; null by default.
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, emitting pre-change and post-change
3744
- * notifications to observers. Does not render; call updateRecyclerView() to apply to the DOM.
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
- * Iterates through all items and ensures each has a viewer. For new items, calls viewHolder()
3764
- * to create the viewer, then binds via onViewHolder() and marks the item as initialized.
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 element in which item viewers are rendered.
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
- * Updates adapter data without performing any default actions.
3782
- * Override in subclasses to implement custom data refresh logic.
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 - The incoming data to apply to the adapter.
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
- * @template TTags
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
- * Initializes the view with a parent container element that will host its rendered content.
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 {HTMLElement} parent - The parent element into which this view will render.
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 {HTMLElement} - The root view element.
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
- * @extends {View<GroupViewTags>}
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
- * Renders the group view structure (header + items container), sets ARIA attributes,
3837
- * and appends the root element to the parent container.
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
- render() {
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
- * Performs a lightweight refresh of the view (currently updates the header label).
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 group header text content if a label is provided.
4906
+ * Updates the text content of the group header.
3880
4907
  *
3881
- * @param {string|null} [label=null] - The new label to display; if null, keeps current.
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/item views in this group.
4920
+ * Returns the container element that holds all item/option views
4921
+ * belonging to this group.
3892
4922
  *
3893
- * @returns {HTMLDivElement} - The items container element.
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 is not rendered.");
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
- * Toggles the group's visibility based on whether any child item is visible.
3902
- * Hides the entire group when all children are hidden.
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((child) => !child.classList.contains("hide"));
3909
- const isVisible = visibleItems.length > 0;
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 on the group and updates ARIA attributes accordingly.
4946
+ * Sets the collapsed/expanded state of the group.
3914
4947
  *
3915
- * @param {boolean} collapsed - True to collapse; false to expand.
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
- * @extends {View<OptionViewTags>}
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
- * Initializes the OptionView with a parent container and sets up the reactive config proxy.
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
- * @param {HTMLElement} parent - The parent element into which this view will be mounted.
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.setupConfigProxy();
5007
+ this.initialize();
3942
5008
  }
3943
5009
  /**
3944
- * Creates the internal configuration object and wraps it with a Proxy.
3945
- * The proxy intercepts property assignments and, if the view is rendered,
3946
- * applies only the necessary DOM changes for the updated property.
3947
- * No DOM mutations occur before the first render.
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
- setupConfigProxy() {
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 (checkbox) instead of single (radio).
5048
+ * Indicates whether the option supports multiple selection.
3979
5049
  *
3980
- * @returns {boolean} True if multiple selection is enabled; otherwise false.
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
- * @param {boolean} value - True to enable multiple selection; false for single selection.
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 includes an image block alongside the label.
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 for the option.
4004
- * When rendered, toggles related CSS classes and creates/removes the image element accordingly.
5073
+ * Shows or hides the image block.
4005
5074
  *
4006
- * @param {boolean} value - True to show the image; false to hide it.
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 entire option configuration via a Proxy.
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
- * @returns {object} The proxied configuration object.
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 set of configuration changes in batch.
4022
- * Only properties that differ from the current config are updated.
4023
- * When rendered, each changed property triggers a targeted DOM update via the proxy.
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
- * Renders the option view into the parent element.
4046
- * Builds the DOM structure (input, optional image, label) based on current config,
4047
- * assigns classes and ARIA attributes, mounts via Libs.mountView, and marks as rendered
4048
- * to allow future incremental updates through the config proxy.
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
- render() {
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 property change.
4114
- * Safely updates classes, attributes, styles, and child elements without re-rendering the whole view.
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.replace(/image-(top|right|bottom|left)/g, "").trim();
5219
+ root.className = root.className
5220
+ .replace(/image-(top|right|bottom|left)/g, "")
5221
+ .trim();
4141
5222
  const img = v.tags?.OptionImage;
4142
- if (img) {
4143
- img.remove();
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.replace(/image-(top|right|bottom|left)/g, "").trim();
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
- const val = String(newValue);
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
- const newClass = `align-vertical-${this.config.labelValign} align-horizontal-${this.config.labelHalign}`;
4174
- if (label.className !== newClass)
4175
- label.className = newClass;
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 <img> element for the option on demand and inserts it into the DOM.
4183
- * Skips creation if the view or root is missing, or if an image already exists.
4184
- * The image receives configured styles (width, height, borderRadius) and is placed
4185
- * before the label if present; otherwise appended to the root. Updates `v.tags.OptionImage`.
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
- const existing = v.tags?.OptionImage;
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 && label.parentElement)
5280
+ if (label?.parentElement) {
4202
5281
  root.insertBefore(image, label);
4203
- else
5282
+ }
5283
+ else {
4204
5284
  root.appendChild(image);
5285
+ }
4205
5286
  v.tags.OptionImage = image;
4206
5287
  }
4207
5288
  }
4208
5289
 
4209
5290
  /**
4210
- * @extends {Adapter<GroupModel|OptionModel>}
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
- * Build flat list of all options for navigation
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
- * Creates and returns the appropriate view instance for the given item type.
5376
+ * Factory method returning the appropriate view implementation per item type.
4254
5377
  *
4255
- * @param {HTMLElement} parent - The parent container element where the view will be attached.
4256
- * @param {GroupModel|OptionModel} item - The data model representing either a group or an option.
4257
- * @returns {GroupView|OptionView} - A new view instance corresponding to the item type.
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 (GroupModel or OptionModel) to its corresponding view
4266
- * and delegates rendering logic based on the type of item.
5388
+ * Binds a data model (group or option) to its view and delegates rendering
5389
+ * to specialized handlers.
4267
5390
  *
4268
- * @param {GroupModel|OptionModel} item - The data model representing either a group or an option.
4269
- * @param {GroupView|OptionView|null} viewer - The view instance that displays the item in the UI.
4270
- * @param {number} position - The position (index) of the item within its parent list.
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 and rendering logic for a group view, including header behavior,
4284
- * collapse/expand state, and initialization of option items.
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 {GroupModel} groupModel - The data model representing the group and its items.
4287
- * @param {GroupView} groupView - The view instance that renders the group in the UI.
4288
- * @param {number} position - The position (index) of the group within a list.
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 and rendering logic for an option item, including image setup,
4323
- * label rendering, event wiring, and selection state synchronization.
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 {OptionModel} optionModel - The data model representing a single option.
4326
- * @param {OptionView} optionViewer - The view instance that renders the option in the UI.
4327
- * @param {number} position - The index of this option within its group's item list.
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
- * Updates the list of items in the component and rebuilds its internal flat structure.
5525
+ * Replaces items and rebuilds the internal flat structure.
5526
+ * Emits the standard pre/post change notifications and updates lifecycle.
4397
5527
  *
4398
- * @param {Array<GroupModel|OptionModel>} items - The new collection of items to be displayed.
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 the component's items from an external source by delegating to setItems().
5538
+ * Synchronizes items from an external source by delegating to `setItems()`.
4408
5539
  *
4409
- * @param {Array<GroupModel|OptionModel>} items - The new collection of items to sync.
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 the component's data items and rebuilds the internal flat structure
4416
- * without triggering change notifications.
5546
+ * Updates items and rebuilds the flat structure **without** firing change notifications.
4417
5547
  *
4418
- * @param {Array<GroupModel|OptionModel>} items - The new collection of items to update.
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 that are currently selected.
5577
+ * Returns all currently selected option items.
4426
5578
  *
4427
- * @returns {OptionModel[]} - An array of selected option items from the flat list.
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 item, if any.
5585
+ * Returns the first selected option (if any).
4434
5586
  *
4435
- * @returns {OptionModel|undefined} - The first selected option or undefined if none are selected.
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 or unchecks all options when in multiple selection mode.
5593
+ * Checks/unchecks all options when in multiple selection mode.
4442
5594
  *
4443
- * @param {boolean} isChecked - If true, select all; if false, deselect all.
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 {(stats: {visibleCount:number,totalCount:number,hasVisible:boolean,isEmpty:boolean}) => void} callback
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
- * Notifies all registered visibility-change callbacks with up-to-date statistics.
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 (index 0).
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 forward/backward among visible options and optionally scrolls into view.
5640
+ * Moves the highlight among visible options and optionally scrolls into view.
4489
5641
  *
4490
- * @param {number} direction - Increment (+1) or decrement (-1) of the current visible index.
4491
- * @param {boolean} [isScrollToView=true] - Whether to scroll the highlighted item into view.
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 and visible option to select it.
4511
- * No-op if nothing is highlighted or the highlighted item is not visible.
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 instance, skipping invisible items,
4525
- * and optionally scrolls the highlighted element into view.
5676
+ * Highlights a target option (by flat index or model reference),
5677
+ * skipping invisible items and optionally scrolling into view.
4526
5678
  *
4527
- * @param {number|OptionModel} target - Flat index or the specific OptionModel to highlight.
4528
- * @param {boolean} [isScrollToView=true] - Whether to scroll the highlighted item into view.
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 highlight changes.
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 when a group's collapsed state changes.
4571
- * Override to handle side effects like analytics or layout adjustments.
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 sum queries.
4578
- * Supports O(log n) update and query operations for cumulative item heights.
4579
- * Uses 1-based indexing internally for BIT operations.
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
- constructor(n = 0) {
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
- this.n = 0;
4585
- this.reset(n);
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
- /** Resets tree to new size, clearing all values. */
4588
- reset(n) {
4589
- this.n = n;
4590
- this.bit = new Array(n + 1).fill(0);
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
- /** Adds delta to element at 1-based index i. */
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.n; x += x & -x)
5785
+ for (let x = i; x <= this.stackNum; x += x & -x)
4595
5786
  this.bit[x] += delta;
4596
5787
  }
4597
- /** Returns prefix sum for range [1..i]. */
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
- /** Returns sum in range [l..r] (1-based, inclusive). */
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
- /** Builds tree from 0-based array in O(n log n). */
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
- * Binary search to find largest index where prefix sum <= target.
4615
- * Returns count of items that fit within target height.
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.n)
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.n && cur + this.bit[next] <= target) {
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 height support.
5854
+ * Virtual RecyclerView with efficient windowing and dynamic-height support.
4634
5855
  *
4635
- * Only renders items visible in viewport plus overscan buffer, using padding
4636
- * elements to simulate scroll height. Supports variable item heights with
4637
- * adaptive estimation and maintains scroll position during height changes.
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
- * @template TItem - Model type for list items
4640
- * @template TAdapter - Adapter managing item views
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
- /** Updates virtualization settings (overscan, heights, etc). */
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
- * Removes previous adapter if exists, sets up scroll listeners and DOM structure.
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, rebuilds Fenwick tree.
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, false on initial setup
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
- * Useful for programmatic navigation to specific items.
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
- * Hard reset after visibility changes (e.g., search/filter cleared).
4799
- * Rebuilds all height structures and remounts visible window.
4800
- * Essential for fixing padding calculations after bulk visibility changes.
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
- * Removes probe element if dynamic heights disabled.
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
- * Checks if item is visible (not filtered/hidden).
4865
- * Defaults to visible if property undefined.
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 given index.
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 DOM thrashing.
4907
- * Used to adjust viewport calculations.
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
- * Used for accurate item height tracking.
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 get 0 height, others use cached or estimated height.
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
- * Updates running average for adaptive estimation.
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
- * @returns True if height changed beyond epsilon threshold
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 binary search then adjusts for visibility.
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 DOM maintaining index order.
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 if siblings indicate wrong position.
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 - only creates once per instance.
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 if any heights changed.
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 - schedules render update. */
6388
+ /** Scroll event handler schedules a render update. */
5083
6389
  onScroll() {
5084
6390
  this.scheduleUpdateWindow();
5085
6391
  }
5086
6392
  /**
5087
- * Core rendering logic - calculates and updates visible window.
6393
+ * Core rendering logic calculates and updates the visible window.
5088
6394
  *
5089
- * 1. Calculates viewport bounds accounting for scroll and sticky headers
5090
- * 2. Uses anchor item to prevent scroll jumping during height changes
5091
- * 3. Determines start/end indices with overscan buffer
5092
- * 4. Mounts/unmounts items as needed
5093
- * 5. Measures visible items if dynamic heights enabled
5094
- * 6. Updates padding elements to maintain total scroll height
5095
- * 7. Adjusts scroll position to maintain anchor item position
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
- /** Removes all mounted items outside [start..end] range. */
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
- /** Removes all items marked as invisible from DOM. */
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
- /** Returns cumulative height from start to top of item at index. */
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
- /** Returns total height of items in range [start..end]. */
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
- /** Returns total scrollable height for all items. */
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.init(select, Selective);
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
- * Initializes the SelectBox UI and behavior by wiring core components.
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
- init(select, Selective) {
6648
+ initialize(select, Selective) {
5318
6649
  const bindedMap = Libs.getBinderMap(select);
5319
- const options = bindedMap.options;
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((options).SEID_HOLDER ?? "");
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
- // Mount into DOM: wrapper before select, then move select inside
5362
- select.parentNode?.insertBefore(this.node, select);
5363
- this.node.insertBefore(select, container.tags.ViewPanel);
5364
- accessoryBox.setRoot(container.tags.ViewPanel);
5365
- accessoryBox.setModelManager(optionModelManager);
5366
- container.tags.ViewPanel.addEventListener("mousedown", (e) => {
5367
- e.stopPropagation();
5368
- e.preventDefault();
5369
- });
5370
- Refresher.resizeBox(select, container.tags.ViewPanel);
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
- // Observers
5411
- selectObserver.connect();
5412
- selectObserver.onChanged = (sel) => {
5413
- optionModelManager.update(Libs.parseSelectToArray(sel));
5414
- this.getAction()?.refreshMask();
5415
- };
5416
- datasetObserver.connect();
5417
- datasetObserver.onChanged = (dataset) => {
5418
- if (Libs.string2Boolean(dataset.disabled) !== this.isDisabled) {
5419
- this.isDisabled = Libs.string2Boolean(dataset.disabled);
5420
- }
5421
- if (Libs.string2Boolean(dataset.readonly) !== this.isReadOnly) {
5422
- this.isReadOnly = Libs.string2Boolean(dataset.readonly);
5423
- }
5424
- if (Libs.string2Boolean(dataset.visible) !== this.isVisible) {
5425
- this.isVisible = Libs.string2Boolean(dataset.visible ?? "1");
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
- // Initial states
5498
- this.isDisabled = Libs.string2Boolean(options.disabled);
5499
- this.isReadOnly = Libs.string2Boolean(options.readonly);
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
- * Starts observing the document for additions of elements matching the given tag.
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
- start(tag) {
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
- stop() {
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)], ...(merged.on.load));
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 = new ElementAdditionObserver();
6056
- this.EAObserver.onDetect((selectElement) => {
6057
- this.bindedQueries.forEach((options, query) => {
6058
- try {
6059
- if (selectElement.matches(query)) {
6060
- this.applySelectBox(selectElement, options);
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
- catch (error) {
6064
- console.warn(`Invalid selector: ${query}`, error);
6065
- }
7521
+ catch (error) {
7522
+ console.warn(`Invalid selector: ${query}`, error);
7523
+ }
7524
+ });
6066
7525
  });
6067
- });
6068
- this.EAObserver.start("select");
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?.stop();
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 popup = bindMap.container?.popup;
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?.stop();
7600
+ this.EAObserver?.disconnect();
6134
7601
  try {
6135
7602
  bindMap.self?.deInit?.();
6136
7603
  }
6137
7604
  catch (_) { }
6138
- const wrapper = bindMap.container?.element ?? selectElement.parentElement;
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
- document.body.appendChild(selectElement);
7615
+ selectElement.appendChild(selectElement);
6148
7616
  }
6149
7617
  Libs.removeBinderMap(selectElement);
6150
7618
  if (wasObserving && this.bindedQueries.size > 0) {
6151
- this.EAObserver?.start("select");
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) || Libs.getUnbinderMap(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 || (descriptor?.set && typeof action[actionName] !== "function")) {
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.3";
7769
+ const version = "1.2.4";
6288
7770
  /**
6289
7771
  * Library name identifier.
6290
7772
  *