selective-ui 1.2.0 → 1.2.1

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.0",
3
+ "version": "1.2.1",
4
4
  "description": "An overlay for the HTML select element.",
5
5
  "author": "Huỳnh Công Xuân Mai",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@ import { OptionView } from "../views/option-view";
6
6
  import { MixedItem, VisibilityStats } from "../types/core/base/mixed-adapter.type";
7
7
  import { IEventCallback } from "../types/utils/ievents.type";
8
8
  import { ImagePosition, LabelHalign, LabelValign } from "../types/views/view.option.type";
9
+ import { Libs } from "../utils/libs";
9
10
 
10
11
  /**
11
12
  * @extends {Adapter<GroupModel|OptionModel>}
@@ -25,6 +26,21 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
25
26
  constructor(items: MixedItem[] = []) {
26
27
  super(items);
27
28
  this._buildFlatStructure();
29
+
30
+ Libs.callbackScheduler.on(`sche_vis_${this.adapterKey}`, () => {
31
+ const visibleCount = this.flatOptions.filter((item) => item.visible).length;
32
+ const totalCount = this.flatOptions.length;
33
+
34
+ this._visibilityChangedCallbacks.forEach((callback) => {
35
+ callback({
36
+ visibleCount,
37
+ totalCount,
38
+ hasVisible: visibleCount > 0,
39
+ isEmpty: totalCount === 0,
40
+ });
41
+ });
42
+ Libs.callbackScheduler.run(`sche_vis_proxy_${this.adapterKey}`);
43
+ }, {debounce: 10});
28
44
  }
29
45
 
30
46
  /**
@@ -289,17 +305,7 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
289
305
  * Computes visible and total counts, then emits aggregated state.
290
306
  */
291
307
  private _notifyVisibilityChanged(): void {
292
- const visibleCount = this.flatOptions.filter((item) => item.visible).length;
293
- const totalCount = this.flatOptions.length;
294
-
295
- this._visibilityChangedCallbacks.forEach((callback) => {
296
- callback({
297
- visibleCount,
298
- totalCount,
299
- hasVisible: visibleCount > 0,
300
- isEmpty: totalCount === 0,
301
- });
302
- });
308
+ Libs.callbackScheduler.run(`sche_vis_${this.adapterKey}`);
303
309
  }
304
310
 
305
311
  /**
@@ -251,6 +251,7 @@ export class SelectBox {
251
251
  }
252
252
 
253
253
  const optionAdapter = container.popup!.optionAdapter as MixedAdapter;
254
+ let hightlightTimer : ReturnType<typeof setTimeout> | null = null;
254
255
 
255
256
  const searchHandle = (keyword: string, isTrigger: boolean) => {
256
257
  if (!isTrigger && keyword === "") {
@@ -261,13 +262,17 @@ export class SelectBox {
261
262
  searchController
262
263
  .search(keyword)
263
264
  .then((result: any) => {
264
- container.popup?.triggerResize?.();
265
- if (result?.hasResults) {
266
- setTimeout(() => {
267
- container.popup?.triggerResize?.();
268
- optionAdapter.resetHighlight();
269
- }, options.animationtime ? options.animationtime + 10 : 0);
270
- }
265
+ clearTimeout(hightlightTimer!);
266
+ Libs.callbackScheduler.on(`sche_vis_proxy_${optionAdapter.adapterKey}`, () => {
267
+ container.popup?.triggerResize?.();
268
+
269
+ if (result?.hasResults) {
270
+ hightlightTimer = setTimeout(() => {
271
+ optionAdapter.resetHighlight();
272
+ container.popup?.triggerResize?.();
273
+ }, options.animationtime ?? 0);
274
+ }
275
+ }, { debounce: 10 });
271
276
  })
272
277
  .catch((error: unknown) => {
273
278
  console.error("Search error:", error);
@@ -279,15 +284,17 @@ export class SelectBox {
279
284
 
280
285
  searchbox.onSearch = (keyword: string, isTrigger: boolean) => {
281
286
  if (!searchController.compareSearchTrigger(keyword)) return;
287
+ if (searchHandleTimer) clearTimeout(searchHandleTimer);
282
288
 
283
289
  if (searchController.isAjax()) {
284
- if (searchHandleTimer) clearTimeout(searchHandleTimer);
285
290
  container.popup?.showLoading?.();
286
291
  searchHandleTimer = setTimeout(() => {
287
292
  searchHandle(keyword, isTrigger);
288
293
  }, options.delaysearchtime ?? 0);
289
294
  } else {
290
- searchHandle(keyword, isTrigger);
295
+ searchHandleTimer = setTimeout(() => {
296
+ searchHandle(keyword, isTrigger);
297
+ }, 10);
291
298
  }
292
299
  };
293
300
 
@@ -696,6 +696,7 @@ export class VirtualRecyclerView<
696
696
  }
697
697
 
698
698
  const item = this.adapter!.items[index];
699
+ if (!item) return;
699
700
  const existing = this.created.get(index);
700
701
 
701
702
  if (existing) {
@@ -223,10 +223,7 @@ export class SearchController {
223
223
  let hasVisibleItems = false;
224
224
 
225
225
  flatOptions.forEach((opt) => {
226
- const text = String(opt.textContent ?? opt.text ?? "").toLowerCase();
227
- const textNA = Libs.string2normalize(text);
228
-
229
- const isVisible = lower === "" || text.includes(lower) || textNA.includes(lowerNA);
226
+ const isVisible = lower === "" || opt.textToFind.includes(lowerNA);
230
227
 
231
228
  opt.visible = isVisible;
232
229
  if (isVisible) hasVisibleItems = true;
@@ -6,12 +6,12 @@ import { OptionView } from "../views/option-view";
6
6
  import type { IEventCallback } from "../types/utils/ievents.type";
7
7
  import type { OptionViewTags } from "../types/views/view.option.type";
8
8
  import type { GroupModel } from "./group-model";
9
- import { DefaultConfig } from "../types/utils/istorage.type";
9
+ import { SelectiveOptions } from "../types/utils/selective.type";
10
10
 
11
11
  /**
12
- * @extends {Model<HTMLOptionElement, OptionViewTags, OptionView>}
12
+ * @extends {Model<HTMLOptionElement, OptionViewTags, OptionView, SelectiveOptions>}
13
13
  */
