selective-ui 1.2.2 → 1.2.3

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.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "An overlay for the HTML select element.",
5
5
  "author": "Huỳnh Công Xuân Mai",
6
6
  "license": "MIT",
@@ -180,23 +180,19 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
180
180
  optionViewer.view.tags.LabelContent.innerHTML = optionModel.text;
181
181
 
182
182
  if (!optionModel.isInit) {
183
- optionViewer.view.tags.OptionView.addEventListener("click", (ev: MouseEvent) => {
183
+ optionViewer.view.tags.OptionView.addEventListener("click", async (ev: MouseEvent) => {
184
184
  ev.stopPropagation();
185
185
  ev.preventDefault();
186
186
 
187
187
  if (this.isSkipEvent) return;
188
188
 
189
189
  if (this.isMultiple) {
190
- this.changingProp("select");
191
- setTimeout(() => {
192
- optionModel.selected = !optionModel.selected;
193
- }, 5);
190
+ await this.changingProp("select");
191
+ optionModel.selected = !optionModel.selected;
194
192
  } else if (optionModel.selected !== true) {
195
- this.changingProp("select");
196
- setTimeout(() => {
197
- if (this.selectedItemSingle) this.selectedItemSingle.selected = false;
198
- optionModel.selected = true;
199
- }, 5);
193
+ await this.changingProp("select");
194
+ if (this.selectedItemSingle) this.selectedItemSingle.selected = false;
195
+ optionModel.selected = true;
200
196
  }
201
197
  });
202
198
 
@@ -233,11 +229,11 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
233
229
  *
234
230
  * @param {Array<GroupModel|OptionModel>} items - The new collection of items to be displayed.
235
231
  */
236
- override setItems(items: MixedItem[]): void {
237
- this.changingProp("items", items);
232
+ override async setItems(items: MixedItem[]): Promise<void> {
233
+ await this.changingProp("items", items);
238
234
  this.items = items;
239
235
  this.buildFlatStructure();
240
- this.changeProp("items", items);
236
+ await this.changeProp("items", items);
241
237
  }
242
238
 
243
239
  /**
@@ -245,8 +241,8 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
245
241
  *
246
242
  * @param {Array<GroupModel|OptionModel>} items - The new collection of items to sync.
247
243
  */
248
- override syncFromSource(items: MixedItem[]): void {
249
- this.setItems(items);
244
+ override async syncFromSource(items: MixedItem[]): Promise<void> {
245
+ await this.setItems(items);
250
246
  }
251
247
 
252
248
  /**
@@ -125,14 +125,12 @@ export class AccessoryBox {
125
125
  role: "button",
126
126
  ariaLabel: `${this.options!.textAccessoryDeselect}${modelData.textContent}`,
127
127
  title: `${this.options!.textAccessoryDeselect}${modelData.textContent}`,
128
- onclick: (evt: MouseEvent) => {
128
+ onclick: async (evt: MouseEvent) => {
129
129
  evt.preventDefault();
130
- this.modelManager?.triggerChanging?.(
130
+ await this.modelManager?.triggerChanging?.(
131
131
  "select",
132
132
  );
133
- setTimeout(() => {
134
- modelData.selected = false;
135
- }, 10);
133
+ modelData.selected = false;
136
134
  },
137
135
  },
138
136
  },
@@ -187,7 +187,7 @@ export class Popup {
187
187
  this.updateEmptyState(stats ?? undefined);
188
188
 
189
189
  this.triggerResize();
190
- }, 200);
190
+ }, this.options.animationtime);
191
191
  }
192
192
 
193
193
  /**
@@ -240,13 +240,6 @@ export class SelectBox {
240
240
  }
241
241
  };
242
242
 
243
- // Custom event (manual refresh)
244
- select.addEventListener("options:changed", () => {
245
- optionModelManager.update(Libs.parseSelectToArray(select));
246
- this.getAction()?.refreshMask();
247
- container.popup?.triggerResize?.();
248
- });
249
-
250
243
  // AJAX setup (if provided)
251
244
  if (options.ajax) {
252
245
  searchController.setAjax(options.ajax);
@@ -64,7 +64,7 @@ export class Adapter<
64
64
  * @param {Function} callback - Function to execute before the property changes.
65
65
  */
66
66
  public onPropChanging(propName: string, callback: (...args: unknown[]) => void): void {
67
- Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce: 1 });
67
+ Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce: 0 });
68
68
  }
69
69
 
70
70
  /**
@@ -75,7 +75,7 @@ export class Adapter<
75
75
  * @param {Function} callback - Function to execute after the property changes.
76
76
  */
77
77
  public onPropChanged(propName: string, callback: (...args: unknown[]) => void): void {
78
- Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback);
78
+ Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback, { debounce: 0 });
79
79
  }
