selective-ui 1.4.1 → 1.4.2
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 +8 -2
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +158 -23
- 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 +159 -24
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/css/views/option-view.css +2 -2
- package/src/ts/adapter/mixed-adapter.ts +2 -3
- package/src/ts/components/selectbox.ts +19 -4
- package/src/ts/core/base/adapter.ts +24 -2
- package/src/ts/core/base/virtual-recyclerview.ts +89 -8
- package/src/ts/models/option-model.ts +28 -1
- package/src/ts/services/refresher.ts +7 -1
package/package.json
CHANGED
|
@@ -610,7 +610,7 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
|
|
|
610
610
|
* @returns {void}
|
|
611
611
|
*/
|
|
612
612
|
public resetHighlight(): void {
|
|
613
|
-
this.setHighlight(0);
|
|
613
|
+
this.setHighlight(0, false);
|
|
614
614
|
}
|
|
615
615
|
|
|
616
616
|
/**
|
|
@@ -716,10 +716,9 @@ export class MixedAdapter extends Adapter<MixedItem, GroupView | OptionView> {
|
|
|
716
716
|
|
|
717
717
|
if (isScrollToView) {
|
|
718
718
|
const el = item.view?.getView?.();
|
|
719
|
-
if (el) {
|
|
719
|
+
if (el?.isConnected) {
|
|
720
720
|
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
721
721
|
} else {
|
|
722
|
-
// If virtualized, ensure the item is rendered before trying to scroll.
|
|
723
722
|
this.recyclerView?.ensureRendered?.(i, {
|
|
724
723
|
scrollIntoView: true,
|
|
725
724
|
});
|
|
@@ -471,6 +471,7 @@ export class SelectBox extends Lifecycle {
|
|
|
471
471
|
);
|
|
472
472
|
|
|
473
473
|
Refresher.resizeBox(select, container.tags.ViewPanel);
|
|
474
|
+
Refresher.resizeBox(select, this.node, true);
|
|
474
475
|
select.classList.add("init");
|
|
475
476
|
|
|
476
477
|
// initial mask
|
|
@@ -836,7 +837,7 @@ export class SelectBox extends Lifecycle {
|
|
|
836
837
|
get mask() {
|
|
837
838
|
const item_list: string[] = [];
|
|
838
839
|
superThis.getModelOption(true).forEach((m) => {
|
|
839
|
-
item_list.push(m.
|
|
840
|
+
item_list.push(m.mask);
|
|
840
841
|
});
|
|
841
842
|
return item_list;
|
|
842
843
|
},
|
|
@@ -988,6 +989,9 @@ export class SelectBox extends Lifecycle {
|
|
|
988
989
|
|
|
989
990
|
// AJAX: load missing values
|
|
990
991
|
if (container.searchController?.isAjax?.()) {
|
|
992
|
+
container.searchController.resetPagination();
|
|
993
|
+
superThis.hasLoadedOnce = false;
|
|
994
|
+
|
|
991
995
|
const { missing } =
|
|
992
996
|
container.searchController.checkMissingValues(value);
|
|
993
997
|
|
|
@@ -997,7 +1001,6 @@ export class SelectBox extends Lifecycle {
|
|
|
997
1001
|
container.popup?.showLoading?.();
|
|
998
1002
|
|
|
999
1003
|
try {
|
|
1000
|
-
container.searchController.resetPagination();
|
|
1001
1004
|
const result =
|
|
1002
1005
|
await container.searchController.loadByValues(
|
|
1003
1006
|
missing,
|
|
@@ -1026,6 +1029,10 @@ export class SelectBox extends Lifecycle {
|
|
|
1026
1029
|
`Could not load ${missing.length} values:`,
|
|
1027
1030
|
missing,
|
|
1028
1031
|
);
|
|
1032
|
+
setTimeout(() => {
|
|
1033
|
+
container.searchController.resetPagination();
|
|
1034
|
+
this.change(false, trigger);
|
|
1035
|
+
}, 200);
|
|
1029
1036
|
}
|
|
1030
1037
|
} catch (error) {
|
|
1031
1038
|
console.error(
|
|
@@ -1121,7 +1128,13 @@ export class SelectBox extends Lifecycle {
|
|
|
1121
1128
|
|
|
1122
1129
|
this.load();
|
|
1123
1130
|
container.popup.open(
|
|
1124
|
-
|
|
1131
|
+
() => {
|
|
1132
|
+
setTimeout(() => {
|
|
1133
|
+
if (selectedOption) {
|
|
1134
|
+
adapter.setHighlight(selectedOption, bindedOptions.autoscroll);
|
|
1135
|
+
}
|
|
1136
|
+
}, 100);
|
|
1137
|
+
},
|
|
1125
1138
|
!container.popup.loadingState.isVisible,
|
|
1126
1139
|
);
|
|
1127
1140
|
|
|
@@ -1292,7 +1305,9 @@ export class SelectBox extends Lifecycle {
|
|
|
1292
1305
|
.search("")
|
|
1293
1306
|
.then(() => {
|
|
1294
1307
|
container.popup?.triggerResize?.();
|
|
1295
|
-
|
|
1308
|
+
setTimeout(() => {
|
|
1309
|
+
resove(getInstance());
|
|
1310
|
+
}, 60);
|
|
1296
1311
|
})
|
|
1297
1312
|
.catch((err: unknown) => {
|
|
1298
1313
|
console.error("Initial ajax load error:", err);
|
|
@@ -95,6 +95,17 @@ export class Adapter<
|
|
|
95
95
|
*/
|
|
96
96
|
recyclerView: any;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Tracks all scheduler keys registered by this adapter instance via
|
|
100
|
+
* {@link onPropChanging} and {@link onPropChanged}.
|
|
101
|
+
*
|
|
102
|
+
* Used during {@link destroy} to clean up all associated pipelines
|
|
103
|
+
* from the global {@link Libs.callbackScheduler}.
|
|
104
|
+
*
|
|
105
|
+
* Keys are deduplicated automatically by Set semantics.
|
|
106
|
+
*/
|
|
107
|
+
callbackSchedulerList: Set<string> = new Set();
|
|
108
|
+
|
|
98
109
|
/**
|
|
99
110
|
* Creates an adapter with an optional initial item list and initializes its lifecycle.
|
|
100
111
|
*
|
|
@@ -153,11 +164,13 @@ export class Adapter<
|
|
|
153
164
|
propName: string,
|
|
154
165
|
callback: (...args: unknown[]) => void,
|
|
155
166
|
): void {
|
|
167
|
+
const key = `${propName}ing_${this.adapterKey}`;
|
|
156
168
|
Libs.callbackScheduler.on(
|
|
157
|
-
|
|
169
|
+
key,
|
|
158
170
|
callback,
|
|
159
171
|
{ debounce: 0 },
|
|
160
172
|
);
|
|
173
|
+
this.callbackSchedulerList.add(key);
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
/**
|
|
@@ -177,9 +190,11 @@ export class Adapter<
|
|
|
177
190
|
propName: string,
|
|
178
191
|
callback: (...args: unknown[]) => void,
|
|
179
192
|
): void {
|
|
180
|
-
|
|
193
|
+
const key = `${propName}_${this.adapterKey}`;
|
|
194
|
+
Libs.callbackScheduler.on(key, callback, {
|
|
181
195
|
debounce: 0,
|
|
182
196
|
});
|
|
197
|
+
this.callbackSchedulerList.add(key);
|
|
183
198
|
}
|
|
184
199
|
|
|
185
200
|
/**
|
|
@@ -341,10 +356,17 @@ export class Adapter<
|
|
|
341
356
|
return;
|
|
342
357
|
}
|
|
343
358
|
|
|
359
|
+
this.callbackSchedulerList.forEach((key) => {
|
|
360
|
+
Libs.callbackScheduler.off(key);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
this.callbackSchedulerList.clear();
|
|
344
364
|
this.recyclerView = null;
|
|
345
365
|
this.items.forEach((item) => {
|
|
346
366
|
item?.destroy?.();
|
|
347
367
|
});
|
|
348
368
|
this.items = [];
|
|
369
|
+
|
|
370
|
+
super.destroy();
|
|
349
371
|
}
|
|
350
372
|
}
|
|
@@ -122,6 +122,12 @@ export class VirtualRecyclerView<
|
|
|
122
122
|
private suspended = false;
|
|
123
123
|
private boundOnScroll?: () => void;
|
|
124
124
|
private resumeResizeAfter = false;
|
|
125
|
+
/**
|
|
126
|
+
* When set, scrollToIndex() will be called after the next measureVisibleAndUpdate()
|
|
127
|
+
* completes and Fenwick has been updated with real heights.
|
|
128
|
+
* Set by ensureRendered() and cleared after the corrective scroll fires.
|
|
129
|
+
*/
|
|
130
|
+
private pendingScrollToIndex: number | null = null;
|
|
125
131
|
|
|
126
132
|
/** Small cache for sticky header height (≈16ms TTL) to limit layout reads. */
|
|
127
133
|
private stickyCacheTick = 0;
|
|
@@ -332,8 +338,21 @@ export class VirtualRecyclerView<
|
|
|
332
338
|
index: number,
|
|
333
339
|
opt?: { scrollIntoView?: boolean },
|
|
334
340
|
): void {
|
|
335
|
-
|
|
336
|
-
|
|
341
|
+
if (!opt?.scrollIntoView) {
|
|
342
|
+
// No scroll requested — mount only (legacy path, used by probes).
|
|
343
|
+
this.mountRange(index, index);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Pass 1: instant — brings window to vicinity, triggers measure.
|
|
348
|
+
// Must be instant so Pass 2 smooth scroll isn't interrupted mid-animation.
|
|
349
|
+
this.scrollToIndex(index, "instant");
|
|
350
|
+
|
|
351
|
+
// Pass 2: measureVisibleAndUpdate() will consume this and fire a corrective
|
|
352
|
+
// smooth scroll after Fenwick has been updated with real heights.
|
|
353
|
+
// rv.resume() is guaranteed to run before this callback (popup.open onComplete
|
|
354
|
+
// calls rv.resume() first), so the window is already rendered when we arrive here.
|
|
355
|
+
this.pendingScrollToIndex = index;
|
|
337
356
|
}
|
|
338
357
|
|
|
339
358
|
/**
|
|
@@ -348,19 +367,32 @@ export class VirtualRecyclerView<
|
|
|
348
367
|
* @param {number} index - Item index to bring into view.
|
|
349
368
|
* @returns {void}
|
|
350
369
|
*/
|
|
351
|
-
public scrollToIndex(index: number): void {
|
|
370
|
+
public scrollToIndex(index: number, behavior: ScrollBehavior = "smooth"): void {
|
|
352
371
|
const count = this.adapter?.itemCount?.() ?? 0;
|
|
353
372
|
if (count <= 0) return;
|
|
354
373
|
|
|
355
374
|
const topInContainer = this.offsetTopOf(index);
|
|
356
375
|
const containerTop = this.containerTopInScroll();
|
|
357
|
-
const
|
|
376
|
+
const stickyH = this.stickyTopHeight();
|
|
377
|
+
const viewportH = Math.max(0, this.scrollEl.clientHeight - stickyH);
|
|
378
|
+
|
|
379
|
+
// item height from cache, or current estimate for unmeasured items
|
|
380
|
+
const est = this.getEstimate();
|
|
381
|
+
const itemH = this.heightCache[index] ?? est;
|
|
382
|
+
|
|
383
|
+
// Align item center to viewport center (below any sticky header).
|
|
384
|
+
// viewportH already excludes stickyH, so no further offset needed.
|
|
385
|
+
// Equivalent to scrollIntoView({ block: "center" }).
|
|
386
|
+
const centeredTarget = containerTop + topInContainer
|
|
387
|
+
- (viewportH - itemH) / 3;
|
|
388
|
+
|
|
358
389
|
const maxScroll = Math.max(
|
|
359
390
|
0,
|
|
360
391
|
this.scrollEl.scrollHeight - this.scrollEl.clientHeight,
|
|
361
392
|
);
|
|
362
393
|
|
|
363
|
-
|
|
394
|
+
const clamped = Math.min(Math.max(0, centeredTarget), maxScroll);
|
|
395
|
+
this.scrollEl.scrollTo({ top: clamped, behavior });
|
|
364
396
|
}
|
|
365
397
|
|
|
366
398
|
/**
|
|
@@ -435,9 +467,24 @@ export class VirtualRecyclerView<
|
|
|
435
467
|
if (count <= 0) return;
|
|
436
468
|
|
|
437
469
|
this.suspend();
|
|
438
|
-
this.
|
|
470
|
+
this.pendingScrollToIndex = null;
|
|
471
|
+
|
|
472
|
+
// When visibility changes (search filter applied or cleared), heightCache may
|
|
473
|
+
// contain heights measured while only a subset of items was visible. Re-using
|
|
474
|
+
// these partial measurements in rebuildFenwick() causes incorrect prefix sums
|
|
475
|
+
// (e.g. items measured while scrolled into a filtered window have real heights,
|
|
476
|
+
// while surrounding items still use estimates — creating an uneven Fenwick).
|
|
477
|
+
//
|
|
478
|
+
// Safe fix: clear heightCache entirely on visibility change. The adaptive
|
|
479
|
+
// estimator will re-seed from probeInitialHeight() on the next render, and
|
|
480
|
+
// items will be re-measured as they scroll into view.
|
|
481
|
+
this.heightCache = [];
|
|
482
|
+
this.measuredSum = 0;
|
|
483
|
+
this.measuredCount = 0;
|
|
484
|
+
this.firstMeasured = false;
|
|
485
|
+
|
|
486
|
+
this.resetDOM();
|
|
439
487
|
this.cleanupInvisibleItems();
|
|
440
|
-
this.recomputeMeasuredStats(count);
|
|
441
488
|
this.rebuildFenwick(count);
|
|
442
489
|
this.start = 0;
|
|
443
490
|
this.end = -1;
|
|
@@ -457,7 +504,31 @@ export class VirtualRecyclerView<
|
|
|
457
504
|
}
|
|
458
505
|
|
|
459
506
|
/**
|
|
460
|
-
* Resets
|
|
507
|
+
* Resets DOM nodes, Fenwick sums, padding, and estimator stats — but preserves {@link heightCache}.
|
|
508
|
+
*
|
|
509
|
+
* Use this inside {@link refreshItem} so that {@link recomputeMeasuredStats} can still
|
|
510
|
+
* read previously measured heights before the Fenwick tree is rebuilt.
|
|
511
|
+
*
|
|
512
|
+
* DOM side effects:
|
|
513
|
+
* - Removes all currently mounted item elements tracked in {@link created}.
|
|
514
|
+
* - Resets pad heights to `0px`.
|
|
515
|
+
*
|
|
516
|
+
* @returns {void}
|
|
517
|
+
*/
|
|
518
|
+
private resetDOM(): void {
|
|
519
|
+
this.created.forEach((el) => el.remove());
|
|
520
|
+
this.created.clear();
|
|
521
|
+
this.fenwick.reset(0);
|
|
522
|
+
this.PadTop.style.height = "0px";
|
|
523
|
+
this.PadBottom.style.height = "0px";
|
|
524
|
+
this.firstMeasured = false;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Full reset: clears DOM nodes, Fenwick sums, padding, estimator stats, AND {@link heightCache}.
|
|
529
|
+
*
|
|
530
|
+
* Use this for complete teardown (e.g., adapter swap, destroy sequence) where all
|
|
531
|
+
* cached measurements should be discarded.
|
|
461
532
|
*
|
|
462
533
|
* DOM side effects:
|
|
463
534
|
* - Removes all currently mounted item elements tracked in {@link created}.
|
|
@@ -843,6 +914,16 @@ export class VirtualRecyclerView<
|
|
|
843
914
|
if (this.opts.adaptiveEstimate) this.rebuildFenwick(count);
|
|
844
915
|
this.scheduleUpdateWindow();
|
|
845
916
|
}
|
|
917
|
+
|
|
918
|
+
// Corrective scroll: if ensureRendered() registered a target index, fire
|
|
919
|
+
// scrollToIndex() now that real heights are in Fenwick. Clear the target
|
|
920
|
+
// first to prevent infinite re-triggering (scrollToIndex may cause another
|
|
921
|
+
// measure cycle, but heights won't change so changed === false next time).
|
|
922
|
+
if (this.pendingScrollToIndex !== null) {
|
|
923
|
+
const target = this.pendingScrollToIndex;
|
|
924
|
+
this.pendingScrollToIndex = null;
|
|
925
|
+
this.scrollToIndex(target, "smooth");
|
|
926
|
+
}
|
|
846
927
|
}
|
|
847
928
|
|
|
848
929
|
/**
|
|
@@ -266,6 +266,33 @@ export class OptionModel extends Model<
|
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Resolved display mask for this option.
|
|
271
|
+
*
|
|
272
|
+
* The mask is the primary render label used by the UI layer and supports
|
|
273
|
+
* optional inline tag translation / rich HTML rendering.
|
|
274
|
+
*
|
|
275
|
+
* Source priority:
|
|
276
|
+
* 1. `data-mask` (`dataset.mask`)
|
|
277
|
+
* 2. Native `<option>` text content (`targetElement.text`)
|
|
278
|
+
*
|
|
279
|
+
* Processing pipeline:
|
|
280
|
+
* - Raw content is first passed through {@link Libs.tagTranslate}.
|
|
281
|
+
* - When `options.allowHtml === true`, translated HTML is preserved.
|
|
282
|
+
* - Otherwise, all markup is stripped via {@link Libs.stripHtml}.
|
|
283
|
+
*
|
|
284
|
+
* Unlike {@link text}, this getter prioritizes the custom dataset mask,
|
|
285
|
+
* making it suitable for display overrides without mutating the native
|
|
286
|
+
* `<option>` label.
|
|
287
|
+
*
|
|
288
|
+
* @returns {string} Render-ready option label.
|
|
289
|
+
*/
|
|
290
|
+
public get mask(): string {
|
|
291
|
+
const raw = this.dataset?.mask ?? this.targetElement?.text ?? "";
|
|
292
|
+
const translated = Libs.tagTranslate(raw);
|
|
293
|
+
return this.options.allowHtml ? translated : Libs.stripHtml(translated);
|
|
294
|
+
}
|
|
295
|
+
|
|
269
296
|
/**
|
|
270
297
|
* Display label for rendering (with tag translation and HTML policy).
|
|
271
298
|
*
|
|
@@ -279,7 +306,7 @@ export class OptionModel extends Model<
|
|
|
279
306
|
* @returns {string}
|
|
280
307
|
*/
|
|
281
308
|
public get text(): string {
|
|
282
|
-
const raw = this.
|
|
309
|
+
const raw = this.targetElement?.text ?? this.dataset?.mask ?? "";
|
|
283
310
|
const translated = Libs.tagTranslate(raw);
|
|
284
311
|
return this.options.allowHtml ? translated : Libs.stripHtml(translated);
|
|
285
312
|
}
|
|
@@ -36,10 +36,12 @@ export class Refresher {
|
|
|
36
36
|
*
|
|
37
37
|
* @param select - Native `<select>` element used as the sizing reference and option source.
|
|
38
38
|
* @param view - View panel element whose inline styles will be updated.
|
|
39
|
+
* @param isWidthOnly - If true, only the width will be updated; height will be left unchanged.
|
|
39
40
|
*/
|
|
40
41
|
public static resizeBox(
|
|
41
42
|
select: HTMLSelectElement,
|
|
42
43
|
view: HTMLElement,
|
|
44
|
+
isWidthOnly = false,
|
|
43
45
|
): void {
|
|
44
46
|
const bindedMap = Libs.getBinderMap<BinderMap>(select);
|
|
45
47
|
if (!bindedMap?.options) return;
|
|
@@ -64,6 +66,10 @@ export class Refresher {
|
|
|
64
66
|
if (cfgWidth > 0) width = options.width;
|
|
65
67
|
if (cfgHeight > 0) height = options.height;
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
if (isWidthOnly) {
|
|
70
|
+
Object.assign(view.style, { width, maxWidth: width, minWidth });
|
|
71
|
+
} else {
|
|
72
|
+
Object.assign(view.style, { width, height, maxWidth: width, minWidth, minHeight });
|
|
73
|
+
}
|
|
68
74
|
}
|
|
69
75
|
}
|