selective-ui 1.2.6 → 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.6 | 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
  },
@@ -3788,6 +3821,7 @@
3788
3821
  */
3789
3822
  this.privOnCollapsedChanged = [];
3790
3823
  this.label = this.targetElement.label;
3824
+ this.collapsed = Libs.string2Boolean(this.targetElement.dataset?.collapsed);
3791
3825
  }
3792
3826
  /**
3793
3827
  * Initializes group state from the backing `<optgroup>` (if present) and mounts the model.
@@ -3804,7 +3838,6 @@
3804
3838
  * @override
3805
3839
  */
3806
3840
  init() {
3807
- this.collapsed = Libs.string2Boolean(this.targetElement.dataset?.collapsed);
3808
3841
  super.init();
3809
3842
  this.mount();
3810
3843
  }
@@ -5524,7 +5557,8 @@
5524
5557
  .join(",");
5525
5558
  let payload;
5526
5559
  if (typeof cfg.data === "function") {
5527
- payload = cfg.data.bind(this.selectBox.Selective.find(this.selectBox.container.targetElement))(keyword, page);
5560
+ const selectiveInstance = this.selectBox?.Selective?.find(this.selectBox?.container?.targetElement);
5561
+ payload = cfg.data.call(selectiveInstance, keyword, page);
5528
5562
  if (payload && typeof payload.selectedValue === "undefined")
5529
5563
  payload.selectedValue = selectedValues;
5530
5564
  }
@@ -6469,8 +6503,18 @@
6469
6503
  * @see {@link View}
6470
6504
  */
6471
6505
  class GroupView extends View {
6472
- constructor() {
6473
- 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);
6474
6518
  /**
6475
6519
  * Strongly-typed reference to the mounted group view structure.
6476
6520
  *
@@ -6485,6 +6529,16 @@
6485
6529
  * @public
6486
6530
  */
6487
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;
6488
6542
  }
6489
6543
  /**
6490
6544
  * Mounts the group view into the DOM.
@@ -6492,8 +6546,8 @@
6492
6546
  * Creation flow:
6493
6547
  * 1. Generates unique group ID (7-character random string).
6494
6548
  * 2. Creates DOM structure via {@link Libs.mountNode}:
6495
- * - Root: `<div role="group" aria-labelledby="seui-{id}-header">`
6496
- * - 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">`
6497
6551
  * - Items: `<div role="group">` (nested group for child items)
6498
6552
  * 3. Appends root to {@link parent} container.
6499
6553
  * 4. Transitions `INITIALIZED → MOUNTED` via `super.mount()`.
@@ -6519,8 +6573,8 @@
6519
6573
  node: "div",
6520
6574
  classList: ["seui-group"],
6521
6575
  role: "group",
6522
- ariaLabelledby: `seui-${group_id}-header`,
6523
- 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`,
6524
6578
  },
6525
6579
  child: {
6526
6580
  GroupHeader: {
@@ -6528,7 +6582,7 @@
6528
6582
  node: "div",
6529
6583
  classList: ["seui-group-header"],
6530
6584
  role: "presentation",
6531
- id: `seui-${group_id}-header`,
6585
+ id: `seui-${this.options?.SEID || "default"}-${group_id}-header`,
6532
6586
  },
6533
6587
  },
6534
6588
  GroupItems: {
@@ -6737,8 +6791,9 @@
6737
6791
  *
6738
6792
  * @public
6739
6793
  * @param {HTMLElement} parent - Container element that will host this option view.
6794
+ * @param {SelectiveOptions} options - Optional configuration for this option view.
6740
6795
  */