80
80
 
81
81
  /**
@@ -85,8 +85,8 @@ export class Adapter<
85
85
  * @param {string} propName - The property name to emit (e.g., "items").
86
86
  * @param {...any} params - Parameters forwarded to the callbacks.
87
87
  */
88
- public changeProp(propName: string, ...params: unknown[]): void {
89
- Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params);
88
+ public changeProp(propName: string, ...params: unknown[]): Promise<void> {
89
+ return Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params) as Promise<void>;
90
90
  }
91
91
 
92
92
  /**
@@ -96,8 +96,8 @@ export class Adapter<
96
96
  * @param {string} propName - The property name to emit (e.g., "items").
97
97
  * @param {...any} params - Parameters forwarded to the callbacks.
98
98
  */
99
- public changingProp(propName: string, ...params: unknown[]): void {
100
- Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params);
99
+ public changingProp(propName: string, ...params: unknown[]): Promise<void> {
100
+ return Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params) as Promise<void>;
101
101
  }
102
102
 
103
103
  /**
@@ -129,10 +129,10 @@ export class Adapter<
129
129
  *
130
130
  * @param {TItem[]} items - The new list of items to set.
131
131
  */
132
- public setItems(items: TItem[]): void {
133
- this.changingProp("items", items);
132
+ public async setItems(items: TItem[]): Promise<void> {
133
+ await this.changingProp("items", items);
134
134
  this.items = items;
135
- this.changeProp("items", items);
135
+ await this.changeProp("items", items);
136
136
  }
137
137
 
138
138
  /**
@@ -141,8 +141,8 @@ export class Adapter<
141
141
  *
142
142
  * @param {TItem[]} items - The source list of items to synchronize.
143
143
  */
144
- public syncFromSource(items: TItem[]): void {
145
- this.setItems(items);
144
+ public async syncFromSource(items: TItem[]): Promise<void> {
145
+ await this.setItems(items);
146
146
  }
147
147
 
148
148
  /**
@@ -384,7 +384,7 @@ export class VirtualRecyclerView<
384
384
  private containerTopInScroll(): number {
385
385
  const a = this.viewElement!.getBoundingClientRect();
386
386
  const b = this.scrollEl.getBoundingClientRect();
387
- return a.top - b.top + this.scrollEl.scrollTop;
387
+ return Math.max(0, a.top - b.top + this.scrollEl.scrollTop);
388
388
  }
389
389
 
390
390
  /**
@@ -670,7 +670,10 @@ export class VirtualRecyclerView<
670
670
  const maxScroll = Math.max(0, this.scrollEl.scrollHeight - this.scrollEl.clientHeight);
671
671
  const clamped = Math.min(Math.max(0, targetScroll), maxScroll);
672
672
 
673
- if (Math.abs(this.scrollEl.scrollTop - clamped) > 0.5) {
673
+ const heightChanged = Math.abs(anchorTopNew - anchorTop) > 1;
674
+ const scrollDiff = Math.abs(this.scrollEl.scrollTop - clamped);
675
+
676
+ if (heightChanged && scrollDiff > 0.5 && scrollDiff < 100) {
674
677
  this.scrollEl.scrollTop = clamped;
675
678
  }
676
679
  } finally {
@@ -29,6 +29,8 @@ export class ModelManager<
29
29
 
30
30
  private options: SelectiveOptions = null;
31
31
 
32
+ private oldPosition = 0;
33
+
32
34
  /**
33
35
  * Constructs a ModelManager with configuration options used by created models and components.
34
36
  *
@@ -142,13 +144,13 @@ export class ModelManager<
142
144
  *
143
145
  * @param {Array<HTMLOptGroupElement|HTMLOptionElement>} modelData - New source elements to rebuild models from.
144
146
  */
145
- public replace(modelData: Array<HTMLOptGroupElement | HTMLOptionElement>): void {
147
+ public async replace(modelData: Array<HTMLOptGroupElement | HTMLOptionElement>): Promise<void> {
146
148
  this.lastFingerprint = null;
147
149
  this.createModelResources(modelData);
148
150
 
149
151
  if (this.privAdapterHandle) {
150
152
  // Adapter expects TModel[], but this manager's list is GroupModel|OptionModel.
151
- this.privAdapterHandle.syncFromSource(this.privModelList as unknown as TModel[]);
153
+ await this.privAdapterHandle.syncFromSource(this.privModelList as unknown as TModel[]);
152
154
  }
153
155
 
154
156
  this.refresh(false);
@@ -273,6 +275,11 @@ export class ModelManager<
273
275
  });
274
276
 
275
277
  let isUpdate = true;
278
+ if (this.oldPosition == 0) {
279
+ isUpdate = false;
280
+ }
281
+ this.oldPosition = position;
282
+
276
283
  oldGroupMap.forEach((removedGroup) => {
277
284
  isUpdate = false;
278
285
  removedGroup.remove();
@@ -289,7 +296,7 @@ export class ModelManager<
289
296
  this.privAdapterHandle.updateData(this.privModelList as unknown as TModel[]);
290
297
  }
291
298
 
292
- this.onUpdated();
299
+ // this.onUpdated();
293
300
  this.refresh(isUpdate);
294
301
  }
295
302
 
@@ -340,15 +347,15 @@ export class ModelManager<
340
347
  * Triggers the adapter's pre-change pipeline for a named event,
341
348
  * enabling observers to react before a change is applied.
342
349
  */
343
- public triggerChanging(event_name: string): void {
344
- this.privAdapterHandle?.changingProp(event_name);
350
+ public triggerChanging(event_name: string): Promise<void> {
351
+ return this.privAdapterHandle?.changingProp(event_name);
345
352
  }
346
353
 
347
354
  /**
348
355
  * Triggers the adapter's post-change pipeline for a named event,
349
356
  * notifying observers after a change has been applied.
350
357
  */
351
- public triggerChanged(event_name: string): void {
352
- this.privAdapterHandle?.changeProp(event_name);
358
+ public triggerChanged(event_name: string): Promise<void> {
359
+ return this.privAdapterHandle?.changeProp(event_name);
353
360
  }
354
361
  }
@@ -467,7 +467,5 @@ export class SearchController {
467
467
  select.appendChild(option);
468
468
  }
469
469
  });
470
-
471
- select.dispatchEvent(new CustomEvent("options:changed"));
472
470
  }
