selective-ui 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selective-ui",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "An overlay for the HTML select element.",
5
5
  "author": "Huỳnh Công Xuân Mai",
6
6
  "license": "MIT",
@@ -137,13 +137,7 @@ export class SelectBox {
137
137
  tag: {
138
138
  node: "div",
139
139
  classList: "selective-ui-view",
140
- tabIndex: 0,
141
- role: "combobox",
142
- ariaExpanded: "false",
143
- ariaLabelledby: options.SEID_HOLDER,
144
- ariaControls: options.SEID_LIST,
145
- ariaHaspopup: "true",
146
- ariaMultiselectable: options.multiple ? "true" : "false",
140
+ tabIndex: 0,
147
141
  onkeydown: (e) => {
148
142
  if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
149
143
  e.preventDefault();
@@ -463,10 +457,21 @@ export class SelectBox {
463
457
  },
464
458
  setValue(evtToken = null, value, trigger = true, force = false) {
465
459
  !Array.isArray(value) && (value = [value]);
460
+
461
+ value = value.filter(v => v !== "" && v != null);
462
+
463
+ if (value.length === 0) {
464
+ superThis.getModelOption().forEach(modelOption => {
465
+ modelOption["selectedNonTrigger"] = false;
466
+ });
467
+ this.change(false, trigger);
468
+ return;
469
+ }
466
470
 
467
471
  if (bindedOptions.multiple && bindedOptions.maxSelected > 0) {
468
472
  if (value.length > bindedOptions.maxSelected) {
469
- return
473
+ console.warn(`Cannot select more than ${bindedOptions.maxSelected} items`);
474
+ return;
470
475
  }
471
476
  }
472
477
 
@@ -474,12 +479,58 @@ export class SelectBox {
474
479
  return;
475
480
  }
476
481
 
482
+ if (container.searchController?.isAjax()) {
483
+ const { existing, missing } = container.searchController.checkMissingValues(value);
484
+
485
+ if (missing.length > 0) {
486
+ console.log(`Loading ${missing.length} missing values from server...`);
487
+
488
+ (async () => {
489
+ if (bindedOptions.loadingfield) {
490
+ container.popup?.showLoading();
491
+ }
492
+
493
+ try {
494
+ const result = await container.searchController.loadByValues(missing);
495
+
496
+ if (result.success && result.items.length > 0) {
497
+ result.items.forEach(item => {
498
+ if (missing.includes(item.value)) {
499
+ item.selected = true;
500
+ }
501
+ });
502
+
503
+ container.searchController['#applyAjaxResult'](
504
+ result.items,
505
+ true,
506
+ true
507
+ );
508
+
509
+ setTimeout(() => {
510
+ superThis.getModelOption().forEach(modelOption => {
511
+ modelOption["selectedNonTrigger"] = value.some(v => v == modelOption["value"]);
512
+ });
513
+ this.change(false, false);
514
+ }, 100);
515
+ } else if (missing.length > 0) {
516
+ console.warn(`Could not load ${missing.length} values:`, missing);
517
+ }
518
+ } catch (error) {
519
+ console.error("Error loading missing values:", error);
520
+ } finally {
521
+ if (bindedOptions.loadingfield) {
522
+ container.popup?.hideLoading();
523
+ }
524
+ }
525
+ })();
526
+ }
527
+ }
528
+
477
529
  if (trigger) {
478
530
  const beforeChangeToken = iEvents.callEvent([this], ...bindedOptions.on.beforeChange);
479
531
  if (beforeChangeToken.isCancel) {
480
532
  return;
481
533
  }
482
-
483
534
  superThis.oldValue = this.value;
484
535
  }
485
536
 
@@ -487,7 +538,7 @@ export class SelectBox {
487
538
  modelOption["selectedNonTrigger"] = value.some(v => v == modelOption["value"]);
488
539
  });
489
540
 
490
- if (!bindedOptions.multiple){
541
+ if (!bindedOptions.multiple && value.length > 0) {
491
542
  container.targetElement.value = value[0];
492
543
  }
493
544
 
@@ -539,8 +590,14 @@ export class SelectBox {
539
590
 
540
591
  container.popup.open();
541
592
  container.searchbox.show();
542
-
543
- container.tags.ViewPanel.setAttribute("aria-expanded", "true");
593
+ const ViewPanel = /** @type {HTMLElement} */ (container.tags.ViewPanel);
594
+ ViewPanel.setAttribute("aria-expanded", "true");
595
+ ViewPanel.setAttribute("aria-controls", bindedOptions.SEID_LIST);
596
+ ViewPanel.setAttribute("aria-haspopup", "listbox");
597
+ ViewPanel.setAttribute("aria-labelledby", bindedOptions.SEID_HOLDER);
598
+ if (bindedOptions.multiple) {
599
+ ViewPanel.setAttribute("aria-multiselectable", "true");
600
+ }
544
601
 
545
602
  iEvents.callEvent([this], ...bindedOptions.on.show);
546
603
 
@@ -46,6 +46,88 @@ export class SearchController {
46
46
  return !(!this.#ajaxConfig);
47
47
  }
48
48
 
49
+ /**
50
+ * Load specific options by their values from server
51
+ * @param {string|string[]} values - Values to load
52
+ * @returns {Promise<{success: boolean, items: Array, message?: string}>}
53
+ */
54
+ async loadByValues(values) {
55
+ if (!this.#ajaxConfig) {
56
+ return { success: false, items: [], message: "Ajax not configured" };
57
+ }
58
+
59
+ const valuesArray = Array.isArray(values) ? values : [values];
60
+ if (valuesArray.length === 0) {
61
+ return { success: true, items: [] };
62
+ }
63
+
64
+ try {
65
+ const cfg = this.#ajaxConfig;
66
+
67
+ let payload;
68
+ if (typeof cfg.dataByValues === "function") {
69
+ payload = cfg.dataByValues(valuesArray);
70
+ } else {
71
+ payload = {
72
+ values: valuesArray.join(","),
73
+ load_by_values: "1",
74
+ ...(typeof cfg.data === "function" ? cfg.data("", 0) : (cfg.data || {}))
75
+ };
76
+ }
77
+
78
+ let response;
79
+ if (cfg.method === "POST") {
80
+ const formData = new URLSearchParams();
81
+ Object.keys(payload).forEach(key => {
82
+ formData.append(key, payload[key]);
83
+ });
84
+
85
+ response = await fetch(cfg.url, {
86
+ method: "POST",
87
+ body: formData,
88
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
89
+ });
90
+ } else {
91
+ const params = new URLSearchParams(payload).toString();
92
+ response = await fetch(`${cfg.url}?${params}`);
93
+ }
94
+
95
+ if (!response.ok) {
96
+ throw new Error(`HTTP error! status: ${response.status}`);
97
+ }
98
+
99
+ const data = await response.json();
100
+ const result = this.#parseResponse(data);
101
+
102
+ return {
103
+ success: true,
104
+ items: result.items
105
+ };
106
+ } catch (error) {
107
+ console.error("Load by values error:", error);
108
+ return {
109
+ success: false,
110
+ message: error.message,
111
+ items: []
112
+ };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Check if values exist in current options
118
+ * @param {string[]} values - Values to check
119
+ * @returns {{existing: string[], missing: string[]}}
120
+ */
121
+ checkMissingValues(values) {
122
+ const allOptions = Array.from(this.#select.options);
123
+ const existingValues = allOptions.map(opt => opt.value);
124
+
125
+ const existing = values.filter(v => existingValues.includes(v));
126
+ const missing = values.filter(v => !existingValues.includes(v));
127
+
128
+ return { existing, missing };
129
+ }
130
+
49
131
  /**
50
132
  * Configures AJAX settings used for remote searching and pagination.
51
133
  *
package/src/js/index.js CHANGED
@@ -26,7 +26,7 @@ import { checkDuplicate, markLoaded } from "./utils/guard";
26
26
  import { Libs } from "./utils/libs";
27
27
  import { Effector } from "./services/effector";
28
28
 
29
- export const version = "1.0.3";
29
+ export const version = "1.0.4";
30
30
  export const name = "SelectiveUI";
31
31
 
32
32
  const alreadyLoaded = checkDuplicate(name);