selective-ui 1.2.7 → 1.3.0

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.
@@ -1,4 +1,4 @@
1
- /*! Selective UI v1.2.7 | MIT License */
1
+ /*! Selective UI v1.3.0 | MIT License */
2
2
  (function (global, factory) {
3
3
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
4
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
@@ -33,6 +33,7 @@
33
33
  autofocus: true,
34
34
  searchable: true,
35
35
  loadingfield: true,
36
+ preload: false,
36
37
  visible: true,
37
38
  skipError: false,
38
39
  customDelimiter: ",",
@@ -42,8 +43,8 @@
42
43
  textSelectAll: "Select all",
43
44
  textDeselectAll: "Deselect all",
44
45
  textAccessoryDeselect: "Deselect: ",
45
- animationtime: 200, // millisecond
46
- delaysearchtime: 200, // millisecond
46
+ animationtime: 200, // milliseconds
47
+ delaysearchtime: 200, // milliseconds
47
48
  allowHtml: false,
48
49
  maxSelected: 0,
49
50
  labelHalign: "left",
@@ -661,8 +662,7 @@
661
662
  tmp.innerHTML = s;
662
663
  tmp.querySelectorAll("script, style, iframe, object, embed, link").forEach((n) => n.remove());
663
664
  tmp.querySelectorAll("*").forEach((n) => {
664
- for (const k in n.attributes) {
665
- const a = n.attributes[k];
665
+ for (const a of Array.from(n.attributes)) {
666
666
  const name = a.name ?? "";
667
667
  const value = a.value ?? "";
668
668
  if (/^on/i.test(name)) {
@@ -703,26 +703,34 @@
703
703
  return s.replace(/đ/g, "d").replace(/Đ/g, "d");
704
704
  }
705
705
  /**
706
- * Parse select element to array (including optgroups)
707
- * @param {HTMLSelectElement} selectElement
708
- * @returns {Array<HTMLOptGroupElement|HTMLOptionElement>}
706
+ * Flattens a `<select>` element into an ordered array that includes optgroups
707
+ * and their child options.
708
+ *
709
+ * Notes:
710
+ * - Keeps original DOM order.
711
+ * - Adds a non-standard `__parentGroup` pointer on options inside optgroups.
712
+ *
713
+ * @param {HTMLSelectElement} selectElement - The source select element.
714
+ * @returns {Array<HTMLOptGroupElement | HTMLOptionElement>} Flattened node list.
709
715
  */
710
716
  static parseSelectToArray(selectElement) {
711
717
  const result = [];
712
- const children = Array.from(selectElement.children);
713
- children.forEach((child) => {
718
+ const children = selectElement.children;
719
+ for (let childIndex = 0; childIndex < children.length; childIndex++) {
720
+ const child = children[childIndex];
714
721
  if (child.tagName === "OPTGROUP") {
715
722
  const group = child;
716
723
  result.push(group);
717
- Array.from(group.children).forEach((option) => {
724
+ for (let optionIndex = 0; optionIndex < group.children.length; optionIndex++) {
725
+ const option = group.children[optionIndex];
718
726
  option["__parentGroup"] = group;
719
727
  result.push(option);
720
- });
728
+ }
721
729
  }
722
730
  else if (child.tagName === "OPTION") {
723
731
  result.push(child);
724
732
  }
725
- });
733
+ }
726
734
  return result;
727
735
  }
728
736
  /**
@@ -2404,7 +2412,7 @@
2404
2412
  }
2405
2413
  : {};
2406
2414
  // Load ModelManager resources into the list container
2407
- this.modelManager.load(this.optionsContainer, { isMultiple: options.multiple }, recyclerViewOpt);
2415
+ this.modelManager.load(this.optionsContainer, { isMultiple: options.multiple, options: options }, recyclerViewOpt);
2408
2416
  const MMResources = this.modelManager.getResources();
2409
2417
  this.optionAdapter = MMResources.adapter;
2410
2418
  this.recyclerView = MMResources.recyclerView;
@@ -2531,36 +2539,60 @@
2531
2539
  setupEffector(effectorSvc) {
2532
2540
  this.effSvc = effectorSvc;
2533
2541
  }
2542
+ /**
2543
+ * Loads and initializes the popup (one-time setup):
2544
+ * - Appends the popup node to `document.body`
2545
+ * - Initializes the resize observer service
2546
+ * - Binds the effect service to the popup element
2547
+ * - Blocks mousedown events inside the popup to prevent auto-close
2548
+ *
2549
+ * Safely no-ops when the popup has already been created
2550
+ * or required dependencies are missing.
2551
+ */
2552
+ load() {
2553
+ if (!this.node || !this.parent || !this.effSvc)
2554
+ return;
2555
+ if (this.isCreated)
2556
+ return;
2557
+ document.body.appendChild(this.node);
2558
+ this.isCreated = true;
2559
+ this.resizeObser = new ResizeObserverService();
2560
+ this.effSvc.setElement(this.node);
2561
+ this.node.addEventListener("mousedown", (e) => {
2562
+ e.stopPropagation();
2563
+ e.preventDefault();
2564
+ });
2565
+ }
2534
2566
  /**
2535
2567
  * Opens (expands) the popup:
2536
- * - On first open: appends to `document.body`, sets up resize observer, and blocks outside mousedown
2537
- * - Synchronizes the OptionHandle visibility and (optionally) the empty state
2538
- * - Computes placement from the parent anchor and runs the expand animation
2539
- * - Resumes recycler view after the animation completes
2568
+ * - Ensures the popup is loaded and initialized
2569
+ * - Synchronizes option handle visibility
2570
+ * - Optionally evaluates and applies the empty/not-found state
2571
+ * - Computes placement relative to the parent anchor
2572
+ * - Runs the expand animation
2573
+ * - Connects the resize observer after animation completes
2574
+ * - Resumes the recycler view
2575
+ *
2576
+ * Safely no-ops when required dependencies are missing.
2540
2577
  *
2541
2578
  * @param callback - Optional callback invoked when the opening animation completes.
2542
- * @param isShowEmptyState - If true, evaluates and applies empty/not-found state before animation.
2579
+ * @param isShowEmptyState - If true, applies the empty/not-found state before animation.
2543
2580
  */
2544
2581
  open(callback = null, isShowEmptyState) {
2545
2582
  if (!this.node || !this.options || !this.optionHandle || !this.parent || !this.effSvc)
2546
2583
  return;
2547
- if (!this.isCreated) {
2548
- document.body.appendChild(this.node);
2549
- this.isCreated = true;
2550
- this.resizeObser = new ResizeObserverService();
2551
- this.effSvc.setElement(this.node);
2552
- // Prevent the popup from closing when clicking inside
2553
- this.node.addEventListener("mousedown", (e) => {
2554
- e.stopPropagation();
2555
- e.preventDefault();
2556
- });
2557
- }
2584
+ // Ensure one-time initialization
2585
+ this.load();
2586
+ // Sync option visibility state
2558
2587
  this.optionHandle.update();
2588
+ // Apply empty state if requested
2559
2589
  if (isShowEmptyState) {
2560
2590
  this.updateEmptyState();
2561
2591
  }
2592
+ // Compute placement based on parent anchor
2562
2593
  const location = this.getParentLocation();
2563
2594
  const { position, top, maxHeight, realHeight } = this.calculatePosition(location);
2595
+ // Run expand animation
2564
2596
  this.effSvc.expand({
2565
2597
  duration: this.options.animationtime,
2566
2598
  display: "flex",
@@ -2573,13 +2605,14 @@
2573
2605
  onComplete: () => {
2574
2606
  if (!this.resizeObser || !this.parent)
2575
2607
  return;
2608
+ // Recompute position on parent resize to keep behavior consistent
2576
2609
  this.resizeObser.onChanged = (_metrics) => {
2577
- // Recompute from parent each time to keep behavior identical.
2578
2610
  const loc = this.getParentLocation();
2579
2611
  this.handleResize(loc);
2580
2612
  };
2581
2613
  this.resizeObser.connect(this.parent.container.tags.ViewPanel);
2582
2614
  callback?.();
2615
+ // Resume recycler view rendering after animation
2583
2616
  const rv = this.recyclerView;
2584
2617
  rv?.resume?.();
2585
2618
  },
@@ -6470,8 +6503,18 @@
6470
6503
  * @see {@link View}
6471
6504
  */
6472
6505
  class GroupView extends View {
6473
- constructor() {
6474
- super(...arguments);
6506
+ /**
6507
+ * Creates a new GroupView bound to the given parent element.
6508
+ *
6509
+ * Initialization flow:
6510
+ * 1. Calls `super(parent)` (View base constructor).
6511
+ *
6512
+ * @public
6513
+ * @param {HTMLElement} parent - Container element that will host this group view.
6514
+ * @param {SelectiveOptions} options - Optional configuration for this group view.
6515
+ */
6516
+ constructor(parent, options) {
6517
+ super(parent);
6475
6518
  /**
6476
6519
  * Strongly-typed reference to the mounted group view structure.
6477
6520
  *
@@ -6486,6 +6529,16 @@
6486
6529
  * @public
6487
6530
  */
6488
6531
  this.view = null;
6532
+ /**
6533
+ * Parsed configuration (bound from the `<select>` element via binder map).
6534
+ *
6535
+ * Provides feature flags (multiple/disabled/readonly/visible/virtualScroll/ajax/autoclose…),
6536
+ * a11y ids (e.g. `SEID_LIST`, `SEID_HOLDER`) and user callbacks under `options.on`.
6537
+ *
6538
+ * @internal
6539
+ */
6540
+ this.options = null;
6541
+ this.options = options;
6489
6542
  }
6490
6543
  /**
6491
6544
  * Mounts the group view into the DOM.
@@ -6493,8 +6546,8 @@
6493
6546
  * Creation flow:
6494
6547
  * 1. Generates unique group ID (7-character random string).
6495
6548
  * 2. Creates DOM structure via {@link Libs.mountNode}:
6496
- * - Root: `<div role="group" aria-labelledby="seui-{id}-header">`
6497
- * - Header: `<div role="presentation" id="seui-{id}-header">`
6549
+ * - Root: `<div role="group" aria-labelledby="seui-{this.options?.SEID || default}-{id}-header">`
6550
+ * - Header: `<div role="presentation" id="seui-{this.options?.SEID || default}-{id}-header">`
6498
6551
  * - Items: `<div role="group">` (nested group for child items)
6499
6552
  * 3. Appends root to {@link parent} container.
6500
6553
  * 4. Transitions `INITIALIZED → MOUNTED` via `super.mount()`.
@@ -6520,8 +6573,8 @@
6520
6573
  node: "div",
6521
6574
  classList: ["seui-group"],
6522
6575
  role: "group",
6523
- ariaLabelledby: `seui-${group_id}-header`,
6524
- id: `seui-${group_id}-group`,
6576
+ ariaLabelledby: `seui-${this.options?.SEID || "default"}-${group_id}-header`,
6577
+ id: `seui-${this.options?.SEID || "default"}-${group_id}-group`,
6525
6578
  },
6526
6579
  child: {
6527
6580
  GroupHeader: {
@@ -6529,7 +6582,7 @@
6529
6582
  node: "div",
6530
6583
  classList: ["seui-group-header"],
6531
6584
  role: "presentation",
6532
- id: `seui-${group_id}-header`,
6585
+ id: `seui-${this.options?.SEID || "default"}-${group_id}-header`,
6533
6586
  },
6534
6587
  },
6535
6588
  GroupItems: {
@@ -6738,8 +6791,9 @@
6738
6791
  *
6739
6792
  * @public
6740
6793
  * @param {HTMLElement} parent - Container element that will host this option view.
6794
+ * @param {SelectiveOptions} options - Optional configuration for this option view.
6741
6795
  */
6742
- constructor(parent) {
6796
+ constructor(parent, options) {
6743
6797
  super(parent);
6744
6798
  /**
6745
6799
  * Strongly-typed reference to the mounted option view structure.
@@ -6755,6 +6809,15 @@
6755
6809
  * @public
6756
6810
  */
6757
6811
  this.view = null;
6812
+ /**
6813
+ * Parsed configuration (bound from the `<select>` element via binder map).
6814
+ *
6815
+ * Provides feature flags (multiple/disabled/readonly/visible/virtualScroll/ajax/autoclose…),
6816
+ * a11y ids (e.g. `SEID_LIST`, `SEID_HOLDER`) and user callbacks under `options.on`.
6817
+ *
6818
+ * @internal
6819
+ */
6820
+ this.options = null;
6758
6821
  /**
6759
6822
  * Internal configuration object (Proxy target).
6760
6823
  *
@@ -6796,6 +6859,7 @@
6796
6859
  * @private
6797
6860
  */
6798
6861
  this.isRendered = false;
6862
+ this.options = options;
6799
6863
  this.initialize();
6800
6864
  }
6801
6865
  /**
@@ -7000,7 +7064,7 @@
7000
7064
  mount() {
7001
7065
  const viewClass = ["seui-option-view"];
7002
7066
  const opt_id = Libs.randomString(7);
7003
- const inputID = `option_${opt_id}`;
7067
+ const inputID = `option_${this.options?.SEID ?? "default"}_${opt_id}`;
7004
7068
  if (this.config.isMultiple)
7005
7069
  viewClass.push("multiple");
7006
7070
  if (this.config.hasImage) {
@@ -7046,7 +7110,7 @@
7046
7110
  OptionView: {
7047
7111
  tag: {
7048
7112
  node: "div",
7049
- id: `seui-${opt_id}-option`,
7113
+ id: `seui-${this.options?.SEID ?? "default"}-${opt_id}-option`,
7050
7114
  classList: viewClass,
7051
7115
  role: "option",
7052
7116
  ariaSelected: "false",
@@ -7260,6 +7324,15 @@
7260
7324
  super(items);
7261
7325
  /** Whether the adapter operates in multi-selection mode. */
7262
7326
  this.isMultiple = false;
7327
+ /**
7328
+ * Parsed configuration (bound from the `<select>` element via binder map).
7329
+ *
7330
+ * Provides feature flags (multiple/disabled/readonly/visible/virtualScroll/ajax/autoclose…),
7331
+ * a11y ids (e.g. `SEID_LIST`, `SEID_HOLDER`) and user callbacks under `options.on`.
7332
+ *
7333
+ * @internal
7334
+ */
7335
+ this.options = null;
7263
7336
  /**
7264
7337
  * Subscribers for aggregated visibility statistics.
7265
7338
  * Fired via a debounced scheduler to avoid repeated recomputation during batch updates.
@@ -7352,8 +7425,8 @@
7352
7425
  */
7353
7426
  viewHolder(parent, item) {
7354
7427
  if (item instanceof GroupModel)
7355
- return new GroupView(parent);
7356
- return new OptionView(parent);
7428
+ return new GroupView(parent, this.options);
7429
+ return new OptionView(parent, this.options);
7357
7430
  }
7358
7431
  /**
7359
7432
  * Binds a model (group or option) to its view and delegates to specialized handlers.
@@ -7421,7 +7494,7 @@
7421
7494
  groupModel.items.forEach((optionModel, idx) => {
7422
7495
  let optionViewer = optionModel.view;
7423
7496
  if (!optionModel.isInit || !optionViewer) {
7424
- optionViewer = new OptionView(itemsContainer);
7497
+ optionViewer = new OptionView(itemsContainer, this.options);
7425
7498
  }
7426
7499
  this.handleOptionView(optionModel, optionViewer, idx);
7427
7500
  optionModel.isInit = true;
@@ -9178,16 +9251,17 @@
9178
9251
  });
9179
9252
  this.optionModelManager = optionModelManager;
9180
9253
  // Popup
9181
- container.popup = new Popup(select, options, optionModelManager);
9182
- container.popup.setupEffector(effector);
9183
- container.popup.setupInfiniteScroll(searchController, options);
9184
- container.popup.onAdapterPropChanged("selected", () => {
9254
+ const popup = new Popup(select, options, optionModelManager);
9255
+ container.popup = popup;
9256
+ popup.setupEffector(effector);
9257
+ popup.setupInfiniteScroll(searchController, options);
9258
+ popup.onAdapterPropChanged("selected", () => {
9185
9259
  this.getAction()?.change(null, true);
9186
9260
  });
9187
- container.popup.onAdapterPropChanged("selected_internal", () => {
9261
+ popup.onAdapterPropChanged("selected_internal", () => {
9188
9262
  this.getAction()?.change(null, false);
9189
9263
  });
9190
- container.popup.onAdapterPropChanging("select", () => {
9264
+ popup.onAdapterPropChanging("select", () => {
9191
9265
  this.oldValue = this.getAction()?.value ?? "";
9192
9266
  });
9193
9267
  accessoryBox.setRoot(container.tags.ViewPanel);
@@ -9244,7 +9318,11 @@
9244
9318
  Refresher.resizeBox(select, container.tags.ViewPanel);
9245
9319
  select.classList.add("init");
9246
9320
  // initial mask
9247
- this.getAction()?.change(null, false);
9321
+ const action = this.getAction();
9322
+ action?.change?.(null, false);
9323
+ if (this.options.preload) {
9324
+ action?.load?.();
9325
+ }
9248
9326
  // Call parent lifecycle mount
9249
9327
  super.mount();
9250
9328
  }
@@ -9684,6 +9762,26 @@
9684
9762
  }
9685
9763
  this.change(false, trigger);
9686
9764
  },
9765
+ load() {
9766
+ if ((!superThis.hasLoadedOnce || superThis.isBeforeSearch) && bindedOptions?.ajax) {
9767
+ container.searchController.resetPagination();
9768
+ container.popup.showLoading();
9769
+ superThis.hasLoadedOnce = true;
9770
+ superThis.isBeforeSearch = false;
9771
+ setTimeout(() => {
9772
+ if (!container.popup || !container.searchController)
9773
+ return;
9774
+ container.searchController
9775
+ .search("")
9776
+ .then(() => container.popup?.triggerResize?.())
9777
+ .catch((err) => console.error("Initial ajax load error:", err));
9778
+ }, bindedOptions.animationtime);
9779
+ container.popup.load();
9780
+ }
9781
+ else {
9782
+ container.popup.load();
9783
+ }
9784
+ },
9687
9785
  open() {
9688
9786
  if (superThis.isOpen)
9689
9787
  return;
@@ -9693,45 +9791,34 @@
9693
9791
  if (closeToken.isCancel)
9694
9792
  return;
9695
9793
  }
9696
- if (this.disabled)
9794
+ if (this.disabled) {
9697
9795
  return;
9796
+ }
9698
9797
  const beforeShowToken = iEvents.callEvent([getInstance()], ...bindedOptions.on.beforeShow);
9699
- if (beforeShowToken.isCancel)
9798
+ if (beforeShowToken.isCancel) {
9700
9799
  return;
9800
+ }
9701
9801
  superThis.isOpen = true;
9702
9802
  container.directive.setDropdown(true);
9703
9803
  const adapter = container.popup.optionAdapter;
9704
9804
  const selectedOption = adapter.getSelectedItem();
9705
- if (selectedOption)
9805
+ if (selectedOption) {
9706
9806
  adapter.setHighlight(selectedOption, false);
9707
- else
9708
- adapter.resetHighlight();
9709
- if ((!superThis.hasLoadedOnce || superThis.isBeforeSearch) && bindedOptions?.ajax) {
9710
- container.searchController.resetPagination();
9711
- container.popup.showLoading();
9712
- superThis.hasLoadedOnce = true;
9713
- superThis.isBeforeSearch = false;
9714
- setTimeout(() => {
9715
- if (!container.popup || !container.searchController)
9716
- return;
9717
- container.searchController
9718
- .search("")
9719
- .then(() => container.popup?.triggerResize?.())
9720
- .catch((err) => console.error("Initial ajax load error:", err));
9721
- }, bindedOptions.animationtime);
9722
- container.popup.open(null, false);
9723
9807
  }
9724
9808
  else {
9725
- container.popup.open(null, true);
9809
+ adapter.resetHighlight();
9726
9810
  }
9811
+ this.load();
9812
+ container.popup.open(null, !container.popup.loadingState.isVisible);
9727
9813
  container.searchbox.show();
9728
9814
  const ViewPanel = container.tags.ViewPanel;
9729
9815
  ViewPanel.setAttribute("aria-expanded", "true");
9730
9816
  ViewPanel.setAttribute("aria-controls", bindedOptions.SEID_LIST);
9731
9817
  ViewPanel.setAttribute("aria-haspopup", "listbox");
9732
9818
  ViewPanel.setAttribute("aria-labelledby", bindedOptions.SEID_HOLDER);
9733
- if (bindedOptions.multiple)
9819
+ if (bindedOptions.multiple) {
9734
9820
  ViewPanel.setAttribute("aria-multiselectable", "true");
9821
+ }
9735
9822
  iEvents.callEvent([getInstance()], ...bindedOptions.on.show);
9736
9823
  if (superThis.pluginContext) {
9737
9824
  superThis.runPluginHook("onOpen", (plugin) => plugin.onOpen?.(superThis.pluginContext));
@@ -10808,7 +10895,7 @@
10808
10895
  if (typeof globalThis.GLOBAL_SEUI == "undefined") {
10809
10896
  const SECLASS = new Selective();
10810
10897
  globalThis.GLOBAL_SEUI = {
10811
- version: "1.2.7",
10898
+ version: "1.3.0",
10812
10899
  name: "SelectiveUI",
10813
10900
  bind: SECLASS.bind.bind(SECLASS),
10814
10901
  find: SECLASS.find.bind(SECLASS),
@@ -10841,7 +10928,7 @@
10841
10928
  init();
10842
10929
  }
10843
10930
  }
10844
- console.log(`[${"SelectiveUI"}] v${"1.2.7"} loaded successfully`);
10931
+ console.log(`[${"SelectiveUI"}] v${"1.3.0"} loaded successfully`);
10845
10932
  }
10846
10933
  else {
10847
10934
  console.warn(`[${globalThis.GLOBAL_SEUI.name}] Already loaded (v${globalThis.GLOBAL_SEUI.version}). ` +