selective-ui 1.1.6 → 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/dist/selective-ui.css +7 -1
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +794 -57
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +2 -2
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.css +1 -1
- package/dist/selective-ui.min.css.br +0 -0
- package/dist/selective-ui.min.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +795 -58
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/css/components/popup.css +7 -1
- package/src/ts/adapter/mixed-adapter.ts +29 -18
- package/src/ts/components/empty-state.ts +5 -4
- package/src/ts/components/loading-state.ts +4 -4
- package/src/ts/components/option-handle.ts +4 -4
- package/src/ts/components/popup.ts +35 -6
- package/src/ts/components/searchbox.ts +2 -0
- package/src/ts/components/selectbox.ts +23 -9
- package/src/ts/core/base/adapter.ts +8 -5
- package/src/ts/core/base/model.ts +19 -1
- package/src/ts/core/base/recyclerview.ts +3 -1
- package/src/ts/core/base/virtual-recyclerview.ts +763 -0
- package/src/ts/core/model-manager.ts +24 -16
- package/src/ts/core/search-controller.ts +5 -8
- package/src/ts/models/option-model.ts +22 -3
- package/src/ts/services/effector.ts +7 -7
- package/src/ts/types/components/state.box.type.ts +1 -18
- package/src/ts/types/core/base/adapter.type.ts +14 -0
- package/src/ts/types/core/base/model.type.ts +5 -0
- package/src/ts/types/core/base/recyclerview.type.ts +3 -1
- package/src/ts/types/core/base/view.type.ts +6 -0
- package/src/ts/types/core/base/virtual-recyclerview.type.ts +66 -0
- package/src/ts/types/utils/istorage.type.ts +1 -0
- package/src/ts/utils/istorage.ts +3 -2
- package/src/ts/utils/libs.ts +26 -25
- package/src/ts/utils/selective.ts +7 -7
- package/src/ts/views/option-view.ts +8 -8
|
@@ -3,6 +3,7 @@ import { GroupModel } from "../models/group-model";
|
|
|
3
3
|
import { OptionModel } from "../models/option-model";
|
|
4
4
|
import { ModelContract } from "../types/core/base/model.type";
|
|
5
5
|
import { RecyclerViewContract } from "../types/core/base/recyclerview.type";
|
|
6
|
+
import { ViewContract } from "../types/core/base/view.type";
|
|
6
7
|
import { SelectiveOptions } from "../types/utils/selective.type";
|
|
7
8
|
import { Adapter } from "./base/adapter";
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ import { Adapter } from "./base/adapter";
|
|
|
12
13
|
*/
|
|
13
14
|
export class ModelManager<
|
|
14
15
|
TModel extends ModelContract<any, any>,
|
|
15
|
-
TAdapter extends Adapter<TModel
|
|
16
|
+
TAdapter extends Adapter<TModel, ViewContract<any>>
|
|
16
17
|
> {
|
|
17
18
|
private _privModelList: Array<GroupModel | OptionModel> = [];
|
|
18
19
|
|
|
@@ -120,7 +121,7 @@ export class ModelManager<
|
|
|
120
121
|
const optionEl = data as HTMLOptionElement;
|
|
121
122
|
const optionModel = new OptionModel(this.options, optionEl);
|
|
122
123
|
|
|
123
|
-
const parentGroup =
|
|
124
|
+
const parentGroup = optionEl["__parentGroup"] as HTMLOptGroupElement | undefined;
|
|
124
125
|
|
|
125
126
|
if (parentGroup && currentGroup && parentGroup === currentGroup.targetElement) {
|
|
126
127
|
currentGroup.addItem(optionModel);
|
|
@@ -150,7 +151,7 @@ export class ModelManager<
|
|
|
150
151
|
this._privAdapterHandle.syncFromSource(this._privModelList as unknown as TModel[]);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
this.refresh();
|
|
154
|
+
this.refresh(false);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
|
@@ -159,25 +160,27 @@ export class ModelManager<
|
|
|
159
160
|
*/
|
|
160
161
|
notify(): void {
|
|
161
162
|
if (!this._privAdapterHandle) return;
|
|
162
|
-
this.refresh();
|
|
163
|
+
this.refresh(false);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
/**
|
|
166
167
|
* Initializes adapter and recycler view instances, attaches them to a container element,
|
|
167
168
|
* and applies optional configuration overrides for adapter and recyclerView.
|
|
168
169
|
*/
|
|
169
|
-
|
|
170
|
+
|
|
171
|
+
load<TExtra extends object = {}>(
|
|
170
172
|
viewElement: HTMLElement,
|
|
171
173
|
adapterOpt: Partial<TAdapter> = {},
|
|
172
|
-
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> = {}
|
|
174
|
+
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> & TExtra = {} as any
|
|
173
175
|
): void {
|
|
176
|
+
|
|
174
177
|
this._privAdapterHandle = new this._privAdapter(this._privModelList as unknown as TModel[]);
|
|
175
178
|
Object.assign(this._privAdapterHandle, adapterOpt);
|
|
176
179
|
|
|
177
180
|
this._privRecyclerViewHandle = new this._privRecyclerView(viewElement);
|
|
178
|
-
this._privRecyclerViewHandle.setAdapter(this._privAdapterHandle);
|
|
179
|
-
|
|
180
181
|
Object.assign(this._privRecyclerViewHandle, recyclerViewOpt);
|
|
182
|
+
|
|
183
|
+
this._privRecyclerViewHandle.setAdapter(this._privAdapterHandle);
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
/**
|
|
@@ -240,7 +243,7 @@ export class ModelManager<
|
|
|
240
243
|
existingOption.update(dataVset);
|
|
241
244
|
existingOption.position = position;
|
|
242
245
|
|
|
243
|
-
const parentGroup =
|
|
246
|
+
const parentGroup = dataVset["__parentGroup"] as HTMLOptGroupElement | undefined;
|
|
244
247
|
|
|
245
248
|
if (parentGroup && currentGroup) {
|
|
246
249
|
currentGroup.addItem(existingOption);
|
|
@@ -255,7 +258,7 @@ export class ModelManager<
|
|
|
255
258
|
const newOption = new OptionModel(this.options, dataVset);
|
|
256
259
|
newOption.position = position;
|
|
257
260
|
|
|
258
|
-
const parentGroup =
|
|
261
|
+
const parentGroup = dataVset["__parentGroup"] as HTMLOptGroupElement | undefined;
|
|
259
262
|
|
|
260
263
|
if (parentGroup && currentGroup) {
|
|
261
264
|
currentGroup.addItem(newOption);
|
|
@@ -269,12 +272,15 @@ export class ModelManager<
|
|
|
269
272
|
}
|
|
270
273
|
});
|
|
271
274
|
|
|
275
|
+
let isUpdate = true;
|
|
272
276
|
oldGroupMap.forEach((removedGroup) => {
|
|
273
|
-
|
|
277
|
+
isUpdate = false;
|
|
278
|
+
removedGroup.remove();
|
|
274
279
|
});
|
|
275
280
|
|
|
276
281
|
oldOptionMap.forEach((removedOption) => {
|
|
277
|
-
|
|
282
|
+
isUpdate = false;
|
|
283
|
+
removedOption.remove();
|
|
278
284
|
});
|
|
279
285
|
|
|
280
286
|
this._privModelList = newModels;
|
|
@@ -284,7 +290,7 @@ export class ModelManager<
|
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
this.onUpdated();
|
|
287
|
-
this.refresh();
|
|
293
|
+
this.refresh(isUpdate);
|
|
288
294
|
}
|
|
289
295
|
|
|
290
296
|
/**
|
|
@@ -299,16 +305,18 @@ export class ModelManager<
|
|
|
299
305
|
* @param {boolean} value - True to skip events; false to restore normal behavior.
|
|
300
306
|
*/
|
|
301
307
|
skipEvent(value: boolean): void {
|
|
302
|
-
if (this._privAdapterHandle)
|
|
308
|
+
if (this._privAdapterHandle) this._privAdapterHandle.isSkipEvent = value;
|
|
303
309
|
}
|
|
304
310
|
|
|
305
311
|
/**
|
|
306
312
|
* Re-renders the recycler view if present and invokes the post-refresh hook.
|
|
307
313
|
* No-op if the recycler view is not initialized.
|
|
314
|
+
*
|
|
315
|
+
* @param isUpdate - Indicates if this refresh is due to an update operation.
|
|
308
316
|
*/
|
|
309
|
-
refresh(): void {
|
|
317
|
+
refresh(isUpdate: boolean): void {
|
|
310
318
|
if (!this._privRecyclerViewHandle) return;
|
|
311
|
-
this._privRecyclerViewHandle.refresh();
|
|
319
|
+
this._privRecyclerViewHandle.refresh(isUpdate);
|
|
312
320
|
this.onUpdated();
|
|
313
321
|
}
|
|
314
322
|
|
|
@@ -91,7 +91,7 @@ export class SearchController {
|
|
|
91
91
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
92
92
|
});
|
|
93
93
|
} else {
|
|
94
|
-
const params = new URLSearchParams(payload
|
|
94
|
+
const params = new URLSearchParams(payload).toString();
|
|
95
95
|
response = await fetch(`${cfg.url}?${params}`);
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -168,7 +168,7 @@ export class SearchController {
|
|
|
168
168
|
clear(): void {
|
|
169
169
|
this._paginationState.currentKeyword = "";
|
|
170
170
|
|
|
171
|
-
const { modelList } = this._modelManager.getResources()
|
|
171
|
+
const { modelList } = this._modelManager.getResources();
|
|
172
172
|
const flatOptions: OptionModel[] = [];
|
|
173
173
|
|
|
174
174
|
for (const m of modelList as MixedItem[]) {
|
|
@@ -212,7 +212,7 @@ export class SearchController {
|
|
|
212
212
|
const lower = String(keyword ?? "").toLowerCase();
|
|
213
213
|
const lowerNA = Libs.string2normalize(lower);
|
|
214
214
|
|
|
215
|
-
const { modelList } = this._modelManager.getResources()
|
|
215
|
+
const { modelList } = this._modelManager.getResources();
|
|
216
216
|
|
|
217
217
|
const flatOptions: OptionModel[] = [];
|
|
218
218
|
for (const m of modelList as MixedItem[]) {
|
|
@@ -223,10 +223,7 @@ export class SearchController {
|
|
|
223
223
|
let hasVisibleItems = false;
|
|
224
224
|
|
|
225
225
|
flatOptions.forEach((opt) => {
|
|
226
|
-
const
|
|
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;
|
|
@@ -291,7 +288,7 @@ export class SearchController {
|
|
|
291
288
|
signal: this._abortController.signal,
|
|
292
289
|
});
|
|
293
290
|
} else {
|
|
294
|
-
const params = new URLSearchParams(payload
|
|
291
|
+
const params = new URLSearchParams(payload).toString();
|
|
295
292
|
response = await fetch(`${cfg.url}?${params}`, { signal: this._abortController.signal });
|
|
296
293
|
}
|
|
297
294
|
|
|
@@ -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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
346
|
+
this.element.style.transition = null;
|
|
347
347
|
}
|
|
348
348
|
}, duration);
|
|
349
349
|
}
|
|
@@ -352,15 +352,15 @@ class EffectorImpl implements EffectorInterface {
|
|
|
352
352
|
|
|
353
353
|
if (animate && (isPositionChanged || heightDiff > 1)) {
|
|
354
354
|
this._resizeTimeout = setTimeout(() => {
|
|
355
|
-
this.element.style.transition =
|
|
356
|
-
if (isPositionChanged) delete
|
|
355
|
+
this.element.style.transition = null;
|
|
356
|
+
if (isPositionChanged) delete this.element.style.transition;
|
|
357
357
|
onComplete?.();
|
|
358
358
|
}, duration);
|
|
359
359
|
} else {
|
|
360
|
-
if (isPositionChanged) delete
|
|
360
|
+
if (isPositionChanged) delete this.element.style.transition;
|
|
361
361
|
onComplete?.();
|
|
362
362
|
}
|
|
363
|
-
});
|
|
363
|
+
}, 20);
|
|
364
364
|
|
|
365
365
|
return this;
|
|
366
366
|
}
|
|
@@ -3,21 +3,4 @@
|
|
|
3
3
|
* - "notfound": No matching results found.
|
|
4
4
|
* - "nodata": No data available to display.
|
|
5
5
|
*/
|
|
6
|
-
export type EmptyStateType = "notfound" | "nodata";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Configuration options for displaying empty states.
|
|
10
|
-
*/
|
|
11
|
-
export interface EmptyStateOptions {
|
|
12
|
-
textNoData: string; // Text to display when there is no data
|
|
13
|
-
textNotFound: string; // Text to display when no results are found
|
|
14
|
-
[key: string]: unknown; // Allows additional custom properties
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Configuration options for displaying loading state.
|
|
19
|
-
*/
|
|
20
|
-
export interface LoadingStateOptions {
|
|
21
|
-
textLoading: string; // Text to display while data is loading
|
|
22
|
-
[key: string]: unknown; // Allows additional custom properties
|
|
23
|
-
}
|
|
6
|
+
export type EmptyStateType = "notfound" | "nodata";
|
|
@@ -18,6 +18,20 @@ export interface AdapterContract<TItem extends ModelContract<any, any>> {
|
|
|
18
18
|
*/
|
|
19
19
|
adapterKey: string;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Reference to the associated RecyclerView instance.
|
|
23
|
+
*
|
|
24
|
+
* Acts as the rendering host and lifecycle coordinator for this adapter.
|
|
25
|
+
* The adapter uses this reference to:
|
|
26
|
+
* - Access recycler-level configuration and state
|
|
27
|
+
* - Request layout, rebind, or invalidation operations
|
|
28
|
+
* - Coordinate virtual scrolling, recycling, or view reuse strategies
|
|
29
|
+
*
|
|
30
|
+
* The concrete type is intentionally left open to avoid tight coupling
|
|
31
|
+
* between the adapter contract and a specific RecyclerView implementation.
|
|
32
|
+
*/
|
|
33
|
+
recyclerView: any;
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* Replace the current list of items with a new list.
|
|
23
37
|
* Implementations should also trigger a re-render when appropriate.
|
|
@@ -48,6 +48,8 @@ export interface RecyclerViewContract<TAdapter extends AdapterContract<any>> {
|
|
|
48
48
|
/**
|
|
49
49
|
* Refresh the rendered views to reflect current adapter data/state.
|
|
50
50
|
* May perform diffing, partial updates, or full re-binding.
|
|
51
|
+
*
|
|
52
|
+
* @param isUpdate - Indicates if this refresh is due to an update operation.
|
|
51
53
|
*/
|
|
52
|
-
refresh(): void;
|
|
54
|
+
refresh(isUpdate: boolean): void;
|
|
53
55
|
}
|
|
@@ -27,6 +27,12 @@ export interface ViewContract<TTags extends Record<string, HTMLElement>> {
|
|
|
27
27
|
*/
|
|
28
28
|
render(): void;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Update the view.
|
|
32
|
+
* Implementations typically refresh displayed data without a full re-render.
|
|
33
|
+
*/
|
|
34
|
+
update(): void;
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* Get the root HTMLElement for the mounted view.
|
|
32
38
|
*
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for the virtualized recycler view.
|
|
4
|
+
* Controls which element is used for scrolling, sizing heuristics, and how many
|
|
5
|
+
* extra items are rendered outside the viewport to reduce popping.
|
|
6
|
+
*/
|
|
7
|
+
export type VirtualOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The scroll container element used to measure scroll position and viewport.
|
|
10
|
+
* If omitted, the implementation may default to the nearest scrollable parent
|
|
11
|
+
* or the window (depending on your mount strategy).
|
|
12
|
+
*/
|
|
13
|
+
scrollEl?: HTMLElement;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Estimated height (in pixels) for a single item.
|
|
17
|
+
* Used as a heuristic before real measurements are available (especially when
|
|
18
|
+
* `dynamicHeights` is enabled) to compute the total scrollable size.
|
|
19
|
+
*/
|
|
20
|
+
estimateItemHeight?: number;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Number of extra items to render above and below the visible range.
|
|
24
|
+
* Higher values reduce blanking during fast scroll at the cost of more DOM work.
|
|
25
|
+
*/
|
|
26
|
+
overscan?: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Enables measuring and caching actual item heights.
|
|
30
|
+
* When `true`, the recycler view supports variable-height items instead of
|
|
31
|
+
* assuming a fixed height for all items.
|
|
32
|
+
*/
|
|
33
|
+
dynamicHeights?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enables adaptive estimation based on observed item measurements.
|
|
37
|
+
* When `true`, `estimateItemHeight` may be refined over time to improve
|
|
38
|
+
* scrollbar accuracy and reduce layout jumps.
|
|
39
|
+
*/
|
|
40
|
+
adaptiveEstimate?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Tag map for the virtual recycler view DOM structure.
|
|
45
|
+
* These nodes are typically produced by `mountView`/`mountNode` and used to
|
|
46
|
+
* manipulate padding and host the rendered item elements.
|
|
47
|
+
*/
|
|
48
|
+
export type VirtualRecyclerViewTags = {
|
|
49
|
+
/**
|
|
50
|
+
* Top spacer element that simulates the height of items scrolled past.
|
|
51
|
+
* Its height is adjusted to keep the visible items aligned with the scroll offset.
|
|
52
|
+
*/
|
|
53
|
+
PadTop: HTMLDivElement;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Container that hosts the currently rendered (visible + overscan) item elements.
|
|
57
|
+
* Items are inserted/updated within this element during virtualization.
|
|
58
|
+
*/
|
|
59
|
+
ItemsHost: HTMLDivElement;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Bottom spacer element that simulates the height of items not yet rendered.
|
|
63
|
+
* Its height is adjusted to represent remaining content below the visible range.
|
|
64
|
+
*/
|
|
65
|
+
PadBottom: HTMLDivElement;
|
|
66
|
+
};
|
|
@@ -21,6 +21,7 @@ export type StorageEvents = {
|
|
|
21
21
|
*/
|
|
22
22
|
export interface DefaultConfig {
|
|
23
23
|
showPanel?: boolean; // Whether to show the panel initially
|
|
24
|
+
virtualScroll?: boolean; // Enable virtual scroll
|
|
24
25
|
accessoryStyle?: string; // CSS style for accessory elements
|
|
25
26
|
multiple?: boolean; // Enable multiple selection
|
|
26
27
|
minWidth?: string; // Minimum width of the component
|
package/src/ts/utils/istorage.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { BinderMap, DefaultConfig } from "../types/utils/istorage.type";
|
|
|
6
6
|
export class iStorage {
|
|
7
7
|
defaultConfig: DefaultConfig = {
|
|
8
8
|
showPanel: true,
|
|
9
|
+
virtualScroll: true,
|
|
9
10
|
accessoryStyle: "top",
|
|
10
11
|
multiple: false,
|
|
11
12
|
minWidth: "50px",
|
|
@@ -59,10 +60,10 @@ export class iStorage {
|
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
/** Bound instance map (keyed by select element). */
|
|
62
|
-
bindedMap: Map<HTMLSelectElement, BinderMap> = new Map();
|
|
63
|
+
bindedMap: Map<HTMLSelectElement|HTMLElement, BinderMap> = new Map();
|
|
63
64
|
|
|
64
65
|
/** Unbind cache map (keyed by select element). */
|
|
65
|
-
unbindedMap: Map<HTMLSelectElement, BinderMap> = new Map();
|
|
66
|
+
unbindedMap: Map<HTMLSelectElement|HTMLElement, BinderMap> = new Map();
|
|
66
67
|
|
|
67
68
|
/** List of bound selectors/commands. */
|
|
68
69
|
bindedCommand: string[] = [];
|
package/src/ts/utils/libs.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { BinderMap } from "../types/utils/istorage.type";
|
|
|
2
2
|
import { MountViewResult, NodeSpec } from "../types/utils/libs.type";
|
|
3
3
|
import { iStorage } from "./istorage";
|
|
4
4
|
import { CallbackScheduler } from "./callback-scheduler";
|
|
5
|
+
import { SelectiveOptions } from "../types/utils/selective.type";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @class
|
|
@@ -88,8 +89,8 @@ export class Libs {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
// NodeList or array-like
|
|
91
|
-
if (
|
|
92
|
-
return Array.from(queryCommon
|
|
92
|
+
if (queryCommon instanceof NodeList || Array.isArray(queryCommon)) {
|
|
93
|
+
return Array.from(queryCommon);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
return [];
|
|
@@ -131,7 +132,7 @@ export class Libs {
|
|
|
131
132
|
(["style", "dataset"] as const).forEach((property) => {
|
|
132
133
|
const value = nodeOption[property];
|
|
133
134
|
if (value && typeof value === "object") {
|
|
134
|
-
Object.assign(
|
|
135
|
+
Object.assign(element_creation[property], value);
|
|
135
136
|
}
|
|
136
137
|
delete nodeOption[property];
|
|
137
138
|
});
|
|
@@ -176,7 +177,7 @@ export class Libs {
|
|
|
176
177
|
if (value === null) {
|
|
177
178
|
element_creation.removeAttribute(key);
|
|
178
179
|
} else {
|
|
179
|
-
|
|
180
|
+
element_creation[key] = value;
|
|
180
181
|
}
|
|
181
182
|
});
|
|
182
183
|
|
|
@@ -260,18 +261,18 @@ export class Libs {
|
|
|
260
261
|
* matching element properties or data-* attributes when present.
|
|
261
262
|
*
|
|
262
263
|
* @param {HTMLElement} element - Source element providing overrides.
|
|
263
|
-
* @param {
|
|
264
|
-
* @returns {
|
|
264
|
+
* @param {SelectiveOptions} options - Default configuration to be merged.
|
|
265
|
+
* @returns {SelectiveOptions} - Final configuration after element overrides.
|
|
265
266
|
*/
|
|
266
|
-
static buildConfig
|
|
267
|
-
const myOptions = this.jsCopyObject(options);
|
|
267
|
+
static buildConfig(element: HTMLElement, options: SelectiveOptions): SelectiveOptions {
|
|
268
|
+
const myOptions = this.jsCopyObject<SelectiveOptions>(options);
|
|
268
269
|
|
|
269
270
|
for (const optionKey in myOptions) {
|
|
270
|
-
const propValue =
|
|
271
|
+
const propValue = element[optionKey];
|
|
271
272
|
if (propValue) {
|
|
272
|
-
|
|
273
|
+
myOptions[optionKey] = propValue;
|
|
273
274
|
} else if (typeof element?.dataset?.[optionKey] !== "undefined") {
|
|
274
|
-
|
|
275
|
+
myOptions[optionKey] = element.dataset[optionKey];
|
|
275
276
|
}
|
|
276
277
|
}
|
|
277
278
|
|
|
@@ -298,10 +299,10 @@ export class Libs {
|
|
|
298
299
|
const cfgVar = cfg[optionKey];
|
|
299
300
|
for (const actKey in cfgVar) {
|
|
300
301
|
// Keep original behavior (push), do not change semantics.
|
|
301
|
-
|
|
302
|
+
level0[optionKey][actKey].push(cfgVar[actKey]);
|
|
302
303
|
}
|
|
303
304
|
} else {
|
|
304
|
-
|
|
305
|
+
level0[optionKey] = cfg[optionKey];
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
}
|
|
@@ -343,7 +344,7 @@ export class Libs {
|
|
|
343
344
|
* @returns {boolean} - True if an entry existed and was removed.
|
|
344
345
|
*/
|
|
345
346
|
static removeBinderMap(element: HTMLElement): boolean {
|
|
346
|
-
return this.iStorage.bindedMap.delete(element
|
|
347
|
+
return this.iStorage.bindedMap.delete(element);
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
/**
|
|
@@ -353,17 +354,17 @@ export class Libs {
|
|
|
353
354
|
* @returns {BinderMap | null} - The stored binder map value or undefined if absent.
|
|
354
355
|
*/
|
|
355
356
|
static getBinderMap(item: HTMLElement): BinderMap | null {
|
|
356
|
-
return this.iStorage.bindedMap.get(item
|
|
357
|
+
return this.iStorage.bindedMap.get(item);
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
/**
|
|
360
361
|
* Sets or updates the binder map entry for a given element.
|
|
361
362
|
*
|
|
362
363
|
* @param {HTMLElement} item - Element key to associate with the binder map.
|
|
363
|
-
* @param {
|
|
364
|
+
* @param {BinderMap} bindMap - Value to store in the binder map.
|
|
364
365
|
*/
|
|
365
|
-
static setBinderMap(item: HTMLElement, bindMap:
|
|
366
|
-
this.iStorage.bindedMap.set(item
|
|
366
|
+
static setBinderMap(item: HTMLElement, bindMap: BinderMap): void {
|
|
367
|
+
this.iStorage.bindedMap.set(item, bindMap);
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
/**
|
|
@@ -373,7 +374,7 @@ export class Libs {
|
|
|
373
374
|
* @returns {boolean} - True if an entry existed and was removed.
|
|
374
375
|
*/
|
|
375
376
|
static removeUnbinderMap(element: HTMLElement): boolean {
|
|
376
|
-
return this.iStorage.unbindedMap.delete(element
|
|
377
|
+
return this.iStorage.unbindedMap.delete(element);
|
|
377
378
|
}
|
|
378
379
|
|
|
379
380
|
/**
|
|
@@ -383,17 +384,17 @@ export class Libs {
|
|
|
383
384
|
* @returns {unknown} - The stored unbinder map value or undefined if absent.
|
|
384
385
|
*/
|
|
385
386
|
static getUnbinderMap(item: HTMLElement): unknown {
|
|
386
|
-
return this.iStorage.unbindedMap.get(item
|
|
387
|
+
return this.iStorage.unbindedMap.get(item);
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
/**
|
|
390
391
|
* Sets or updates the unbinder map entry for a given element.
|
|
391
392
|
*
|
|
392
393
|
* @param {HTMLElement} item - Element key to associate with the unbinder map.
|
|
393
|
-
* @param {
|
|
394
|
+
* @param {BinderMap} bindMap - Value to store in the unbinder map.
|
|
394
395
|
*/
|
|
395
|
-
static setUnbinderMap(item: HTMLElement, bindMap:
|
|
396
|
-
this.iStorage.unbindedMap.set(item
|
|
396
|
+
static setUnbinderMap(item: HTMLElement, bindMap: BinderMap): void {
|
|
397
|
+
this.iStorage.unbindedMap.set(item, bindMap);
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
/**
|
|
@@ -430,7 +431,7 @@ export class Libs {
|
|
|
430
431
|
.replace(/\`\>/g, ">")
|
|
431
432
|
.trim();
|
|
432
433
|
|
|
433
|
-
const doc =
|
|
434
|
+
const doc = globalThis?.document as Document | undefined;
|
|
434
435
|
|
|
435
436
|
if (!doc || typeof doc.createElement !== "function") {
|
|
436
437
|
s = s
|
|
@@ -510,7 +511,7 @@ export class Libs {
|
|
|
510
511
|
result.push(group);
|
|
511
512
|
|
|
512
513
|
Array.from(group.children).forEach((option) => {
|
|
513
|
-
|
|
514
|
+
option["__parentGroup"] = group;
|
|
514
515
|
result.push(option as HTMLOptionElement);
|
|
515
516
|
});
|
|
516
517
|
} else if (child.tagName === "OPTION") {
|
|
@@ -30,7 +30,7 @@ export class Selective {
|
|
|
30
30
|
|
|
31
31
|
const doneToken = Libs.randomString();
|
|
32
32
|
Libs.callbackScheduler.on(doneToken, () => {
|
|
33
|
-
iEvents.callEvent([this.find(query)], ...(merged.on!.load
|
|
33
|
+
iEvents.callEvent([this.find(query)], ...(merged.on!.load));
|
|
34
34
|
Libs.callbackScheduler.clear(doneToken);
|
|
35
35
|
merged.on!.load = [];
|
|
36
36
|
});
|
|
@@ -194,7 +194,7 @@ export class Selective {
|
|
|
194
194
|
selectElement.style.display = "";
|
|
195
195
|
selectElement.style.visibility = "";
|
|
196
196
|
selectElement.disabled = false;
|
|
197
|
-
delete
|
|
197
|
+
delete selectElement.dataset.selectiveId;
|
|
198
198
|
|
|
199
199
|
if (wrapper && wrapper.parentNode) {
|
|
200
200
|
wrapper.parentNode.replaceChild(selectElement, wrapper);
|
|
@@ -245,12 +245,12 @@ export class Selective {
|
|
|
245
245
|
const bindMap: BinderMap = { options: options_cfg };
|
|
246
246
|
Libs.setBinderMap(selectElement, bindMap);
|
|
247
247
|
|
|
248
|
-
const selectBox = new SelectBox(selectElement, this
|
|
249
|
-
bindMap.container =
|
|
250
|
-
bindMap.action =
|
|
251
|
-
bindMap.self = selectBox
|
|
248
|
+
const selectBox = new SelectBox(selectElement, this);
|
|
249
|
+
bindMap.container = selectBox.container;
|
|
250
|
+
bindMap.action = selectBox.getAction();
|
|
251
|
+
bindMap.self = selectBox;
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
selectBox.container.view.addEventListener("mouseup", () => {
|
|
254
254
|
bindMap.action?.toggle?.();
|
|
255
255
|
});
|
|
256
256
|
|