6741
- constructor(parent) {
6796
+ constructor(parent, options) {
6742
6797
  super(parent);
6743
6798
  /**
6744
6799
  * Strongly-typed reference to the mounted option view structure.
@@ -6754,6 +6809,15 @@
6754
6809
  * @public
6755
6810
  */
6756
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;
6757
6821
  /**
6758
6822
  * Internal configuration object (Proxy target).
6759
6823
  *
@@ -6795,6 +6859,7 @@
6795
6859
  * @private
6796
6860
  */
6797
6861
  this.isRendered = false;
6862
+ this.options = options;
6798
6863
  this.initialize();
6799
6864
  }
6800
6865
  /**
@@ -6999,7 +7064,7 @@
6999
7064
  mount() {
7000
7065
  const viewClass = ["seui-option-view"];
7001
7066
  const opt_id = Libs.randomString(7);
7002
- const inputID = `option_${opt_id}`;
7067
+ const inputID = `option_${this.options?.SEID ?? "default"}_${opt_id}`;
7003
7068
  if (this.config.isMultiple)
7004
7069
  viewClass.push("multiple");
7005
7070
  if (this.config.hasImage) {
@@ -7045,7 +7110,7 @@
7045
7110
  OptionView: {
7046
7111
  tag: {
7047
7112
  node: "div",
7048
- id: `seui-${opt_id}-option`,
7113
+ id: `seui-${this.options?.SEID ?? "default"}-${opt_id}-option`,
7049
7114
  classList: viewClass,
7050
7115
  role: "option",
7051
7116
  ariaSelected: "false",
@@ -7259,6 +7324,15 @@
7259
7324
  super(items);
7260
7325
  /** Whether the adapter operates in multi-selection mode. */
7261
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;
7262
7336
  /**
7263
7337
  * Subscribers for aggregated visibility statistics.
7264
7338
  * Fired via a debounced scheduler to avoid repeated recomputation during batch updates.
@@ -7351,8 +7425,8 @@
7351
7425
  */
7352
7426
  viewHolder(parent, item) {
7353
7427
  if (item instanceof GroupModel)
7354
- return new GroupView(parent);
7355
- return new OptionView(parent);
7428
+ return new GroupView(parent, this.options);
7429
+ return new OptionView(parent, this.options);
7356
7430
  }
7357
7431
  /**
7358
7432
  * Binds a model (group or option) to its view and delegates to specialized handlers.
@@ -7420,7 +7494,7 @@
7420
7494
  groupModel.items.forEach((optionModel, idx) => {
7421
7495
  let optionViewer = optionModel.view;
7422
7496
  if (!optionModel.isInit || !optionViewer) {
7423
- optionViewer = new OptionView(itemsContainer);
7497
+ optionViewer = new OptionView(itemsContainer, this.options);
7424
7498
  }
7425
7499
  this.handleOptionView(optionModel, optionViewer, idx);
7426
7500
  optionModel.isInit = true;
@@ -9177,16 +9251,17 @@
9177
9251
  });
9178
9252
  this.optionModelManager = optionModelManager;
9179
9253
  // Popup
9180
- container.popup = new Popup(select, options, optionModelManager);
9181
- container.popup.setupEffector(effector);
9182
- container.popup.setupInfiniteScroll(searchController, options);
9183
- 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", () => {
9184
9259
  this.getAction()?.change(null, true);
9185
9260
  });
9186
- container.popup.onAdapterPropChanged("selected_internal", () => {
9261
+ popup.onAdapterPropChanged("selected_internal", () => {
9187
9262
  this.getAction()?.change(null, false);
9188
9263
  });
9189
- container.popup.onAdapterPropChanging("select", () => {
9264
+ popup.onAdapterPropChanging("select", () => {
9190
9265
  this.oldValue = this.getAction()?.value ?? "";
9191
9266
  });
9192
9267
  accessoryBox.setRoot(container.tags.ViewPanel);
@@ -9243,7 +9318,11 @@
9243
9318
  Refresher.resizeBox(select, container.tags.ViewPanel);
9244
9319
  select.classList.add("init");
9245
9320
  // initial mask
9246
- this.getAction()?.change(null, false);
9321
+ const action = this.getAction();
9322
+ action?.change?.(null, false);
9323
+ if (this.options.preload) {
9324
+ action?.load?.();
9325
+ }
9247
9326
  // Call parent lifecycle mount
9248
9327
  super.mount();
9249
9328
  }
@@ -9683,6 +9762,26 @@
9683
9762
  }
9684
9763
  this.change(false, trigger);
9685
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
+ },
9686
9785
  open() {
9687
9786
  if (superThis.isOpen)
9688
9787
  return;
@@ -9692,45 +9791,34 @@
9692
9791
  if (closeToken.isCancel)
9693
9792
  return;
9694
9793
  }
9695
- if (this.disabled)
9794
+ if (this.disabled) {
9696
9795
  return;
9796
+ }
9697
9797
  const beforeShowToken = iEvents.callEvent([getInstance()], ...bindedOptions.on.beforeShow);
9698
- if (beforeShowToken.isCancel)
9798
+ if (beforeShowToken.isCancel) {
9699
9799
  return;
9800
+ }
9700
9801
  superThis.isOpen = true;
9701
9802
  container.directive.setDropdown(true);
9702
9803
  const adapter = container.popup.optionAdapter;
9703
9804
  const selectedOption = adapter.getSelectedItem();
9704
- if (selectedOption)
9805
+ if (selectedOption) {
9705
9806
  adapter.setHighlight(selectedOption, false);
9706
- else
9707
- adapter.resetHighlight();
9708
- if ((!superThis.hasLoadedOnce || superThis.isBeforeSearch) && bindedOptions?.ajax) {
9709
- container.searchController.resetPagination();
9710
- container.popup.showLoading();
9711
- superThis.hasLoadedOnce = true;
9712
- superThis.isBeforeSearch = false;
9713
- setTimeout(() => {
9714
- if (!container.popup || !container.searchController)
9715
- return;
9716
- container.searchController
9717
- .search("")
9718
- .then(() => container.popup?.triggerResize?.())
9719
- .catch((err) => console.error("Initial ajax load error:", err));
9720
- }, bindedOptions.animationtime);
9721
- container.popup.open(null, false);
9722
9807
  }
9723
9808
  else {
9724
- container.popup.open(null, true);
9809
+ adapter.resetHighlight();
9725
9810
  }
9811
+ this.load();
9812
+ container.popup.open(null, !container.popup.loadingState.isVisible);
9726
9813
  container.searchbox.show();
9727
9814
  const ViewPanel = container.tags.ViewPanel;
9728
9815
  ViewPanel.setAttribute("aria-expanded", "true");
9729
9816
  ViewPanel.setAttribute("aria-controls", bindedOptions.SEID_LIST);
9730
9817
  ViewPanel.setAttribute("aria-haspopup", "listbox");
9731
9818
  ViewPanel.setAttribute("aria-labelledby", bindedOptions.SEID_HOLDER);
9732
- if (bindedOptions.multiple)
9819
+ if (bindedOptions.multiple) {
9733
9820
  ViewPanel.setAttribute("aria-multiselectable", "true");
9821
+ }
9734
9822
  iEvents.callEvent([getInstance()], ...bindedOptions.on.show);
9735
9823
  if (superThis.pluginContext) {
9736
9824
  superThis.runPluginHook("onOpen", (plugin) => plugin.onOpen?.(superThis.pluginContext));
@@ -10004,7 +10092,7 @@
10004
10092
  *
10005
10093
  * @internal
10006
10094
  */
10007
- this.actions = [];
10095
+ this.actions = new Set();
10008
10096
  }