473
471
  }
@@ -326,7 +326,7 @@ class EffectorImpl implements EffectorInterface {
326
326
  this.element.style.transition = `top ${duration}ms ease-out, height ${duration}ms ease-out, max-height ${duration}ms ease-out;`;
327
327
  }
328
328
 
329
- setTimeout(() => {
329
+ requestAnimationFrame(() => {
330
330
  const styles: Partial<CSSStyleDeclaration> & Record<string, string> = {
331
331
  width: `${width}px`,
332
332
  left: `${left}px`,
@@ -360,7 +360,7 @@ class EffectorImpl implements EffectorInterface {
360
360
  if (isPositionChanged) delete this.element.style.transition;
361
361
  onComplete?.();
362
362
  }
363
- }, 20);
363
+ });
364
364
 
365
365
  return this;
366
366
  }
@@ -16,7 +16,7 @@ export class SelectObserver {
16
16
 
17
17
  /**
18
18
  * Initializes the SelectObserver for a given <select> element.
19
- * Captures the initial snapshot, sets up a MutationObserver, and listens for custom "options:changed" events.
19
+ * Captures the initial snapshot, sets up a MutationObserver.
20
20
  * Changes are debounced to prevent excessive calls.
21
21
  *
22
22
  * @param {HTMLSelectElement} select - The <select> element to observe.
@@ -31,13 +31,6 @@ export class SelectObserver {
31
31
  this.handleChange();
32
32
  }, this._DEBOUNCE_DELAY);
33
33
  });
34
-
35
- select.addEventListener("options:changed", () => {
36
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
37
- this.debounceTimer = setTimeout(() => {
38
- this.handleChange();
39
- }, this._DEBOUNCE_DELAY);
40
- });
41
34
  }
42
35
 
43
36
  /**
@@ -84,12 +84,18 @@ export class CallbackScheduler {
84
84
  * - If an entry has `once = true`, it is removed after execution by setting its slot to `undefined`.
85
85
  * (The list is not spliced to preserve indices.)
86
86
  */
87
- public run(key: TimerKey, ...params: any[]): void {
87
+ public run(key: TimerKey, ...params: any[]): Promise<void> | void {
88
88
  const executes = this.executeStored.get(key);
89
- if (!executes) return;
89
+ if (!executes || executes.length === 0) {
90
+ return Promise.resolve();
91
+ }
92
+
93
+ if (!this.timerRunner.has(key)) {
94
+ this.timerRunner.set(key, new Map());
95
+ }
90
96
 
91
- if (!this.timerRunner.has(key)) this.timerRunner.set(key, new Map());
92
97
  const runner = this.timerRunner.get(key)!;
98
+ const tasks: Promise<void>[] = [];
93
99
 
94
100
  for (let i = 0; i < executes.length; i++) {
95
101
  const entry = executes[i];
@@ -98,22 +104,36 @@ export class CallbackScheduler {
98
104
  const prev = runner.get(i);
99
105
  if (prev) clearTimeout(prev);
100
106
 
101
- const timer = setTimeout(() => {
102
- entry.callback(params.length > 0 ? params : null);
103
-
104
- if (entry.once) {
105
- // Preserve index stability by leaving an empty slot.
106
- executes[i] = undefined;
107
-
108
- // Cleanup the timer handle for this index.
109
- const current = runner.get(i);
110
- if (current) clearTimeout(current);
111
- runner.delete(i);
112
- }
113
- }, entry.timeout);
114
-
115
- runner.set(i, timer);
107
+ const task = new Promise<void>((resolve) => {
108
+ const timer = setTimeout(async () => {
109
+ try {
110
+ const resp = entry.callback(
111
+ params.length > 0 ? params : null
112
+ ) as any;
113
+
114
+ if (resp instanceof Promise) {
115
+ await resp;
116
+ }
117
+ } catch {} finally {
118
+ if (entry.once) {
119
+ executes[i] = undefined;
120
+
121
+ const current = runner.get(i);
122
+ if (current) clearTimeout(current);
123
+ runner.delete(i);
124
+ }
125
+
126
+ resolve();
127
+ }
128
+ }, entry.timeout);
129
+
130
+ runner.set(i, timer);
131
+ });
132
+
133
+ tasks.push(task);
116
134
  }
135
+
136
+ return Promise.all(tasks).then(() => void 0);
117
137
  }
118
138
 
119
139
  /**