selective-ui 1.4.0 → 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 +2 -2
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +407 -573
- 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 +409 -575
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +12 -12
- package/src/css/views/option-view.css +2 -2
- package/src/ts/adapter/mixed-adapter.ts +149 -71
- package/src/ts/components/accessorybox.ts +14 -11
- package/src/ts/components/directive.ts +1 -1
- package/src/ts/components/option-handle.ts +12 -9
- package/src/ts/components/placeholder.ts +5 -5
- package/src/ts/components/popup/empty-state.ts +5 -5
- package/src/ts/components/popup/loading-state.ts +5 -5
- package/src/ts/components/popup/popup.ts +138 -76
- package/src/ts/components/searchbox.ts +17 -13
- package/src/ts/components/selectbox.ts +260 -84
- package/src/ts/core/base/adapter.ts +61 -14
- package/src/ts/core/base/fenwick.ts +3 -2
- package/src/ts/core/base/lifecycle.ts +14 -4
- package/src/ts/core/base/model.ts +17 -15
- package/src/ts/core/base/recyclerview.ts +7 -5
- package/src/ts/core/base/view.ts +10 -5
- package/src/ts/core/base/virtual-recyclerview.ts +178 -45
- package/src/ts/core/model-manager.ts +48 -21
- package/src/ts/core/search-controller.ts +174 -56
- package/src/ts/global.ts +5 -8
- package/src/ts/index.ts +2 -2
- package/src/ts/models/group-model.ts +33 -8
- package/src/ts/models/option-model.ts +88 -20
- package/src/ts/services/dataset-observer.ts +6 -3
- package/src/ts/services/ea-observer.ts +1 -1
- package/src/ts/services/effector.ts +22 -12
- package/src/ts/services/refresher.ts +14 -4
- package/src/ts/services/resize-observer.ts +24 -11
- package/src/ts/services/select-observer.ts +2 -2
- package/src/ts/types/components/popup.type.ts +18 -1
- package/src/ts/types/components/searchbox.type.ts +43 -30
- package/src/ts/types/components/state.box.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +13 -5
- package/src/ts/types/core/base/lifecycle.type.ts +1 -2
- package/src/ts/types/core/base/model.type.ts +3 -3
- package/src/ts/types/core/base/recyclerview.type.ts +7 -5
- package/src/ts/types/core/base/view.type.ts +6 -6
- package/src/ts/types/core/base/virtual-recyclerview.type.ts +45 -46
- package/src/ts/types/core/search-controller.type.ts +18 -2
- package/src/ts/types/css.d.ts +1 -0
- package/src/ts/types/plugins/plugin.type.ts +2 -2
- package/src/ts/types/services/effector.type.ts +25 -25
- package/src/ts/types/services/resize-observer.type.ts +23 -12
- package/src/ts/types/utils/callback-scheduler.type.ts +2 -2
- package/src/ts/types/utils/ievents.type.ts +1 -1
- package/src/ts/types/utils/istorage.type.ts +62 -60
- package/src/ts/types/utils/libs.type.ts +19 -17
- package/src/ts/types/utils/selective.type.ts +6 -3
- package/src/ts/types/views/view.group.type.ts +9 -5
- package/src/ts/types/views/view.option.type.ts +39 -17
- package/src/ts/utils/callback-scheduler.ts +12 -7
- package/src/ts/utils/ievents.ts +12 -5
- package/src/ts/utils/istorage.ts +5 -3
- package/src/ts/utils/libs.ts +122 -43
- package/src/ts/utils/selective.ts +15 -8
- package/src/ts/views/group-view.ts +11 -9
- package/src/ts/views/option-view.ts +37 -18
|
@@ -2,7 +2,10 @@ import { ModelContract } from "src/ts/types/core/base/model.type";
|
|
|
2
2
|
import { RecyclerView } from "./recyclerview";
|
|
3
3
|
import { AdapterContract } from "src/ts/types/core/base/adapter.type";
|
|
4
4
|
import { Libs } from "src/ts/utils/libs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
VirtualOptions,
|
|
7
|
+
VirtualRecyclerViewTags,
|
|
8
|
+
} from "src/ts/types/core/base/virtual-recyclerview.type";
|
|
6
9
|
import { Lifecycle } from "./lifecycle";
|
|
7
10
|
import { LifecycleState } from "src/ts/types/core/base/lifecycle.type";
|
|
8
11
|
import { Fenwick } from "./fenwick";
|
|
@@ -58,7 +61,7 @@ import { Fenwick } from "./fenwick";
|
|
|
58
61
|
*/
|
|
59
62
|
export class VirtualRecyclerView<
|
|
60
63
|
TItem extends ModelContract<any, any>,
|
|
61
|
-
TAdapter extends AdapterContract<TItem
|
|
64
|
+
TAdapter extends AdapterContract<TItem>,
|
|
62
65
|
> extends RecyclerView<TItem, TAdapter> {
|
|
63
66
|
/**
|
|
64
67
|
* Virtualization settings (materialized to `Required<VirtualOptions>`).
|
|
@@ -109,8 +112,8 @@ export class VirtualRecyclerView<
|
|
|
109
112
|
private resizeObs?: ResizeObserver;
|
|
110
113
|
|
|
111
114
|
/** Pending animation frame ids for window and measurement. */
|
|
112
|
-
private rafId
|
|
113
|
-
private measureRaf
|
|
115
|
+
private rafId?: number;
|
|
116
|
+
private measureRaf?: number;
|
|
114
117
|
|
|
115
118
|
/** Re-entrancy/suspension flags used to prevent feedback loops. */
|
|
116
119
|
private updating = false;
|
|
@@ -119,6 +122,12 @@ export class VirtualRecyclerView<
|
|
|
119
122
|
private suspended = false;
|
|
120
123
|
private boundOnScroll?: () => void;
|
|
121
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;
|
|
122
131
|
|
|
123
132
|
/** Small cache for sticky header height (≈16ms TTL) to limit layout reads. */
|
|
124
133
|
private stickyCacheTick = 0;
|
|
@@ -138,9 +147,9 @@ export class VirtualRecyclerView<
|
|
|
138
147
|
*
|
|
139
148
|
* Note: The virtualization scaffold is built when an adapter is set via {@link setAdapter}.
|
|
140
149
|
*
|
|
141
|
-
* @param {HTMLDivElement
|
|
150
|
+
* @param {HTMLDivElement} [viewElement=null] - Optional root container for the recycler view.
|
|
142
151
|
*/
|
|
143
|
-
constructor(viewElement
|
|
152
|
+
constructor(viewElement?: HTMLDivElement) {
|
|
144
153
|
super(viewElement);
|
|
145
154
|
}
|
|
146
155
|
|
|
@@ -187,24 +196,37 @@ export class VirtualRecyclerView<
|
|
|
187
196
|
|
|
188
197
|
this.viewElement.replaceChildren();
|
|
189
198
|
|
|
190
|
-
const nodeMounted = Libs.mountNode(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
const nodeMounted = Libs.mountNode<VirtualRecyclerViewTags>(
|
|
200
|
+
{
|
|
201
|
+
PadTop: {
|
|
202
|
+
tag: { node: "div", classList: "seui-virtual-pad-top" },
|
|
203
|
+
},
|
|
204
|
+
ItemsHost: {
|
|
205
|
+
tag: { node: "div", classList: "seui-virtual-items" },
|
|
206
|
+
},
|
|
207
|
+
PadBottom: {
|
|
208
|
+
tag: { node: "div", classList: "seui-virtual-pad-bottom" },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
this.viewElement,
|
|
212
|
+
);
|
|
195
213
|
|
|
196
214
|
this.PadTop = nodeMounted.PadTop;
|
|
197
215
|
this.ItemsHost = nodeMounted.ItemsHost;
|
|
198
216
|
this.PadBottom = nodeMounted.PadBottom;
|
|
199
217
|
|
|
200
|
-
this.scrollEl =
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
this.scrollEl =
|
|
219
|
+
this.opts.scrollEl ??
|
|
220
|
+
(this.viewElement.closest(".seui-popup") as HTMLElement) ??
|
|
221
|
+
(this.viewElement.parentElement as HTMLElement);
|
|
203
222
|
|
|
204
|
-
if (!this.scrollEl)
|
|
223
|
+
if (!this.scrollEl)
|
|
224
|
+
throw new Error("VirtualRecyclerView: scrollEl not found");
|
|
205
225
|
|
|
206
226
|
this.boundOnScroll = this.onScroll.bind(this);
|
|
207
|
-
this.scrollEl.addEventListener("scroll", this.boundOnScroll, {
|
|
227
|
+
this.scrollEl.addEventListener("scroll", this.boundOnScroll, {
|
|
228
|
+
passive: true,
|
|
229
|
+
});
|
|
208
230
|
|
|
209
231
|
this.refresh(false);
|
|
210
232
|
this.attachResizeObserverOnce();
|
|
@@ -249,7 +271,9 @@ export class VirtualRecyclerView<
|
|
|
249
271
|
this.suspended = false;
|
|
250
272
|
|
|
251
273
|
if (this.scrollEl && this.boundOnScroll) {
|
|
252
|
-
this.scrollEl.addEventListener("scroll", this.boundOnScroll, {
|
|
274
|
+
this.scrollEl.addEventListener("scroll", this.boundOnScroll, {
|
|
275
|
+
passive: true,
|
|
276
|
+
});
|
|
253
277
|
}
|
|
254
278
|
|
|
255
279
|
if (this.resumeResizeAfter) {
|
|
@@ -310,9 +334,25 @@ export class VirtualRecyclerView<
|
|
|
310
334
|
* @param {{ scrollIntoView?: boolean }} [opt] - Optional behavior controls.
|
|
311
335
|
* @returns {void}
|
|
312
336
|
*/
|
|
313
|
-
public ensureRendered(
|
|
314
|
-
|
|
315
|
-
|
|
337
|
+
public ensureRendered(
|
|
338
|
+
index: number,
|
|
339
|
+
opt?: { scrollIntoView?: boolean },
|
|
340
|
+
): void {
|
|
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;
|
|
316
356
|
}
|
|
317
357
|
|
|
318
358
|
/**
|
|
@@ -327,16 +367,32 @@ export class VirtualRecyclerView<
|
|
|
327
367
|
* @param {number} index - Item index to bring into view.
|
|
328
368
|
* @returns {void}
|
|
329
369
|
*/
|
|
330
|
-
public scrollToIndex(index: number): void {
|
|
370
|
+
public scrollToIndex(index: number, behavior: ScrollBehavior = "smooth"): void {
|
|
331
371
|
const count = this.adapter?.itemCount?.() ?? 0;
|
|
332
372
|
if (count <= 0) return;
|
|
333
373
|
|
|
334
374
|
const topInContainer = this.offsetTopOf(index);
|
|
335
375
|
const containerTop = this.containerTopInScroll();
|
|
336
|
-
const
|
|
337
|
-
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;
|
|
338
382
|
|
|
339
|
-
|
|
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
|
+
|
|
389
|
+
const maxScroll = Math.max(
|
|
390
|
+
0,
|
|
391
|
+
this.scrollEl.scrollHeight - this.scrollEl.clientHeight,
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const clamped = Math.min(Math.max(0, centeredTarget), maxScroll);
|
|
395
|
+
this.scrollEl.scrollTo({ top: clamped, behavior });
|
|
340
396
|
}
|
|
341
397
|
|
|
342
398
|
/**
|
|
@@ -358,7 +414,7 @@ export class VirtualRecyclerView<
|
|
|
358
414
|
}
|
|
359
415
|
|
|
360
416
|
this.resizeObs?.disconnect();
|
|
361
|
-
this.created.forEach(el => el.remove());
|
|
417
|
+
this.created.forEach((el) => el.remove());
|
|
362
418
|
this.created.clear();
|
|
363
419
|
}
|
|
364
420
|
|
|
@@ -411,9 +467,24 @@ export class VirtualRecyclerView<
|
|
|
411
467
|
if (count <= 0) return;
|
|
412
468
|
|
|
413
469
|
this.suspend();
|
|
414
|
-
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();
|
|
415
487
|
this.cleanupInvisibleItems();
|
|
416
|
-
this.recomputeMeasuredStats(count);
|
|
417
488
|
this.rebuildFenwick(count);
|
|
418
489
|
this.start = 0;
|
|
419
490
|
this.end = -1;
|
|
@@ -433,7 +504,31 @@ export class VirtualRecyclerView<
|
|
|
433
504
|
}
|
|
434
505
|
|
|
435
506
|
/**
|
|
436
|
-
* 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.
|
|
437
532
|
*
|
|
438
533
|
* DOM side effects:
|
|
439
534
|
* - Removes all currently mounted item elements tracked in {@link created}.
|
|
@@ -442,7 +537,7 @@ export class VirtualRecyclerView<
|
|
|
442
537
|
* @returns {void}
|
|
443
538
|
*/
|
|
444
539
|
private resetState(): void {
|
|
445
|
-
this.created.forEach(el => el.remove());
|
|
540
|
+
this.created.forEach((el) => el.remove());
|
|
446
541
|
this.created.clear();
|
|
447
542
|
this.heightCache = [];
|
|
448
543
|
this.fenwick.reset(0);
|
|
@@ -557,7 +652,9 @@ export class VirtualRecyclerView<
|
|
|
557
652
|
const now = performance.now();
|
|
558
653
|
if (now - this.stickyCacheTick < 16) return this.stickyCacheVal;
|
|
559
654
|
|
|
560
|
-
const sticky = this.scrollEl.querySelector(
|
|
655
|
+
const sticky = this.scrollEl.querySelector(
|
|
656
|
+
".seui-option-handle:not(.hide)",
|
|
657
|
+
) as HTMLElement | null;
|
|
561
658
|
this.stickyCacheVal = sticky?.offsetHeight ?? 0;
|
|
562
659
|
this.stickyCacheTick = now;
|
|
563
660
|
return this.stickyCacheVal;
|
|
@@ -623,7 +720,7 @@ export class VirtualRecyclerView<
|
|
|
623
720
|
private rebuildFenwick(count: number): void {
|
|
624
721
|
const est = this.getEstimate();
|
|
625
722
|
const arr = Array.from({ length: count }, (_, i) =>
|
|
626
|
-
this.isIndexVisible(i) ? (this.heightCache[i] ?? est) : 0
|
|
723
|
+
this.isIndexVisible(i) ? (this.heightCache[i] ?? est) : 0,
|
|
627
724
|
);
|
|
628
725
|
this.fenwick.buildFrom(arr);
|
|
629
726
|
}
|
|
@@ -738,8 +835,12 @@ export class VirtualRecyclerView<
|
|
|
738
835
|
const next = el.nextElementSibling as HTMLElement | null;
|
|
739
836
|
|
|
740
837
|
const needsReorder =
|
|
741
|
-
(prev &&
|
|
742
|
-
|
|
838
|
+
(prev &&
|
|
839
|
+
Number(prev.getAttribute(VirtualRecyclerView.ATTR_INDEX)) >
|
|
840
|
+
index) ||
|
|
841
|
+
(next &&
|
|
842
|
+
Number(next.getAttribute(VirtualRecyclerView.ATTR_INDEX)) <
|
|
843
|
+
index);
|
|
743
844
|
|
|
744
845
|
if (needsReorder) {
|
|
745
846
|
el.remove();
|
|
@@ -763,7 +864,13 @@ export class VirtualRecyclerView<
|
|
|
763
864
|
if (this.resizeObs) return;
|
|
764
865
|
|
|
765
866
|
this.resizeObs = new ResizeObserver(() => {
|
|
766
|
-
if (
|
|
867
|
+
if (
|
|
868
|
+
this.suppressResize ||
|
|
869
|
+
this.suspended ||
|
|
870
|
+
!this.adapter ||
|
|
871
|
+
this.measureRaf != null
|
|
872
|
+
)
|
|
873
|
+
return;
|
|
767
874
|
|
|
768
875
|
this.measureRaf = requestAnimationFrame(() => {
|
|
769
876
|
this.measureRaf = null;
|
|
@@ -794,7 +901,9 @@ export class VirtualRecyclerView<
|
|
|
794
901
|
if (!this.isIndexVisible(i)) continue;
|
|
795
902
|
|
|
796
903
|
const item = this.adapter.items[i];
|
|
797
|
-
const el = (item as any)?.view?.getView?.() as
|
|
904
|
+
const el = (item as any)?.view?.getView?.() as
|
|
905
|
+
| HTMLElement
|
|
906
|
+
| undefined;
|
|
798
907
|
if (!el) continue;
|
|
799
908
|
|
|
800
909
|
const newH = this.measureOuterHeight(el);
|
|
@@ -805,6 +914,16 @@ export class VirtualRecyclerView<
|
|
|
805
914
|
if (this.opts.adaptiveEstimate) this.rebuildFenwick(count);
|
|
806
915
|
this.scheduleUpdateWindow();
|
|
807
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
|
+
}
|
|
808
927
|
}
|
|
809
928
|
|
|
810
929
|
/**
|
|
@@ -857,7 +976,8 @@ export class VirtualRecyclerView<
|
|
|
857
976
|
|
|
858
977
|
const anchorIndex = this.findFirstVisibleIndex(stRel, count);
|
|
859
978
|
const anchorTop = this.offsetTopOf(anchorIndex);
|
|
860
|
-
const anchorDelta =
|
|
979
|
+
const anchorDelta =
|
|
980
|
+
containerTop + anchorTop - this.scrollEl.scrollTop;
|
|
861
981
|
|
|
862
982
|
const firstVis = this.findFirstVisibleIndex(stRel, count);
|
|
863
983
|
if (firstVis === -1) {
|
|
@@ -868,12 +988,21 @@ export class VirtualRecyclerView<
|
|
|
868
988
|
const est = this.getEstimate();
|
|
869
989
|
const overscanPx = this.opts.overscan * est;
|
|
870
990
|
|
|
871
|
-
let startIndex =
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
991
|
+
let startIndex =
|
|
992
|
+
this.nextVisibleFrom(
|
|
993
|
+
Math.min(
|
|
994
|
+
count - 1,
|
|
995
|
+
this.fenwick.lowerBoundPrefix(
|
|
996
|
+
Math.max(0, stRel - overscanPx),
|
|
997
|
+
),
|
|
998
|
+
),
|
|
999
|
+
count,
|
|
1000
|
+
) ?? firstVis;
|
|
1001
|
+
|
|
1002
|
+
let endIndex = Math.min(
|
|
1003
|
+
count - 1,
|
|
1004
|
+
this.fenwick.lowerBoundPrefix(stRel + vhEff + overscanPx),
|
|
1005
|
+
);
|
|
877
1006
|
|
|
878
1007
|
if (startIndex === this.start && endIndex === this.end) return;
|
|
879
1008
|
|
|
@@ -900,8 +1029,12 @@ export class VirtualRecyclerView<
|
|
|
900
1029
|
|
|
901
1030
|
// Keep anchor item stable to prevent scroll jump
|
|
902
1031
|
const anchorTopNew = this.offsetTopOf(anchorIndex);
|
|
903
|
-
const targetScroll =
|
|
904
|
-
|
|
1032
|
+
const targetScroll =
|
|
1033
|
+
this.containerTopInScroll() + anchorTopNew - anchorDelta;
|
|
1034
|
+
const maxScroll = Math.max(
|
|
1035
|
+
0,
|
|
1036
|
+
this.scrollEl.scrollHeight - this.scrollEl.clientHeight,
|
|
1037
|
+
);
|
|
905
1038
|
const clamped = Math.min(Math.max(0, targetScroll), maxScroll);
|
|
906
1039
|
|
|
907
1040
|
const heightChanged = Math.abs(anchorTopNew - anchorTop) > 1;
|
|
@@ -1036,4 +1169,4 @@ export class VirtualRecyclerView<
|
|
|
1036
1169
|
private totalHeight(count: number): number {
|
|
1037
1170
|
return this.fenwick.sum(count);
|
|
1038
1171
|
}
|
|
1039
|
-
}
|
|
1172
|
+
}
|
|
@@ -50,17 +50,19 @@ import { Lifecycle } from "./base/lifecycle";
|
|
|
50
50
|
*/
|
|
51
51
|
export class ModelManager<
|
|
52
52
|
TModel extends MixedItem,
|
|
53
|
-
TAdapter extends Adapter<MixedItem, ViewContract<any
|
|
53
|
+
TAdapter extends Adapter<MixedItem, ViewContract<any>>,
|
|
54
54
|
> extends Lifecycle {
|
|
55
55
|
private privModelList: Array<MixedItem> = [];
|
|
56
56
|
|
|
57
57
|
private privAdapter!: new (...args: any[]) => TAdapter;
|
|
58
58
|
|
|
59
|
-
private privAdapterHandle
|
|
59
|
+
private privAdapterHandle?: TAdapter;
|
|
60
60
|
|
|
61
|
-
private privRecyclerView!: new (
|
|
61
|
+
private privRecyclerView!: new (
|
|
62
|
+
...args: any[]
|
|
63
|
+
) => RecyclerViewContract<TAdapter>;
|
|
62
64
|
|
|
63
|
-
private privRecyclerViewHandle
|
|
65
|
+
private privRecyclerViewHandle?: RecyclerViewContract<TAdapter>;
|
|
64
66
|
|
|
65
67
|
private options: SelectiveOptions = null;
|
|
66
68
|
|
|
@@ -97,7 +99,9 @@ export class ModelManager<
|
|
|
97
99
|
* @param {new (...args: any[]) => RecyclerViewContract<TAdapter>} recyclerView - The recycler view constructor.
|
|
98
100
|
* @returns {void}
|
|
99
101
|
*/
|
|
100
|
-
public setupRecyclerView(
|
|
102
|
+
public setupRecyclerView(
|
|
103
|
+
recyclerView: new (...args: any[]) => RecyclerViewContract<TAdapter>,
|
|
104
|
+
): void {
|
|
101
105
|
this.privRecyclerView = recyclerView;
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -113,7 +117,9 @@ export class ModelManager<
|
|
|
113
117
|
* @param {Array<HTMLOptGroupElement | HTMLOptionElement>} modelData - Parsed DOM elements from the source `<select>`.
|
|
114
118
|
* @returns {Array<GroupModel | OptionModel>} The ordered list of group and option models.
|
|
115
119
|
*/
|
|
116
|
-
public createModelResources(
|
|
120
|
+
public createModelResources(
|
|
121
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
122
|
+
): Array<GroupModel | OptionModel> {
|
|
117
123
|
if (this.is(LifecycleState.INITIALIZED)) {
|
|
118
124
|
this.mount();
|
|
119
125
|
}
|
|
@@ -123,14 +129,26 @@ export class ModelManager<
|
|
|
123
129
|
|
|
124
130
|
modelData.forEach((data) => {
|
|
125
131
|
if (data.tagName === "OPTGROUP") {
|
|
126
|
-
currentGroup = new GroupModel(
|
|
132
|
+
currentGroup = new GroupModel(
|
|
133
|
+
this.options,
|
|
134
|
+
data as HTMLOptGroupElement,
|
|
135
|
+
);
|
|
127
136
|
this.privModelList.push(currentGroup);
|
|
128
137
|
} else if (data.tagName === "OPTION") {
|
|
129
|
-
const optionModel = new OptionModel(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
const optionModel = new OptionModel(
|
|
139
|
+
this.options,
|
|
140
|
+
data as HTMLOptionElement,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const parentGroup = data["__parentGroup"] as
|
|
144
|
+
| HTMLOptGroupElement
|
|
145
|
+
| undefined;
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
parentGroup &&
|
|
149
|
+
currentGroup &&
|
|
150
|
+
parentGroup === currentGroup.targetElement
|
|
151
|
+
) {
|
|
134
152
|
currentGroup.addItem(optionModel);
|
|
135
153
|
optionModel.group = currentGroup;
|
|
136
154
|
} else {
|
|
@@ -155,7 +173,9 @@ export class ModelManager<
|
|
|
155
173
|
* @returns {Promise<void>} Resolves when the adapter (if any) completes syncing.
|
|
156
174
|
* @see Adapter#syncFromSource
|
|
157
175
|
*/
|
|
158
|
-
public async replace(
|
|
176
|
+
public async replace(
|
|
177
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
178
|
+
): Promise<void> {
|
|
159
179
|
this.createModelResources(modelData);
|
|
160
180
|
|
|
161
181
|
if (this.privAdapterHandle) {
|
|
@@ -199,9 +219,9 @@ export class ModelManager<
|
|
|
199
219
|
public load<TExtra extends object = {}>(
|
|
200
220
|
viewElement: HTMLElement,
|
|
201
221
|
adapterOpt: Partial<TAdapter> = {},
|
|
202
|
-
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> &
|
|
222
|
+
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> &
|
|
223
|
+
TExtra = {} as any,
|
|
203
224
|
): void {
|
|
204
|
-
|
|
205
225
|
this.privAdapterHandle = new this.privAdapter(this.privModelList);
|
|
206
226
|
Object.assign(this.privAdapterHandle, adapterOpt);
|
|
207
227
|
|
|
@@ -230,7 +250,9 @@ export class ModelManager<
|
|
|
230
250
|
* @returns {void}
|
|
231
251
|
* @see Adapter#updateData
|
|
232
252
|
*/
|
|
233
|
-
public updateModel(
|
|
253
|
+
public updateModel(
|
|
254
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
255
|
+
): void {
|
|
234
256
|
const oldModels = this.privModelList;
|
|
235
257
|
const newModels: Array<MixedItem> = [];
|
|
236
258
|
|
|
@@ -256,9 +278,10 @@ export class ModelManager<
|
|
|
256
278
|
|
|
257
279
|
if (existingGroup) {
|
|
258
280
|
// Label is used as key; keep original behavior.
|
|
259
|
-
const hasLabelChange =
|
|
281
|
+
const hasLabelChange =
|
|
282
|
+
existingGroup.label !== dataVset.label;
|
|
260
283
|
if (hasLabelChange) {
|
|
261
|
-
existingGroup.updateTarget(dataVset)
|
|
284
|
+
existingGroup.updateTarget(dataVset);
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
existingGroup.position = position;
|
|
@@ -283,7 +306,9 @@ export class ModelManager<
|
|
|
283
306
|
existingOption.updateTarget(dataVset);
|
|
284
307
|
existingOption.position = position;
|
|
285
308
|
|
|
286
|
-
const parentGroup = dataVset[
|
|
309
|
+
const parentGroup = dataVset[
|
|
310
|
+
"__parentGroup"
|
|
311
|
+
] as HTMLOptGroupElement;
|
|
287
312
|
|
|
288
313
|
if (parentGroup && currentGroup) {
|
|
289
314
|
currentGroup.addItem(existingOption);
|
|
@@ -298,7 +323,9 @@ export class ModelManager<
|
|
|
298
323
|
const newOption = new OptionModel(this.options, dataVset);
|
|
299
324
|
newOption.position = position;
|
|
300
325
|
|
|
301
|
-
const parentGroup = dataVset[
|
|
326
|
+
const parentGroup = dataVset[
|
|
327
|
+
"__parentGroup"
|
|
328
|
+
] as HTMLOptGroupElement;
|
|
302
329
|
|
|
303
330
|
if (parentGroup && currentGroup) {
|
|
304
331
|
currentGroup.addItem(newOption);
|
|
@@ -438,4 +465,4 @@ export class ModelManager<
|
|
|
438
465
|
public triggerChanged(event_name: string): Promise<void> {
|
|
439
466
|
return this.privAdapterHandle?.changeProp(event_name);
|
|
440
467
|
}
|
|
441
|
-
}
|
|
468
|
+
}
|