@acorex/components 22.4.29 → 22.4.31

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.
@@ -1,4 +1,4 @@
1
- import { AXDataSource, retryFrames, scrollNearestIntoView, AXListDataSource, NXValueComponent, AXComponent, AXClearableComponent, AXClosableComponent, AXValuableComponent } from '@acorex/cdk/common';
1
+ import { AXDataSource, retryFrames, scrollNearestIntoView, NXValueComponent, AXComponent, AXClearableComponent, AXClosableComponent, AXValuableComponent } from '@acorex/cdk/common';
2
2
  import { AXButtonComponent, AXDecoratorCloseButtonComponent } from '@acorex/components/button';
3
3
  import { AXCheckBoxComponent } from '@acorex/components/check-box';
4
4
  import { AXChipsComponent } from '@acorex/components/chips';
@@ -18,9 +18,9 @@ import { AXListNavigationDirective, AXListNavigationItemDirective, AXListNavigat
18
18
  import { AXHighlightTextComponent } from '@acorex/components/highlight';
19
19
  import * as i1 from '@angular/cdk/scrolling';
20
20
  import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
21
+ import { auditTime, fromEvent } from 'rxjs';
21
22
  import { AXTreeViewComponent } from '@acorex/components/tree-view';
22
23
  import { AXDataTableComponent, AXDataTableTextColumnComponent } from '@acorex/components/data-table';
23
- import { fromEvent, auditTime } from 'rxjs';
24
24
 
25
25
  /** Whether a tree node may have descendants (embedded children or lazy metadata). */
26
26
  function lookupTreeNodeMayHaveChildren(node) {
@@ -483,6 +483,10 @@ function scrollTreeToSelectedNode(host, selectedId, attempts = 5) {
483
483
  /**
484
484
  * Shared `AXDataSource` + CDK virtual scroll plumbing for the list-based lookup views
485
485
  * (`ax-lookup-drop-down-list`, `ax-lookup-multi-select`).
486
+ *
487
+ * Loads pages sequentially on scroll: only one API request at a time; further scroll
488
+ * events are ignored until the current request resolves, then the scroll position is
489
+ * re-checked for the next page.
486
490
  */
487
491
  class AXLookupListViewBase {
488
492
  /** Whether the filtered datasource currently has no items. */
@@ -506,6 +510,47 @@ class AXLookupListViewBase {
506
510
  #syncLoadingState(source, isLoading) {
507
511
  this.isLoading.set(isLoading || this.#hasPendingInitialLoad(source));
508
512
  }
513
+ #syncLoadedFromCache(source) {
514
+ const cached = source.cachedItems;
515
+ const items = [];
516
+ for (let index = 0; index < cached.length; index++) {
517
+ const item = cached[index];
518
+ if (item == null) {
519
+ break;
520
+ }
521
+ items.push(item);
522
+ }
523
+ this.loadedItems.set(items);
524
+ const total = source.totalCount;
525
+ const pendingMore = items.length < total;
526
+ const pendingInitial = total === 0 && (source.isLoading || this.#hasPendingInitialLoad(source));
527
+ this.hasMore.set(pendingMore || pendingInitial);
528
+ }
529
+ #tryLoadMore() {
530
+ const viewport = this.viewportRef();
531
+ const source = this.dataSource();
532
+ if (!viewport || source.isLoading || source.totalCount === 0) {
533
+ return;
534
+ }
535
+ const loaded = this.loadedItems().length;
536
+ if (loaded >= source.totalCount) {
537
+ return;
538
+ }
539
+ const threshold = this.itemHeight() * 2;
540
+ const bottomOffset = viewport.measureScrollOffset('bottom');
541
+ if (bottomOffset > threshold) {
542
+ return;
543
+ }
544
+ const nextPage = Math.floor(loaded / source.pageSize);
545
+ source.setPage(nextPage);
546
+ }
547
+ #scheduleTryLoadMore() {
548
+ if (typeof requestAnimationFrame === 'undefined') {
549
+ this.#tryLoadMore();
550
+ return;
551
+ }
552
+ requestAnimationFrame(() => this.#tryLoadMore());
553
+ }
509
554
  constructor() {
510
555
  /**
511
556
  * The datasource providing the items (paged, remote-capable).
@@ -588,9 +633,21 @@ class AXLookupListViewBase {
588
633
  * item, or a printable key that should return focus to the trigger search input).
589
634
  */
590
635
  this.navigateOut = output();
591
- /** CDK virtual scroll adapter over the datasource. */
592
- this.listDataSource = signal(null, /* @ts-ignore */
593
- ...(ngDevMode ? [{ debugName: "listDataSource" }] : /* istanbul ignore next */ []));
636
+ /** Rows loaded so far (contiguous from the start of the result set). */
637
+ this.loadedItems = signal([], /* @ts-ignore */
638
+ ...(ngDevMode ? [{ debugName: "loadedItems" }] : /* istanbul ignore next */ []));
639
+ /** When true, more pages exist and a trailing loading row is shown. */
640
+ this.hasMore = signal(false, /* @ts-ignore */
641
+ ...(ngDevMode ? [{ debugName: "hasMore" }] : /* istanbul ignore next */ []));
642
+ /** Virtual-scroll data: loaded rows plus an optional `null` loading sentinel. */
643
+ this.renderedItems = computed(() => {
644
+ const items = this.loadedItems();
645
+ if (this.hasMore()) {
646
+ return [...items, null];
647
+ }
648
+ return items;
649
+ }, /* @ts-ignore */
650
+ ...(ngDevMode ? [{ debugName: "renderedItems" }] : /* istanbul ignore next */ []));
594
651
  this.totalCount = signal(0, /* @ts-ignore */
595
652
  ...(ngDevMode ? [{ debugName: "totalCount" }] : /* istanbul ignore next */ []));
596
653
  this.isLoading = signal(true, /* @ts-ignore */
@@ -599,7 +656,7 @@ class AXLookupListViewBase {
599
656
  ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
600
657
  /** Viewport height so the popup grows with the items up to `maxVisibleItems`. */
601
658
  this.viewportHeight = computed(() => {
602
- const count = this.totalCount() || (this.isLoading() ? 3 : 1);
659
+ const count = this.renderedItems().length || (this.isLoading() ? 3 : 1);
603
660
  return Math.min(count, this.maxVisibleItems()) * this.itemHeight();
604
661
  }, /* @ts-ignore */
605
662
  ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
@@ -629,18 +686,36 @@ class AXLookupListViewBase {
629
686
  observer.observe(viewport.elementRef.nativeElement);
630
687
  onCleanup(() => observer.disconnect());
631
688
  });
689
+ effect((onCleanup) => {
690
+ const viewport = this.viewportRef();
691
+ if (!viewport) {
692
+ return;
693
+ }
694
+ const sub = viewport
695
+ .elementScrolled()
696
+ .pipe(auditTime(50))
697
+ .subscribe(() => {
698
+ this.#ngZone.run(() => this.#tryLoadMore());
699
+ });
700
+ onCleanup(() => sub.unsubscribe());
701
+ });
632
702
  effect((onCleanup) => {
633
703
  const source = this.dataSource();
634
- const list = new AXListDataSource({ source });
635
704
  untracked(() => {
636
705
  this.totalCount.set(source.totalCount);
637
706
  this.#syncLoadingState(source, source.isLoading);
638
- this.listDataSource.set(list);
707
+ this.#syncLoadedFromCache(source);
639
708
  this.#pendingSelectedScroll = true;
640
709
  });
641
- const loadingSub = source.onLoadingChanged.subscribe((value) => this.#syncLoadingState(source, value));
710
+ const loadingSub = source.onLoadingChanged.subscribe((value) => {
711
+ this.#syncLoadingState(source, value);
712
+ if (!value) {
713
+ this.#scheduleTryLoadMore();
714
+ }
715
+ });
642
716
  const changedSub = source.onChanged.subscribe((e) => {
643
717
  this.totalCount.set(e.totalCount);
718
+ this.#syncLoadedFromCache(source);
644
719
  if (e.page === 0) {
645
720
  this.#pendingSelectedScroll = true;
646
721
  }
@@ -648,6 +723,18 @@ class AXLookupListViewBase {
648
723
  this.#syncLoadingState(source, false);
649
724
  }
650
725
  });
726
+ queueMicrotask(() => {
727
+ if (!source.cachedItems.some((item) => item != null)) {
728
+ source.setPage(0);
729
+ }
730
+ else {
731
+ untracked(() => {
732
+ this.#syncLoadedFromCache(source);
733
+ this.totalCount.set(source.totalCount);
734
+ this.#scheduleTryLoadMore();
735
+ });
736
+ }
737
+ });
651
738
  onCleanup(() => {
652
739
  loadingSub.unsubscribe();
653
740
  changedSub.unsubscribe();
@@ -657,12 +744,11 @@ class AXLookupListViewBase {
657
744
  effect(() => {
658
745
  const viewport = this.viewportRef();
659
746
  const loading = this.isLoading();
660
- const count = this.totalCount();
747
+ const renderedCount = this.renderedItems().length;
661
748
  const values = this.selectedValues();
662
749
  const field = this.valueField();
663
750
  const height = this.itemHeight();
664
- const source = this.dataSource();
665
- if (!viewport || loading || count === 0) {
751
+ if (!viewport || loading || renderedCount === 0) {
666
752
  return;
667
753
  }
668
754
  if (!this.#pendingSelectedScroll) {
@@ -673,22 +759,25 @@ class AXLookupListViewBase {
673
759
  return;
674
760
  }
675
761
  untracked(() => {
762
+ if (typeof requestAnimationFrame === 'undefined') {
763
+ return;
764
+ }
676
765
  requestAnimationFrame(() => {
677
766
  viewport.checkViewportSize();
678
- const index = this.#resolveSelectedIndex(source, values, field);
767
+ const index = this.#resolveSelectedIndex(values, field);
679
768
  if (index >= 0) {
680
- scrollNearestByOffset(viewport, index, height, count);
769
+ scrollNearestByOffset(viewport, index, height, renderedCount);
681
770
  }
682
771
  });
683
772
  });
684
773
  });
685
774
  }
686
- #resolveSelectedIndex(source, values, valueField) {
775
+ #resolveSelectedIndex(values, valueField) {
687
776
  const selectedValue = values[0];
688
777
  if (selectedValue == null) {
689
778
  return -1;
690
779
  }
691
- const items = source.cachedItems;
780
+ const items = this.loadedItems();
692
781
  for (let index = 0; index < items.length; index++) {
693
782
  const item = items[index];
694
783
  if (item != null && item[valueField] == selectedValue) {
@@ -776,13 +865,17 @@ class AXLookupListViewBase {
776
865
  if (key === 'ArrowDown' && nav && viewport) {
777
866
  const items = this.navItems();
778
867
  if (items.length > 0 && nav.activeIndex() >= items.length - 1) {
779
- const maxOffset = Math.max(0, this.totalCount() * this.itemHeight() - viewport.getViewportSize());
868
+ const renderedCount = this.renderedItems().length;
869
+ const maxOffset = Math.max(0, renderedCount * this.itemHeight() - viewport.getViewportSize());
780
870
  const nextOffset = Math.min(maxOffset, viewport.measureScrollOffset() + this.itemHeight());
781
871
  if (nextOffset > viewport.measureScrollOffset()) {
782
872
  e.nativeEvent.preventDefault();
783
873
  viewport.scrollToOffset(nextOffset);
784
874
  queueMicrotask(() => nav.focusOnIndex(this.navItems().length - 1));
785
875
  }
876
+ else {
877
+ this.#tryLoadMore();
878
+ }
786
879
  return;
787
880
  }
788
881
  }
@@ -835,7 +928,7 @@ class AXLookupDropDownListComponent extends AXLookupListViewBase {
835
928
  (onKeypress)="handleListKeypress($event)"
836
929
  >
837
930
  <div
838
- *cdkVirtualFor="let item of listDataSource(); let i = index; trackBy: trackByIndex"
931
+ *cdkVirtualFor="let item of renderedItems(); let i = index; trackBy: trackByIndex"
839
932
  role="option"
840
933
  class="ax-lookup-option-host"
841
934
  axListNavigationItem
@@ -909,7 +1002,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
909
1002
  (onKeypress)="handleListKeypress($event)"
910
1003
  >
911
1004
  <div
912
- *cdkVirtualFor="let item of listDataSource(); let i = index; trackBy: trackByIndex"
1005
+ *cdkVirtualFor="let item of renderedItems(); let i = index; trackBy: trackByIndex"
913
1006
  role="option"
914
1007
  class="ax-lookup-option-host"
915
1008
  axListNavigationItem
@@ -1606,6 +1699,24 @@ class AXLookupMultiColumnComponent {
1606
1699
  const changedSub = source.onChanged.subscribe((event) => {
1607
1700
  this.#applyLoadedItems(event.cachedItems, event.totalCount);
1608
1701
  });
1702
+ const loadingSub = source.onLoadingChanged.subscribe((isLoading) => {
1703
+ if (isLoading) {
1704
+ return;
1705
+ }
1706
+ const run = () => {
1707
+ const wrapper = this.tableRef()?.getHostElement()?.querySelector('.ax-data-table-wrapper') ??
1708
+ this.#host.nativeElement.querySelector('.ax-data-table-wrapper');
1709
+ if (wrapper) {
1710
+ this.#tryLoadMore(wrapper);
1711
+ }
1712
+ };
1713
+ if (typeof requestAnimationFrame !== 'undefined') {
1714
+ requestAnimationFrame(run);
1715
+ }
1716
+ else {
1717
+ run();
1718
+ }
1719
+ });
1609
1720
  // Fetch page 0 only when the source has never loaded. Reopen remounts this
1610
1721
  // view; setPage(0) is a no-op on a warm source and must not force a new API call.
1611
1722
  queueMicrotask(() => {
@@ -1613,7 +1724,10 @@ class AXLookupMultiColumnComponent {
1613
1724
  source.setPage(0);
1614
1725
  }
1615
1726
  });
1616
- onCleanup(() => changedSub.unsubscribe());
1727
+ onCleanup(() => {
1728
+ changedSub.unsubscribe();
1729
+ loadingSub.unsubscribe();
1730
+ });
1617
1731
  });
1618
1732
  effect((onCleanup) => {
1619
1733
  // Rebind when the table instance appears / source changes.
@@ -2047,7 +2161,7 @@ class AXLookupMultiSelectComponent extends AXLookupListViewBase {
2047
2161
  (onKeypress)="handleListKeypress($event)"
2048
2162
  >
2049
2163
  <div
2050
- *cdkVirtualFor="let item of listDataSource(); let i = index; trackBy: trackByIndex"
2164
+ *cdkVirtualFor="let item of renderedItems(); let i = index; trackBy: trackByIndex"
2051
2165
  role="option"
2052
2166
  class="ax-lookup-option-host ax-lookup-option-multiple"
2053
2167
  axListNavigationItem
@@ -2125,7 +2239,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
2125
2239
  (onKeypress)="handleListKeypress($event)"
2126
2240
  >
2127
2241
  <div
2128
- *cdkVirtualFor="let item of listDataSource(); let i = index; trackBy: trackByIndex"
2242
+ *cdkVirtualFor="let item of renderedItems(); let i = index; trackBy: trackByIndex"
2129
2243
  role="option"
2130
2244
  class="ax-lookup-option-host ax-lookup-option-multiple"
2131
2245
  axListNavigationItem