14
- export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, OptionView, DefaultConfig> {
14
+ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, OptionView, SelectiveOptions> {
15
15
  private _privOnSelected: Array<(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void> = [];
16
16
 
17
17
  private _privOnInternalSelected: Array<(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void> = [];
@@ -24,6 +24,22 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
24
24
 
25
25
  group: GroupModel | null = null;
26
26
 
27
+ /**
28
+ * Constructs a Model instance with configuration options and optional bindings to a target element and view.
29
+ * Stores references for later updates and rendering.
30
+ *
31
+ * @param {SelectiveOptions} options - Configuration options for the model.
32
+ * @param {HTMLOptionElement|null} [targetElement=null] - The underlying element (e.g., <option> or group node).
33
+ * @param {OptionView|null} [view=null] - The associated view responsible for rendering the model.
34
+ */
35
+ constructor(options: SelectiveOptions, targetElement: HTMLOptionElement | null = null, view: OptionView | null = null) {
36
+ super(options, targetElement, view);
37
+
38
+ (async () => {
39
+ this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
40
+ })();
41
+ }
42
+
27
43
  /**
28
44
  * Returns the image source from dataset (imgsrc or image), or an empty string if absent.
29
45
  *
@@ -149,6 +165,8 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
149
165
  return this.options.allowHtml ? Libs.stripHtml(this.text).trim() : this.text.trim();
150
166
  }
151
167
 
168
+ textToFind: string;
169
+
152
170
  /**
153
171
  * Returns the dataset object of the underlying <option> element, or an empty object.
154
172
  *
@@ -214,6 +232,7 @@ export class OptionModel extends Model<HTMLOptionElement, OptionViewTags, Option
214
232
  * and synchronizes initial selected state to the view.
215
233
  */
216
234
  onTargetChanged(): void {
235
+ this.textToFind = Libs.string2normalize(this.textContent.toLowerCase());
217
236
  if (!this.view) return;
218
237
 
219
238
  const labelContent = this.view.view.tags.LabelContent;
@@ -301,7 +301,7 @@ class EffectorImpl implements EffectorInterface {
301
301
  resize(config: ResizeConfig): this {
302
302
  if (!this.element) return this;
303
303
 
304
- if (this._resizeTimeout) clearTimeout(this._resizeTimeout);
304
+ this.cancel();
305
305
 
306
306
  const {
307
307
  duration = 200,
@@ -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
- requestAnimationFrame(() => {
329
+ setTimeout(() => {
330
330
  const styles: Partial<CSSStyleDeclaration> & Record<string, string> = {
331
331
  width: `${width}px`,
332
332
  left: `${left}px`,
@@ -343,7 +343,7 @@ class EffectorImpl implements EffectorInterface {
343
343
  } else {
344
344
  this._resizeTimeout = setTimeout(() => {
345
345
  if (this.element?.style) {
346
- this.element.style.transition = "none";
346
+ this.element.style.transition = null;
347
347
  }
348
348
  }, duration);
349
349
  }
@@ -352,7 +352,7 @@ class EffectorImpl implements EffectorInterface {
352
352
 
353
353
  if (animate && (isPositionChanged || heightDiff > 1)) {
354
354
  this._resizeTimeout = setTimeout(() => {
355
- this.element.style.transition = "none";
355
+ this.element.style.transition = null;
356
356
  if (isPositionChanged) delete this.element.style.transition;
357
357
  onComplete?.();
358
358
  }, duration);
@@ -360,7 +360,7 @@ class EffectorImpl implements EffectorInterface {
360
360
  if (isPositionChanged) delete this.element.style.transition;
361
361
  onComplete?.();
362
362
  }
363
- });
363
+ }, 20);
364
364
 
365
365
  return this;
366
366
  }