10009
10097
  /**
10010
10098
  * Registers a callback invoked whenever a matching element is detected as added to the DOM.
@@ -10016,7 +10104,7 @@
10016
10104
  * @param action - Function executed with the newly detected element.
10017
10105
  */
10018
10106
  onDetect(action) {
10019
- this.actions.push(action);
10107
+ this.actions.add(action);
10020
10108
  }
10021
10109
  /**
10022
10110
  * Clears all registered detection callbacks.
@@ -10025,7 +10113,7 @@
10025
10113
  * to scan mutations but will not invoke any listeners until new callbacks are registered.
10026
10114
  */
10027
10115
  clearDetect() {
10028
- this.actions = [];
10116
+ this.actions.clear();
10029
10117
  }
10030
10118
  /**
10031
10119
  * Starts observing the document for additions of elements matching the given tag name.
@@ -10099,7 +10187,9 @@
10099
10187
  * @internal
10100
10188
  */
10101
10189
  handle(element) {
10102
- this.actions.forEach((action) => action(element));
10190
+ for (const action of this.actions) {
10191
+ action(element);
10192
+ }
10103
10193
  }
10104
10194
  }
10105
10195
 
@@ -10805,7 +10895,7 @@
10805
10895
  if (typeof globalThis.GLOBAL_SEUI == "undefined") {
10806
10896
  const SECLASS = new Selective();
10807
10897
  globalThis.GLOBAL_SEUI = {
10808
- version: "1.2.6",
10898
+ version: "1.3.0",
10809
10899
  name: "SelectiveUI",
10810
10900
  bind: SECLASS.bind.bind(SECLASS),
10811
10901
  find: SECLASS.find.bind(SECLASS),
@@ -10838,7 +10928,7 @@
10838
10928
  init();
10839
10929
  }
10840
10930
  }
10841
- console.log(`[${"SelectiveUI"}] v${"1.2.6"} loaded successfully`);
10931
+ console.log(`[${"SelectiveUI"}] v${"1.3.0"} loaded successfully`);
10842
10932
  }
10843
10933
  else {
10844
10934
  console.warn(`[${globalThis.GLOBAL_SEUI.name}] Already loaded (v${globalThis.GLOBAL_SEUI.version}). ` +