@symbiote-native/angular 0.1.0
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/LICENSE +21 -0
- package/README.md +147 -0
- package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
- package/build/angular/components/activity-indicator/index.android.js +21 -0
- package/build/angular/components/activity-indicator/index.android.js.map +1 -0
- package/build/angular/components/activity-indicator/index.d.ts +1 -0
- package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
- package/build/angular/components/activity-indicator/index.ios.js +21 -0
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
- package/build/angular/components/activity-indicator/index.js +4 -0
- package/build/angular/components/activity-indicator/index.js.map +1 -0
- package/build/angular/components/activity-indicator/shared.d.ts +67 -0
- package/build/angular/components/activity-indicator/shared.js +260 -0
- package/build/angular/components/activity-indicator/shared.js.map +1 -0
- package/build/angular/components/button.d.ts +63 -0
- package/build/angular/components/button.js +332 -0
- package/build/angular/components/button.js.map +1 -0
- package/build/angular/components/drawer-layout-android/index.android.d.ts +34 -0
- package/build/angular/components/drawer-layout-android/index.android.js +291 -0
- package/build/angular/components/drawer-layout-android/index.android.js.map +1 -0
- package/build/angular/components/drawer-layout-android/index.d.ts +11 -0
- package/build/angular/components/drawer-layout-android/index.js +109 -0
- package/build/angular/components/drawer-layout-android/index.js.map +1 -0
- package/build/angular/components/drawer-layout-android/shared.d.ts +74 -0
- package/build/angular/components/drawer-layout-android/shared.js +138 -0
- package/build/angular/components/drawer-layout-android/shared.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +122 -0
- package/build/angular/components/flat-list/index.js +689 -0
- package/build/angular/components/flat-list/index.js.map +1 -0
- package/build/angular/components/image/index.android.d.ts +10 -0
- package/build/angular/components/image/index.android.js +59 -0
- package/build/angular/components/image/index.android.js.map +1 -0
- package/build/angular/components/image/index.d.ts +1 -0
- package/build/angular/components/image/index.ios.d.ts +10 -0
- package/build/angular/components/image/index.ios.js +59 -0
- package/build/angular/components/image/index.ios.js.map +1 -0
- package/build/angular/components/image/index.js +3 -0
- package/build/angular/components/image/index.js.map +1 -0
- package/build/angular/components/image/shared.d.ts +101 -0
- package/build/angular/components/image/shared.js +408 -0
- package/build/angular/components/image/shared.js.map +1 -0
- package/build/angular/components/image-background.d.ts +81 -0
- package/build/angular/components/image-background.js +418 -0
- package/build/angular/components/image-background.js.map +1 -0
- package/build/angular/components/input-accessory-view.d.ts +59 -0
- package/build/angular/components/input-accessory-view.js +266 -0
- package/build/angular/components/input-accessory-view.js.map +1 -0
- package/build/angular/components/keyboard-avoiding-view.d.ts +82 -0
- package/build/angular/components/keyboard-avoiding-view.js +363 -0
- package/build/angular/components/keyboard-avoiding-view.js.map +1 -0
- package/build/angular/components/modal.d.ts +92 -0
- package/build/angular/components/modal.js +365 -0
- package/build/angular/components/modal.js.map +1 -0
- package/build/angular/components/pressable/index.d.ts +123 -0
- package/build/angular/components/pressable/index.js +421 -0
- package/build/angular/components/pressable/index.js.map +1 -0
- package/build/angular/components/refresh-control.d.ts +86 -0
- package/build/angular/components/refresh-control.js +347 -0
- package/build/angular/components/refresh-control.js.map +1 -0
- package/build/angular/components/safe-area-view.d.ts +57 -0
- package/build/angular/components/safe-area-view.js +253 -0
- package/build/angular/components/safe-area-view.js.map +1 -0
- package/build/angular/components/scroll-view/index.android.d.ts +21 -0
- package/build/angular/components/scroll-view/index.android.js +235 -0
- package/build/angular/components/scroll-view/index.android.js.map +1 -0
- package/build/angular/components/scroll-view/index.d.ts +3 -0
- package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
- package/build/angular/components/scroll-view/index.ios.js +86 -0
- package/build/angular/components/scroll-view/index.ios.js.map +1 -0
- package/build/angular/components/scroll-view/index.js +6 -0
- package/build/angular/components/scroll-view/index.js.map +1 -0
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +312 -0
- package/build/angular/components/scroll-view/projection.js.map +1 -0
- package/build/angular/components/scroll-view/shared.d.ts +230 -0
- package/build/angular/components/scroll-view/shared.js +679 -0
- package/build/angular/components/scroll-view/shared.js.map +1 -0
- package/build/angular/components/scroll-view/sticky-header.d.ts +41 -0
- package/build/angular/components/scroll-view/sticky-header.js +208 -0
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
- package/build/angular/components/section-list/index.d.ts +116 -0
- package/build/angular/components/section-list/index.js +641 -0
- package/build/angular/components/section-list/index.js.map +1 -0
- package/build/angular/components/switch/index.android.d.ts +20 -0
- package/build/angular/components/switch/index.android.js +70 -0
- package/build/angular/components/switch/index.android.js.map +1 -0
- package/build/angular/components/switch/index.d.ts +1 -0
- package/build/angular/components/switch/index.ios.d.ts +19 -0
- package/build/angular/components/switch/index.ios.js +69 -0
- package/build/angular/components/switch/index.ios.js.map +1 -0
- package/build/angular/components/switch/index.js +4 -0
- package/build/angular/components/switch/index.js.map +1 -0
- package/build/angular/components/switch/shared.d.ts +40 -0
- package/build/angular/components/switch/shared.js +127 -0
- package/build/angular/components/switch/shared.js.map +1 -0
- package/build/angular/components/text-input.d.ts +153 -0
- package/build/angular/components/text-input.js +604 -0
- package/build/angular/components/text-input.js.map +1 -0
- package/build/angular/components/touchable-native-feedback.d.ts +83 -0
- package/build/angular/components/touchable-native-feedback.js +376 -0
- package/build/angular/components/touchable-native-feedback.js.map +1 -0
- package/build/angular/components/touchable.d.ts +248 -0
- package/build/angular/components/touchable.js +1206 -0
- package/build/angular/components/touchable.js.map +1 -0
- package/build/angular/components/virtualized-list/directives.d.ts +54 -0
- package/build/angular/components/virtualized-list/directives.js +154 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +268 -0
- package/build/angular/components/virtualized-list/index.js +1162 -0
- package/build/angular/components/virtualized-list/index.js.map +1 -0
- package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
- package/build/angular/components/virtualized-section-list/directives.js +77 -0
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
- package/build/angular/components/virtualized-section-list/index.js +722 -0
- package/build/angular/components/virtualized-section-list/index.js.map +1 -0
- package/build/angular/components.d.ts +39 -0
- package/build/angular/components.js +38 -0
- package/build/angular/components.js.map +1 -0
- package/build/angular/create-portal.d.ts +22 -0
- package/build/angular/create-portal.js +74 -0
- package/build/angular/create-portal.js.map +1 -0
- package/build/angular/create-tunnel.d.ts +34 -0
- package/build/angular/create-tunnel.js +126 -0
- package/build/angular/create-tunnel.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +22 -0
- package/build/angular/descriptor-to-angular/index.js +142 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +37 -0
- package/build/angular/host-instance/index.js.map +1 -0
- package/build/angular/index.d.ts +21 -0
- package/build/angular/index.js +42 -0
- package/build/angular/index.js.map +1 -0
- package/build/angular/modules/animated/create-animated-component.d.ts +68 -0
- package/build/angular/modules/animated/create-animated-component.js +429 -0
- package/build/angular/modules/animated/create-animated-component.js.map +1 -0
- package/build/angular/modules/animated/index.d.ts +51 -0
- package/build/angular/modules/animated/index.js +59 -0
- package/build/angular/modules/animated/index.js.map +1 -0
- package/build/angular/modules/app-registry/index.d.ts +7 -0
- package/build/angular/modules/app-registry/index.js +23 -0
- package/build/angular/modules/app-registry/index.js.map +1 -0
- package/build/angular/modules/status-bar.d.ts +20 -0
- package/build/angular/modules/status-bar.js +59 -0
- package/build/angular/modules/status-bar.js.map +1 -0
- package/build/angular/primitives/index.d.ts +63 -0
- package/build/angular/primitives/index.js +189 -0
- package/build/angular/primitives/index.js.map +1 -0
- package/build/angular/primitives/shared.d.ts +89 -0
- package/build/angular/primitives/shared.js +172 -0
- package/build/angular/primitives/shared.js.map +1 -0
- package/build/angular/render.d.ts +11 -0
- package/build/angular/render.js +161 -0
- package/build/angular/render.js.map +1 -0
- package/build/angular/renderer.d.ts +38 -0
- package/build/angular/renderer.js +360 -0
- package/build/angular/renderer.js.map +1 -0
- package/build/angular/services/color-scheme.service.d.ts +16 -0
- package/build/angular/services/color-scheme.service.js +31 -0
- package/build/angular/services/color-scheme.service.js.map +1 -0
- package/build/angular/services/index.d.ts +2 -0
- package/build/angular/services/index.js +3 -0
- package/build/angular/services/index.js.map +1 -0
- package/build/angular/services/window-dimensions.service.d.ts +16 -0
- package/build/angular/services/window-dimensions.service.js +42 -0
- package/build/angular/services/window-dimensions.service.js.map +1 -0
- package/build/components/activity-indicator/index.android.d.ts +9 -0
- package/build/components/activity-indicator/index.android.js +64 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +5 -0
- package/build/components/activity-indicator/index.ios.js +64 -0
- package/build/components/activity-indicator/index.js +3 -0
- package/build/components/activity-indicator/shared.d.ts +64 -0
- package/build/components/activity-indicator/shared.js +424 -0
- package/build/components/button.d.ts +60 -0
- package/build/components/button.js +456 -0
- package/build/components/drawer-layout-android/index.android.d.ts +29 -0
- package/build/components/drawer-layout-android/index.android.js +326 -0
- package/build/components/drawer-layout-android/index.d.ts +8 -0
- package/build/components/drawer-layout-android/index.js +145 -0
- package/build/components/drawer-layout-android/shared.d.ts +71 -0
- package/build/components/drawer-layout-android/shared.js +205 -0
- package/build/components/flat-list/index.d.ts +119 -0
- package/build/components/flat-list/index.js +692 -0
- package/build/components/image/index.android.d.ts +7 -0
- package/build/components/image/index.android.js +88 -0
- package/build/components/image/index.d.ts +1 -0
- package/build/components/image/index.ios.d.ts +7 -0
- package/build/components/image/index.ios.js +88 -0
- package/build/components/image/index.js +2 -0
- package/build/components/image/shared.d.ts +101 -0
- package/build/components/image/shared.js +407 -0
- package/build/components/image-background.d.ts +78 -0
- package/build/components/image-background.js +583 -0
- package/build/components/input-accessory-view.d.ts +56 -0
- package/build/components/input-accessory-view.js +407 -0
- package/build/components/keyboard-avoiding-view.d.ts +79 -0
- package/build/components/keyboard-avoiding-view.js +509 -0
- package/build/components/modal.d.ts +89 -0
- package/build/components/modal.js +534 -0
- package/build/components/pressable/index.d.ts +120 -0
- package/build/components/pressable/index.js +640 -0
- package/build/components/refresh-control.d.ts +83 -0
- package/build/components/refresh-control.js +514 -0
- package/build/components/safe-area-view.d.ts +54 -0
- package/build/components/safe-area-view.js +393 -0
- package/build/components/scroll-view/index.android.d.ts +16 -0
- package/build/components/scroll-view/index.android.js +228 -0
- package/build/components/scroll-view/index.d.ts +3 -0
- package/build/components/scroll-view/index.ios.d.ts +6 -0
- package/build/components/scroll-view/index.ios.js +113 -0
- package/build/components/scroll-view/index.js +5 -0
- package/build/components/scroll-view/projection.d.ts +32 -0
- package/build/components/scroll-view/projection.js +311 -0
- package/build/components/scroll-view/shared.d.ts +225 -0
- package/build/components/scroll-view/shared.js +757 -0
- package/build/components/scroll-view/sticky-header.d.ts +38 -0
- package/build/components/scroll-view/sticky-header.js +257 -0
- package/build/components/section-list/index.d.ts +113 -0
- package/build/components/section-list/index.js +768 -0
- package/build/components/switch/index.android.d.ts +17 -0
- package/build/components/switch/index.android.js +107 -0
- package/build/components/switch/index.d.ts +1 -0
- package/build/components/switch/index.ios.d.ts +16 -0
- package/build/components/switch/index.ios.js +106 -0
- package/build/components/switch/index.js +3 -0
- package/build/components/switch/shared.d.ts +37 -0
- package/build/components/switch/shared.js +176 -0
- package/build/components/text-input.d.ts +150 -0
- package/build/components/text-input.js +826 -0
- package/build/components/touchable-native-feedback.d.ts +80 -0
- package/build/components/touchable-native-feedback.js +550 -0
- package/build/components/touchable.d.ts +241 -0
- package/build/components/touchable.js +1672 -0
- package/build/components/virtualized-list/directives.d.ts +41 -0
- package/build/components/virtualized-list/directives.js +250 -0
- package/build/components/virtualized-list/index.d.ts +265 -0
- package/build/components/virtualized-list/index.js +1344 -0
- package/build/components/virtualized-section-list/directives.d.ts +28 -0
- package/build/components/virtualized-section-list/directives.js +149 -0
- package/build/components/virtualized-section-list/index.d.ts +175 -0
- package/build/components/virtualized-section-list/index.js +854 -0
- package/build/components.d.ts +39 -0
- package/build/components.js +37 -0
- package/build/create-portal.d.ts +17 -0
- package/build/create-portal.js +128 -0
- package/build/create-tunnel.d.ts +29 -0
- package/build/create-tunnel.js +181 -0
- package/build/descriptor-to-angular/index.d.ts +19 -0
- package/build/descriptor-to-angular/index.js +186 -0
- package/build/host-instance/index.d.ts +2 -0
- package/build/host-instance/index.js +36 -0
- package/build/index.d.ts +21 -0
- package/build/index.js +41 -0
- package/build/modules/animated/create-animated-component.d.ts +57 -0
- package/build/modules/animated/create-animated-component.js +490 -0
- package/build/modules/animated/index.d.ts +51 -0
- package/build/modules/animated/index.js +58 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +22 -0
- package/build/modules/status-bar.d.ts +17 -0
- package/build/modules/status-bar.js +121 -0
- package/build/primitives/index.d.ts +32 -0
- package/build/primitives/index.js +386 -0
- package/build/primitives/shared.d.ts +84 -0
- package/build/primitives/shared.js +231 -0
- package/build/render.d.ts +11 -0
- package/build/render.js +160 -0
- package/build/renderer.d.ts +38 -0
- package/build/renderer.js +359 -0
- package/build/services/color-scheme.service.d.ts +13 -0
- package/build/services/color-scheme.service.js +72 -0
- package/build/services/index.d.ts +2 -0
- package/build/services/index.js +2 -0
- package/build/services/window-dimensions.service.d.ts +13 -0
- package/build/services/window-dimensions.service.js +83 -0
- package/metro-css-parser.cjs +11 -0
- package/package.json +57 -0
- package/src/__tests__/responder-change-detection.test.ts +102 -0
- package/src/__tests__/responder-nested-cd.test.ts +113 -0
- package/src/angular-gaps.test.ts +142 -0
- package/src/angular-imports.test.ts +100 -0
- package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
- package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
- package/src/components/activity-indicator/index.android.ts +16 -0
- package/src/components/activity-indicator/index.ios.ts +17 -0
- package/src/components/activity-indicator/index.ts +4 -0
- package/src/components/activity-indicator/shared.ts +206 -0
- package/src/components/button.ts +193 -0
- package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
- package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
- package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
- package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
- package/src/components/flat-list/flat-list.test.ts +99 -0
- package/src/components/flat-list/index.ts +575 -0
- package/src/components/image/image-class-style.test.ts +46 -0
- package/src/components/image/index.android.ts +48 -0
- package/src/components/image/index.ios.ts +48 -0
- package/src/components/image/index.ts +3 -0
- package/src/components/image/shared.ts +478 -0
- package/src/components/image-background-class-style.test.ts +48 -0
- package/src/components/image-background-style-class.test.ts +79 -0
- package/src/components/image-background.ts +270 -0
- package/src/components/input-accessory-view.test.ts +43 -0
- package/src/components/input-accessory-view.ts +191 -0
- package/src/components/keyboard-avoiding-view.test.ts +54 -0
- package/src/components/keyboard-avoiding-view.ts +308 -0
- package/src/components/modal.test.ts +43 -0
- package/src/components/modal.ts +304 -0
- package/src/components/pressable/index.ts +371 -0
- package/src/components/pressable/pressable.test.ts +85 -0
- package/src/components/refresh-control.test.ts +41 -0
- package/src/components/refresh-control.ts +288 -0
- package/src/components/safe-area-view.test.ts +43 -0
- package/src/components/safe-area-view.ts +178 -0
- package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
- package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
- package/src/components/scroll-view/index.android.ts +190 -0
- package/src/components/scroll-view/index.ios.ts +79 -0
- package/src/components/scroll-view/index.ts +7 -0
- package/src/components/scroll-view/projection.ts +384 -0
- package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
- package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
- package/src/components/scroll-view/shared.ts +836 -0
- package/src/components/scroll-view/sticky-header.ts +230 -0
- package/src/components/section-list/index.ts +445 -0
- package/src/components/switch/android-switch-class.test.ts +42 -0
- package/src/components/switch/index.android.ts +61 -0
- package/src/components/switch/index.ios.ts +60 -0
- package/src/components/switch/index.ts +4 -0
- package/src/components/switch/shared.ts +197 -0
- package/src/components/switch/switch.test.ts +92 -0
- package/src/components/text-input-class-style.test.ts +54 -0
- package/src/components/text-input.ts +536 -0
- package/src/components/touchable-native-feedback.test.ts +49 -0
- package/src/components/touchable-native-feedback.ts +260 -0
- package/src/components/touchable.test.ts +102 -0
- package/src/components/touchable.ts +643 -0
- package/src/components/virtualized-list/directives.ts +169 -0
- package/src/components/virtualized-list/index.ts +1204 -0
- package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
- package/src/components/virtualized-section-list/directives.ts +91 -0
- package/src/components/virtualized-section-list/index.ts +598 -0
- package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
- package/src/components.ts +149 -0
- package/src/create-portal.test.ts +90 -0
- package/src/create-portal.ts +74 -0
- package/src/create-tunnel.test.ts +120 -0
- package/src/create-tunnel.ts +136 -0
- package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
- package/src/descriptor-to-angular/index.ts +190 -0
- package/src/host-instance/host-instance.test.ts +49 -0
- package/src/host-instance/index.ts +36 -0
- package/src/index.ts +257 -0
- package/src/modules/animated/animated-scroll-view.test.ts +85 -0
- package/src/modules/animated/create-animated-component.ts +435 -0
- package/src/modules/animated/index.ts +105 -0
- package/src/modules/app-registry/app-registry.test.ts +106 -0
- package/src/modules/app-registry/index.ts +60 -0
- package/src/modules/status-bar.test.ts +50 -0
- package/src/modules/status-bar.ts +57 -0
- package/src/primitives/index.ts +119 -0
- package/src/primitives/shared.ts +191 -0
- package/src/render.test.ts +147 -0
- package/src/render.ts +224 -0
- package/src/renderer.test.ts +179 -0
- package/src/renderer.ts +407 -0
- package/src/services/color-scheme.service.ts +27 -0
- package/src/services/index.ts +2 -0
- package/src/services/services.test.ts +163 -0
- package/src/services/window-dimensions.service.ts +41 -0
|
@@ -0,0 +1,1204 @@
|
|
|
1
|
+
// VirtualizedList, the Angular lifecycle half. The windowing engine (offset table, window
|
|
2
|
+
// compute, batch throttle, viewability, edge-reached, the child PLAN, the imperative-handle
|
|
3
|
+
// geometry) lives in @symbiote-native/components/state, shared verbatim with the React and Vue adapters
|
|
4
|
+
// (<adapters_reach_full_feature_parity>). Here Angular supplies only the lifecycle: plain fields
|
|
5
|
+
// for scroll offset / viewport / measurement bumps, a once-per-CD metrics + view recompute in
|
|
6
|
+
// ngDoCheck (the Angular twin of React render / Vue `computed` — it owns the controlled
|
|
7
|
+
// committedWindow throttle and assembles the windowed cells before the template bindings are read),
|
|
8
|
+
// and the after-commit work (batch fill, onEndReached/onStartReached, viewability, initialScroll, MVCP)
|
|
9
|
+
// in ngAfterViewChecked. OnPush + ChangeDetectorRef.markForCheck() drives re-render off the native
|
|
10
|
+
// scroll/layout/measure callbacks (they fire outside Angular's own event bindings, so they must
|
|
11
|
+
// mark the view dirty). This is the Angular twin of the React useReducer/useEffect and Vue
|
|
12
|
+
// ref/computed/watch over the same shared functions. It composes the Angular ScrollView, exactly
|
|
13
|
+
// as the React/Vue lists drive their ScrollView.
|
|
14
|
+
//
|
|
15
|
+
// Per-item rendering is TEMPLATES, not a callback: React/Vue `renderItem: (info) => element` does
|
|
16
|
+
// not translate to Angular. The app supplies the cell via `<ng-template vListItem>` (and the
|
|
17
|
+
// header/footer/empty/separator slots via vListHeader/vListFooter/vListEmpty/vListSeparator); the
|
|
18
|
+
// list captures them with @ContentChild and stamps the WINDOWED slice through VListOutletDirective
|
|
19
|
+
// (a core-only NgTemplateOutlet twin — @angular/common is not a dependency). See ./directives.ts.
|
|
20
|
+
// Every React/Vue behavior is present (windowing, onEndReached, viewability, headers/footers/empty/
|
|
21
|
+
// separators, refresh, imperative scroll, horizontal, inverted, MVCP); only the cell AUTHORING
|
|
22
|
+
// shape differs, per the per-adapter children/render split of
|
|
23
|
+
// <prop_types_split_agnostic_vs_per_adapter>.
|
|
24
|
+
//
|
|
25
|
+
// Lists have no Descriptor render fn (the cell content is the framework's own children); see
|
|
26
|
+
// core/components/.docs-note-lists.md. Cells/spacers are plain symbiote-view host elements.
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
CUSTOM_ELEMENTS_SCHEMA,
|
|
30
|
+
ChangeDetectionStrategy,
|
|
31
|
+
ChangeDetectorRef,
|
|
32
|
+
Component,
|
|
33
|
+
ContentChild,
|
|
34
|
+
ElementRef,
|
|
35
|
+
EventEmitter,
|
|
36
|
+
Input,
|
|
37
|
+
Output,
|
|
38
|
+
ViewChild,
|
|
39
|
+
inject,
|
|
40
|
+
type AfterViewChecked,
|
|
41
|
+
type DoCheck,
|
|
42
|
+
type OnDestroy,
|
|
43
|
+
} from '@angular/core';
|
|
44
|
+
import {
|
|
45
|
+
DEFAULT_END_REACHED_THRESHOLD,
|
|
46
|
+
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
47
|
+
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
48
|
+
DEFAULT_START_REACHED_THRESHOLD,
|
|
49
|
+
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
50
|
+
DEFAULT_WINDOW_SIZE,
|
|
51
|
+
EMPTY_OFFSET,
|
|
52
|
+
FIRST_INDEX,
|
|
53
|
+
INVERTED_X_STYLE,
|
|
54
|
+
INVERTED_Y_STYLE,
|
|
55
|
+
NO_CONTENT_LENGTH_SENT,
|
|
56
|
+
averageMeasuredLength,
|
|
57
|
+
buildListPlan,
|
|
58
|
+
buildOffsets,
|
|
59
|
+
buildViewabilityPairs,
|
|
60
|
+
computeEndReached,
|
|
61
|
+
computeStartReached,
|
|
62
|
+
computeViewableSet,
|
|
63
|
+
computeWindow,
|
|
64
|
+
diffViewable,
|
|
65
|
+
highestMeasuredIndex,
|
|
66
|
+
maxMinimumViewTime,
|
|
67
|
+
offsetForIndex,
|
|
68
|
+
readLayoutLength,
|
|
69
|
+
readScrollOffset,
|
|
70
|
+
resolveAccessibilityProps,
|
|
71
|
+
throttleWindow,
|
|
72
|
+
type IAccessibilityProps,
|
|
73
|
+
type IAccessibilityStateValue,
|
|
74
|
+
type IAriaProps,
|
|
75
|
+
type ICellLayout,
|
|
76
|
+
type ISeparatorProps,
|
|
77
|
+
type ISeparators,
|
|
78
|
+
type IScrollViewHandle,
|
|
79
|
+
type IViewToken,
|
|
80
|
+
type IViewabilityConfig,
|
|
81
|
+
type IViewabilityConfigCallbackPair,
|
|
82
|
+
type IViewableItemsChangedInfo,
|
|
83
|
+
type IVirtualizedListHandle,
|
|
84
|
+
} from '@symbiote-native/components';
|
|
85
|
+
import {
|
|
86
|
+
dlog,
|
|
87
|
+
flattenStyle,
|
|
88
|
+
type IStyleProp,
|
|
89
|
+
type ISymbioteEvent,
|
|
90
|
+
type ISymbioteNode,
|
|
91
|
+
type IViewStyle,
|
|
92
|
+
} from '@symbiote-native/engine';
|
|
93
|
+
import { ScrollView } from '../scroll-view';
|
|
94
|
+
import { RefreshControl } from '../refresh-control';
|
|
95
|
+
import { anchorHostStyle, ViewHost } from '../../primitives';
|
|
96
|
+
import {
|
|
97
|
+
VListEmptyDirective,
|
|
98
|
+
VListFooterDirective,
|
|
99
|
+
VListHeaderDirective,
|
|
100
|
+
VListItemDirective,
|
|
101
|
+
VListOutletDirective,
|
|
102
|
+
VListSeparatorDirective,
|
|
103
|
+
type IVListItemContext,
|
|
104
|
+
type IVListSeparatorContext,
|
|
105
|
+
} from './directives';
|
|
106
|
+
|
|
107
|
+
// Re-export the shared list types + the authoring directives so flat-list / section-list keep
|
|
108
|
+
// importing them from '../virtualized-list', exactly as the React/Vue adapters re-export them.
|
|
109
|
+
export type {
|
|
110
|
+
ICellLayout,
|
|
111
|
+
ISeparators,
|
|
112
|
+
ISeparatorProps,
|
|
113
|
+
IViewToken,
|
|
114
|
+
IViewableItemsChangedInfo,
|
|
115
|
+
IViewabilityConfig,
|
|
116
|
+
IViewabilityConfigCallbackPair,
|
|
117
|
+
IVirtualizedListHandle,
|
|
118
|
+
} from '@symbiote-native/components';
|
|
119
|
+
export {
|
|
120
|
+
VListEmptyDirective,
|
|
121
|
+
VListFooterDirective,
|
|
122
|
+
VListHeaderDirective,
|
|
123
|
+
VListItemDirective,
|
|
124
|
+
VListSeparatorDirective,
|
|
125
|
+
} from './directives';
|
|
126
|
+
export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
127
|
+
|
|
128
|
+
// The Angular VirtualizedList prop surface. Mirrors React/Vue's IVirtualizedListProps MINUS the
|
|
129
|
+
// element-returning props (renderItem, ListHeader/Footer/Empty Component, ItemSeparatorComponent):
|
|
130
|
+
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular
|
|
131
|
+
// (<prop_types_split_agnostic_vs_per_adapter>). Everything agnostic is the SAME surface.
|
|
132
|
+
export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAriaProps {
|
|
133
|
+
data: unknown;
|
|
134
|
+
getItem: (data: unknown, index: number) => ItemT;
|
|
135
|
+
getItemCount: (data: unknown) => number;
|
|
136
|
+
keyExtractor?: (item: ItemT, index: number) => string;
|
|
137
|
+
getItemLayout?: (
|
|
138
|
+
data: unknown,
|
|
139
|
+
index: number,
|
|
140
|
+
) => { length: number; offset: number; index: number };
|
|
141
|
+
horizontal?: boolean;
|
|
142
|
+
inverted?: boolean;
|
|
143
|
+
extraData?: unknown;
|
|
144
|
+
onEndReached?: (info: { distanceFromEnd: number }) => void;
|
|
145
|
+
onEndReachedThreshold?: number;
|
|
146
|
+
onStartReached?: (info: { distanceFromStart: number }) => void;
|
|
147
|
+
onStartReachedThreshold?: number;
|
|
148
|
+
onRefresh?: () => void;
|
|
149
|
+
refreshing?: boolean | null;
|
|
150
|
+
progressViewOffset?: number;
|
|
151
|
+
onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
|
|
152
|
+
viewabilityConfig?: IViewabilityConfig;
|
|
153
|
+
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
154
|
+
onScrollToIndexFailed?: (info: {
|
|
155
|
+
index: number;
|
|
156
|
+
highestMeasuredFrameIndex: number;
|
|
157
|
+
averageItemLength: number;
|
|
158
|
+
}) => void;
|
|
159
|
+
initialNumToRender?: number;
|
|
160
|
+
initialScrollIndex?: number;
|
|
161
|
+
maxToRenderPerBatch?: number;
|
|
162
|
+
updateCellsBatchingPeriod?: number;
|
|
163
|
+
windowSize?: number;
|
|
164
|
+
stickyHeaderIndices?: number[];
|
|
165
|
+
maintainVisibleContentPosition?: {
|
|
166
|
+
minIndexForVisible: number;
|
|
167
|
+
autoscrollToTopThreshold?: number;
|
|
168
|
+
};
|
|
169
|
+
onScroll?: (event: ISymbioteEvent) => void;
|
|
170
|
+
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
171
|
+
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
172
|
+
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
173
|
+
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
174
|
+
scrollEventThrottle?: number;
|
|
175
|
+
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
176
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
177
|
+
style?: IStyleProp<IViewStyle>;
|
|
178
|
+
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// What the VirtualizedList component itself takes as plain @Input()s: the full surface minus the
|
|
182
|
+
// events it exposes as real @Output() EventEmitters instead (see the class below), mirroring how
|
|
183
|
+
// pressable/index.ts derives IAngularPressableInputs from IAngularPressableProps.
|
|
184
|
+
export type IVirtualizedListInputs<ItemT> = Omit<
|
|
185
|
+
IVirtualizedListProps<ItemT>,
|
|
186
|
+
| 'onEndReached'
|
|
187
|
+
| 'onStartReached'
|
|
188
|
+
| 'onRefresh'
|
|
189
|
+
| 'onViewableItemsChanged'
|
|
190
|
+
| 'onScrollToIndexFailed'
|
|
191
|
+
| 'onAccessibilityAction'
|
|
192
|
+
| 'onAccessibilityTap'
|
|
193
|
+
| 'onMagicTap'
|
|
194
|
+
| 'onAccessibilityEscape'
|
|
195
|
+
>;
|
|
196
|
+
|
|
197
|
+
// The windowing snapshot recomputed once per CD in ngDoCheck. Plain object (not a signal/computed):
|
|
198
|
+
// the OnPush + markForCheck cycle re-runs ngDoCheck whenever scroll / layout / measure change.
|
|
199
|
+
interface IMetricsCore {
|
|
200
|
+
count: number;
|
|
201
|
+
offsets: number[];
|
|
202
|
+
lengths: number[];
|
|
203
|
+
total: number;
|
|
204
|
+
first: number;
|
|
205
|
+
last: number;
|
|
206
|
+
target: { first: number; last: number };
|
|
207
|
+
averageLength: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// One in-window cell, assembled in ngDoCheck and stamped by the template's @for. The
|
|
211
|
+
// context object is fresh each pass; VListOutletDirective folds it onto the live embedded view, so
|
|
212
|
+
// the cell view is reused (the windowing recompute does not tear cells down).
|
|
213
|
+
interface IWindowCell<ItemT> {
|
|
214
|
+
key: string;
|
|
215
|
+
index: number;
|
|
216
|
+
context: IVListItemContext<ItemT>;
|
|
217
|
+
measure: (event: ISymbioteEvent) => void;
|
|
218
|
+
separatorContext?: IVListSeparatorContext<ItemT>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Component({
|
|
222
|
+
selector: 'VirtualizedList',
|
|
223
|
+
standalone: true,
|
|
224
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
225
|
+
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
226
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
227
|
+
template: `
|
|
228
|
+
<ScrollView
|
|
229
|
+
[horizontal]="isHorizontal"
|
|
230
|
+
[style]="resolvedStyle"
|
|
231
|
+
[contentContainerStyle]="resolvedContentContainerStyle"
|
|
232
|
+
[testID]="foldedAccessibility.testID"
|
|
233
|
+
[nativeID]="foldedAccessibility.nativeID"
|
|
234
|
+
[accessible]="foldedAccessibility.accessible"
|
|
235
|
+
[accessibilityLabel]="foldedAccessibility.accessibilityLabel"
|
|
236
|
+
[accessibilityHint]="foldedAccessibility.accessibilityHint"
|
|
237
|
+
[accessibilityRole]="foldedAccessibility.accessibilityRole"
|
|
238
|
+
[accessibilityState]="foldedAccessibility.accessibilityState"
|
|
239
|
+
[accessibilityValue]="foldedAccessibility.accessibilityValue"
|
|
240
|
+
[accessibilityActions]="foldedAccessibility.accessibilityActions"
|
|
241
|
+
[accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
|
|
242
|
+
[importantForAccessibility]="foldedAccessibility.importantForAccessibility"
|
|
243
|
+
[accessibilityLiveRegion]="foldedAccessibility.accessibilityLiveRegion"
|
|
244
|
+
[screenReaderFocusable]="foldedAccessibility.screenReaderFocusable"
|
|
245
|
+
[accessibilityViewIsModal]="foldedAccessibility.accessibilityViewIsModal"
|
|
246
|
+
[accessibilityElementsHidden]="foldedAccessibility.accessibilityElementsHidden"
|
|
247
|
+
[accessibilityIgnoresInvertColors]="foldedAccessibility.accessibilityIgnoresInvertColors"
|
|
248
|
+
[accessibilityLanguage]="foldedAccessibility.accessibilityLanguage"
|
|
249
|
+
[accessibilityRespondsToUserInteraction]="
|
|
250
|
+
foldedAccessibility.accessibilityRespondsToUserInteraction
|
|
251
|
+
"
|
|
252
|
+
[accessibilityShowsLargeContentViewer]="
|
|
253
|
+
foldedAccessibility.accessibilityShowsLargeContentViewer
|
|
254
|
+
"
|
|
255
|
+
[accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
|
|
256
|
+
(accessibilityAction)="accessibilityActionTick($event)"
|
|
257
|
+
(accessibilityTap)="accessibilityTapTick($event)"
|
|
258
|
+
(magicTap)="magicTapTick($event)"
|
|
259
|
+
(accessibilityEscape)="accessibilityEscapeTick($event)"
|
|
260
|
+
[onScroll]="onScrollTick"
|
|
261
|
+
(layout)="onLayoutTick($event)"
|
|
262
|
+
[onScrollBeginDrag]="onScrollBeginDrag"
|
|
263
|
+
[onScrollEndDrag]="onScrollEndDrag"
|
|
264
|
+
[onMomentumScrollBegin]="onMomentumScrollBegin"
|
|
265
|
+
[onMomentumScrollEnd]="onMomentumScrollEnd"
|
|
266
|
+
[scrollEventThrottle]="scrollEventThrottle"
|
|
267
|
+
[keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
|
|
268
|
+
[keyboardDismissMode]="keyboardDismissMode"
|
|
269
|
+
[contentOffset]="commandedOffset"
|
|
270
|
+
[stickyHeaderIndices]="renderedStickyIndices"
|
|
271
|
+
[maintainVisibleContentPosition]="resolvedMaintainVisibleContentPosition"
|
|
272
|
+
>
|
|
273
|
+
@if (shouldRenderRefreshControl) {
|
|
274
|
+
<RefreshControl
|
|
275
|
+
[refreshing]="refreshing ?? false"
|
|
276
|
+
(refresh)="handleRefresh()"
|
|
277
|
+
[progressViewOffset]="progressViewOffset"
|
|
278
|
+
/>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@if (headerDir !== undefined) {
|
|
282
|
+
<symbiote-view>
|
|
283
|
+
<ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
|
|
284
|
+
</symbiote-view>
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@if (itemCount === 0) {
|
|
288
|
+
@if (emptyDir !== undefined) {
|
|
289
|
+
<symbiote-view>
|
|
290
|
+
<ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
|
|
291
|
+
</symbiote-view>
|
|
292
|
+
}
|
|
293
|
+
} @else {
|
|
294
|
+
@if (leadingSpacerStyle !== null) {
|
|
295
|
+
<symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
|
|
296
|
+
}
|
|
297
|
+
@for (cell of windowCells; track cell.key) {
|
|
298
|
+
<symbiote-view (layout)="handleCellLayout(cell.measure, $event)" [style]="cellStyle">
|
|
299
|
+
<ng-container
|
|
300
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
301
|
+
[vListOutletContext]="cell.context"
|
|
302
|
+
></ng-container>
|
|
303
|
+
</symbiote-view>
|
|
304
|
+
@if (cell.separatorContext !== undefined) {
|
|
305
|
+
<symbiote-view>
|
|
306
|
+
<ng-container
|
|
307
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
308
|
+
[vListOutletContext]="cell.separatorContext"
|
|
309
|
+
></ng-container>
|
|
310
|
+
</symbiote-view>
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
@if (trailingSpacerStyle !== null) {
|
|
314
|
+
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@if (footerDir !== undefined) {
|
|
319
|
+
<symbiote-view>
|
|
320
|
+
<ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
|
|
321
|
+
</symbiote-view>
|
|
322
|
+
}
|
|
323
|
+
</ScrollView>
|
|
324
|
+
`,
|
|
325
|
+
})
|
|
326
|
+
export class VirtualizedList<ItemT = unknown>
|
|
327
|
+
implements
|
|
328
|
+
IVirtualizedListInputs<ItemT>,
|
|
329
|
+
IVirtualizedListHandle,
|
|
330
|
+
DoCheck,
|
|
331
|
+
AfterViewChecked,
|
|
332
|
+
OnDestroy
|
|
333
|
+
{
|
|
334
|
+
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
335
|
+
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
336
|
+
// shaped ScrollView/RefreshControl @Input()s are fed from these.
|
|
337
|
+
@Output() readonly endReached = new EventEmitter<{ distanceFromEnd: number }>();
|
|
338
|
+
@Output() readonly startReached = new EventEmitter<{ distanceFromStart: number }>();
|
|
339
|
+
@Output() readonly refresh = new EventEmitter<void>();
|
|
340
|
+
@Output() readonly viewableItemsChanged = new EventEmitter<IViewableItemsChangedInfo<ItemT>>();
|
|
341
|
+
@Output() readonly scrollToIndexFailed = new EventEmitter<{
|
|
342
|
+
index: number;
|
|
343
|
+
highestMeasuredFrameIndex: number;
|
|
344
|
+
averageItemLength: number;
|
|
345
|
+
}>();
|
|
346
|
+
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
347
|
+
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
348
|
+
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
349
|
+
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
350
|
+
@Input({ required: true }) data!: unknown;
|
|
351
|
+
@Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
|
|
352
|
+
@Input({ required: true }) getItemCount!: (data: unknown) => number;
|
|
353
|
+
@Input() keyExtractor?: (item: ItemT, index: number) => string;
|
|
354
|
+
@Input() getItemLayout?: (
|
|
355
|
+
data: unknown,
|
|
356
|
+
index: number,
|
|
357
|
+
) => { length: number; offset: number; index: number };
|
|
358
|
+
@Input() horizontal?: boolean;
|
|
359
|
+
@Input() inverted?: boolean;
|
|
360
|
+
@Input() extraData?: unknown;
|
|
361
|
+
@Input() onEndReachedThreshold?: number;
|
|
362
|
+
@Input() onStartReachedThreshold?: number;
|
|
363
|
+
@Input() refreshing?: boolean | null;
|
|
364
|
+
@Input() progressViewOffset?: number;
|
|
365
|
+
// Set explicitly by a wrapper (FlatList/VirtualizedSectionList) that ALWAYS binds
|
|
366
|
+
// `(refresh)="refresh.emit()"` on this component to re-forward the event outward — that binding
|
|
367
|
+
// itself makes `this.refresh.observed` permanently true (Angular subscribes unconditionally the
|
|
368
|
+
// moment a template writes `(refresh)="…"`, regardless of what the handler does), so `.observed`
|
|
369
|
+
// can no longer tell "the app actually wants pull-to-refresh" from "a wrapper is just forwarding
|
|
370
|
+
// the event". A wrapper passes its OWN public `refresh.observed` here instead; direct usage
|
|
371
|
+
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
372
|
+
@Input() refreshRequested?: boolean;
|
|
373
|
+
@Input() viewabilityConfig?: IViewabilityConfig;
|
|
374
|
+
@Input() viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
375
|
+
@Input() initialNumToRender?: number;
|
|
376
|
+
@Input() initialScrollIndex?: number;
|
|
377
|
+
@Input() maxToRenderPerBatch?: number;
|
|
378
|
+
@Input() updateCellsBatchingPeriod?: number;
|
|
379
|
+
@Input() windowSize?: number;
|
|
380
|
+
@Input() stickyHeaderIndices?: number[];
|
|
381
|
+
@Input() maintainVisibleContentPosition?: {
|
|
382
|
+
minIndexForVisible: number;
|
|
383
|
+
autoscrollToTopThreshold?: number;
|
|
384
|
+
};
|
|
385
|
+
@Input() onScroll?: (event: ISymbioteEvent) => void;
|
|
386
|
+
@Input() onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
387
|
+
@Input() onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
388
|
+
@Input() onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
389
|
+
@Input() onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
390
|
+
@Input() scrollEventThrottle?: number;
|
|
391
|
+
@Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
392
|
+
@Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
393
|
+
@Input() style?: IStyleProp<IViewStyle>;
|
|
394
|
+
@Input() contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
395
|
+
@Input() testID?: string;
|
|
396
|
+
@Input() nativeID?: string;
|
|
397
|
+
@Input() accessible?: boolean;
|
|
398
|
+
@Input() accessibilityLabel?: string;
|
|
399
|
+
@Input() accessibilityHint?: string;
|
|
400
|
+
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
401
|
+
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
402
|
+
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
403
|
+
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
404
|
+
@Input() accessibilityLabelledBy?: string | string[];
|
|
405
|
+
@Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
406
|
+
@Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
407
|
+
@Input() screenReaderFocusable?: boolean;
|
|
408
|
+
@Input() accessibilityViewIsModal?: boolean;
|
|
409
|
+
@Input() accessibilityElementsHidden?: boolean;
|
|
410
|
+
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
411
|
+
@Input() accessibilityLanguage?: string;
|
|
412
|
+
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
413
|
+
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
414
|
+
@Input() accessibilityLargeContentTitle?: string;
|
|
415
|
+
@Input() role?: IAriaProps['role'];
|
|
416
|
+
@Input() ariaLabel?: string;
|
|
417
|
+
@Input() ariaLabelledBy?: string;
|
|
418
|
+
@Input() ariaLive?: IAriaProps['aria-live'];
|
|
419
|
+
@Input() ariaHidden?: boolean;
|
|
420
|
+
@Input() ariaBusy?: boolean;
|
|
421
|
+
@Input() ariaChecked?: boolean | 'mixed';
|
|
422
|
+
@Input() ariaDisabled?: boolean;
|
|
423
|
+
@Input() ariaExpanded?: boolean;
|
|
424
|
+
@Input() ariaSelected?: boolean;
|
|
425
|
+
@Input() ariaModal?: boolean;
|
|
426
|
+
@Input() ariaValueMax?: number;
|
|
427
|
+
@Input() ariaValueMin?: number;
|
|
428
|
+
@Input() ariaValueNow?: number;
|
|
429
|
+
@Input() ariaValueText?: string;
|
|
430
|
+
|
|
431
|
+
// The cell + slot templates the app authors (the Angular twin of renderItem / ListHeaderComponent
|
|
432
|
+
// / ListFooterComponent / ListEmptyComponent / ItemSeparatorComponent), captured from projected
|
|
433
|
+
// <ng-template> content. Resolved by ngAfterContentInit, read each ngDoCheck (from the 2nd CD on).
|
|
434
|
+
@ContentChild(VListItemDirective) itemDir?: VListItemDirective<ItemT>;
|
|
435
|
+
@ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
|
|
436
|
+
@ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
|
|
437
|
+
@ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
|
|
438
|
+
@ContentChild(VListSeparatorDirective) separatorDir?: VListSeparatorDirective<ItemT>;
|
|
439
|
+
|
|
440
|
+
// The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
|
|
441
|
+
// flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
|
|
442
|
+
// Available from ngAfterViewInit on; the handle reads the node lazily and no-ops before commit.
|
|
443
|
+
@ViewChild(ScrollView) private scrollView?: ScrollView;
|
|
444
|
+
|
|
445
|
+
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
446
|
+
itemCount = EMPTY_OFFSET;
|
|
447
|
+
windowCells: IWindowCell<ItemT>[] = [];
|
|
448
|
+
leadingSpacerStyle: IViewStyle | null = null;
|
|
449
|
+
trailingSpacerStyle: IViewStyle | null = null;
|
|
450
|
+
cellStyle: IViewStyle | undefined = undefined;
|
|
451
|
+
renderedStickyIndices: number[] | undefined = undefined;
|
|
452
|
+
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
453
|
+
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
454
|
+
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
455
|
+
// is always pre-flattened via the engine's own flattenStyle, never left as an array.
|
|
456
|
+
resolvedStyle: IViewStyle | undefined = undefined;
|
|
457
|
+
resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
|
|
458
|
+
resolvedMaintainVisibleContentPosition:
|
|
459
|
+
{ minIndexForVisible: number; autoscrollToTopThreshold?: number } | undefined = undefined;
|
|
460
|
+
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
461
|
+
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
462
|
+
commandedOffset: { x: number; y: number } | undefined = undefined;
|
|
463
|
+
|
|
464
|
+
// --- reactive scalars (mutated by the native callbacks; markForCheck re-runs the CD) ---
|
|
465
|
+
private scrollOffset = EMPTY_OFFSET;
|
|
466
|
+
private viewportLength = EMPTY_OFFSET;
|
|
467
|
+
private measureVersion = EMPTY_OFFSET;
|
|
468
|
+
|
|
469
|
+
// --- non-render state (the Angular twin of refs that never trigger a render) ---
|
|
470
|
+
private metricsCore: IMetricsCore = {
|
|
471
|
+
count: EMPTY_OFFSET,
|
|
472
|
+
offsets: [],
|
|
473
|
+
lengths: [],
|
|
474
|
+
total: EMPTY_OFFSET,
|
|
475
|
+
first: FIRST_INDEX,
|
|
476
|
+
last: -1,
|
|
477
|
+
target: { first: FIRST_INDEX, last: -1 },
|
|
478
|
+
averageLength: EMPTY_OFFSET,
|
|
479
|
+
};
|
|
480
|
+
private readonly measured = new Map<number, number>();
|
|
481
|
+
private readonly cellMeasures = new Map<number, (event: ISymbioteEvent) => void>();
|
|
482
|
+
private readonly separatorOverrides = new Map<number, Partial<ISeparatorProps<unknown>>>();
|
|
483
|
+
// The previously committed window, grown by at most maxToRenderPerBatch per tick (no snap).
|
|
484
|
+
private committedWindow: { first: number; last: number } = { first: FIRST_INDEX, last: -1 };
|
|
485
|
+
private sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
486
|
+
private sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
487
|
+
private lastViewable = new Map<string, IViewToken<ItemT>>();
|
|
488
|
+
private viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
489
|
+
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
490
|
+
private hasInteracted = false;
|
|
491
|
+
private appliedInitialScroll = false;
|
|
492
|
+
private firstVisibleKey: string | null = null;
|
|
493
|
+
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
494
|
+
private lastEffectSignature = '';
|
|
495
|
+
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
496
|
+
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
497
|
+
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
498
|
+
// into VListOutletDirective, which (correctly) sees its `context` @Input() change and calls
|
|
499
|
+
// `viewRef.markForCheck()` — which reschedules ANOTHER change-detection tick via the zoneless
|
|
500
|
+
// scheduler (adapters/angular/src/render.ts). Recomputing unconditionally therefore free-runs
|
|
501
|
+
// forever the moment a list actually has cells to stamp (a real, previously-dormant bug only
|
|
502
|
+
// exposed once flat-list.test.ts's projection fix let cells render for the first time — see that
|
|
503
|
+
// test's comment). Guarding recompute behind "did anything relevant actually change" breaks the
|
|
504
|
+
// cycle: unrelated CD passes reuse the same windowCells/context identities, so
|
|
505
|
+
// VListOutletDirective's ngOnChanges sees no change and never reschedules.
|
|
506
|
+
private lastRecompute: unknown[] | undefined = undefined;
|
|
507
|
+
|
|
508
|
+
private readonly cdr = inject(ChangeDetectorRef);
|
|
509
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
510
|
+
// onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
|
|
511
|
+
// inner `<ScrollView>` one level down (itself its own separate anchor host).
|
|
512
|
+
private readonly elementRef = inject(ElementRef);
|
|
513
|
+
|
|
514
|
+
get foldedAccessibility(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
|
|
515
|
+
return resolveAccessibilityProps({
|
|
516
|
+
testID: this.testID,
|
|
517
|
+
nativeID: this.nativeID,
|
|
518
|
+
accessible: this.accessible,
|
|
519
|
+
accessibilityLabel: this.accessibilityLabel,
|
|
520
|
+
accessibilityHint: this.accessibilityHint,
|
|
521
|
+
accessibilityRole: this.accessibilityRole,
|
|
522
|
+
accessibilityState: this.accessibilityState,
|
|
523
|
+
accessibilityValue: this.accessibilityValue,
|
|
524
|
+
accessibilityActions: this.accessibilityActions,
|
|
525
|
+
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
526
|
+
importantForAccessibility: this.importantForAccessibility,
|
|
527
|
+
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
528
|
+
screenReaderFocusable: this.screenReaderFocusable,
|
|
529
|
+
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
530
|
+
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
531
|
+
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
532
|
+
accessibilityLanguage: this.accessibilityLanguage,
|
|
533
|
+
accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
|
|
534
|
+
accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
|
|
535
|
+
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
536
|
+
role: this.role,
|
|
537
|
+
'aria-label': this.ariaLabel,
|
|
538
|
+
'aria-labelledby': this.ariaLabelledBy,
|
|
539
|
+
'aria-live': this.ariaLive,
|
|
540
|
+
'aria-hidden': this.ariaHidden,
|
|
541
|
+
'aria-busy': this.ariaBusy,
|
|
542
|
+
'aria-checked': this.ariaChecked,
|
|
543
|
+
'aria-disabled': this.ariaDisabled,
|
|
544
|
+
'aria-expanded': this.ariaExpanded,
|
|
545
|
+
'aria-selected': this.ariaSelected,
|
|
546
|
+
'aria-modal': this.ariaModal,
|
|
547
|
+
'aria-valuemax': this.ariaValueMax,
|
|
548
|
+
'aria-valuemin': this.ariaValueMin,
|
|
549
|
+
'aria-valuenow': this.ariaValueNow,
|
|
550
|
+
'aria-valuetext': this.ariaValueText,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
get shouldRenderRefreshControl(): boolean {
|
|
555
|
+
return this.refreshRequested ?? this.refresh.observed;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
get isHorizontal(): boolean {
|
|
559
|
+
return this.horizontal === true;
|
|
560
|
+
}
|
|
561
|
+
private get isInverted(): boolean {
|
|
562
|
+
return this.inverted === true;
|
|
563
|
+
}
|
|
564
|
+
private get windowSizeValue(): number {
|
|
565
|
+
return this.windowSize ?? DEFAULT_WINDOW_SIZE;
|
|
566
|
+
}
|
|
567
|
+
private get initialNumToRenderValue(): number {
|
|
568
|
+
return this.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER;
|
|
569
|
+
}
|
|
570
|
+
private get maxToRenderPerBatchValue(): number {
|
|
571
|
+
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
572
|
+
}
|
|
573
|
+
private get updateCellsBatchingPeriodValue(): number {
|
|
574
|
+
return this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD;
|
|
575
|
+
}
|
|
576
|
+
private get onEndReachedThresholdValue(): number {
|
|
577
|
+
return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
|
|
578
|
+
}
|
|
579
|
+
private get onStartReachedThresholdValue(): number {
|
|
580
|
+
return this.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ScrollView's onScroll stays an @Input() callback (an Animated.event(...) target must be able to
|
|
584
|
+
// flow through it, which an @Output() can't carry), so this is a stable arrow field passed
|
|
585
|
+
// straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
|
|
586
|
+
onScrollTick = (event: ISymbioteEvent): void => {
|
|
587
|
+
const offset = readScrollOffset(event, this.isHorizontal);
|
|
588
|
+
if (offset === undefined) return;
|
|
589
|
+
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
590
|
+
// First scroll is the interaction that ungates waitForInteraction configs.
|
|
591
|
+
this.hasInteracted = true;
|
|
592
|
+
this.scrollOffset = offset;
|
|
593
|
+
// A real native scroll supersedes any pending commanded offset.
|
|
594
|
+
this.commandedOffset = undefined;
|
|
595
|
+
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
596
|
+
this.onScroll?.(event);
|
|
597
|
+
this.cdr.markForCheck();
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
onLayoutTick = (event: ISymbioteEvent): void => {
|
|
601
|
+
const length = readLayoutLength(event, this.isHorizontal);
|
|
602
|
+
if (length === undefined) return;
|
|
603
|
+
dlog(`Angular VirtualizedList onLayout viewport=${length}`);
|
|
604
|
+
this.viewportLength = length;
|
|
605
|
+
this.cdr.markForCheck();
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
|
|
609
|
+
// via (refresh)="handleRefresh()" / (accessibilityAction)="accessibilityActionTick($event)" etc.
|
|
610
|
+
// — these fields just adapt VirtualizedList's own @Output() into a plain re-emit callback.
|
|
611
|
+
handleRefresh = (): void => {
|
|
612
|
+
this.refresh.emit();
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
accessibilityActionTick = (event: ISymbioteEvent): void => {
|
|
616
|
+
this.accessibilityAction.emit(event);
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
accessibilityTapTick = (event: ISymbioteEvent): void => {
|
|
620
|
+
this.accessibilityTap.emit(event);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
magicTapTick = (event: ISymbioteEvent): void => {
|
|
624
|
+
this.magicTap.emit(event);
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
accessibilityEscapeTick = (event: ISymbioteEvent): void => {
|
|
628
|
+
this.accessibilityEscape.emit(event);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
private keyFor = (index: number): string => {
|
|
632
|
+
const item = this.getItem(this.data, index);
|
|
633
|
+
return this.keyExtractor ? this.keyExtractor(item, index) : String(index);
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
|
|
637
|
+
// template-bound fields render this pass — computing them in ngAfterContentChecked instead would
|
|
638
|
+
// trip ExpressionChangedAfterItHasBeenChecked). recomputeMetrics owns the controlled committedWindow
|
|
639
|
+
// throttle (the side effect React runs during render); ngDoCheck is its single evaluation point, so
|
|
640
|
+
// the window grows exactly one step per CD. The projected templates (ContentChild) are resolved
|
|
641
|
+
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
642
|
+
ngDoCheck(): void {
|
|
643
|
+
const recomputeInputs: unknown[] = [
|
|
644
|
+
this.data,
|
|
645
|
+
this.extraData,
|
|
646
|
+
this.getItemLayout,
|
|
647
|
+
this.keyExtractor,
|
|
648
|
+
this.isHorizontal,
|
|
649
|
+
this.isInverted,
|
|
650
|
+
this.windowSizeValue,
|
|
651
|
+
this.initialNumToRenderValue,
|
|
652
|
+
this.maxToRenderPerBatchValue,
|
|
653
|
+
this.stickyHeaderIndices,
|
|
654
|
+
this.maintainVisibleContentPosition,
|
|
655
|
+
this.style,
|
|
656
|
+
this.contentContainerStyle,
|
|
657
|
+
this.scrollOffset,
|
|
658
|
+
this.viewportLength,
|
|
659
|
+
this.measureVersion,
|
|
660
|
+
this.headerDir !== undefined,
|
|
661
|
+
this.footerDir !== undefined,
|
|
662
|
+
this.emptyDir !== undefined,
|
|
663
|
+
this.separatorDir !== undefined,
|
|
664
|
+
];
|
|
665
|
+
const previous = this.lastRecompute;
|
|
666
|
+
this.lastRecompute = recomputeInputs;
|
|
667
|
+
if (
|
|
668
|
+
previous !== undefined &&
|
|
669
|
+
previous.length === recomputeInputs.length &&
|
|
670
|
+
previous.every((value, index) => value === recomputeInputs[index])
|
|
671
|
+
) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.recomputeMetrics();
|
|
675
|
+
this.recomputeView();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
ngAfterViewChecked(): void {
|
|
679
|
+
const m = this.metricsCore;
|
|
680
|
+
const signature = `${this.scrollOffset}|${this.viewportLength}|${m.first}|${m.last}|${m.count}|${m.total}|${this.measureVersion}`;
|
|
681
|
+
if (signature === this.lastEffectSignature) return;
|
|
682
|
+
this.lastEffectSignature = signature;
|
|
683
|
+
this.effectBatchFill();
|
|
684
|
+
this.effectEndReached();
|
|
685
|
+
this.effectStartReached();
|
|
686
|
+
this.effectViewability();
|
|
687
|
+
this.effectInitialScroll();
|
|
688
|
+
this.effectMaintainVisibleContentPosition();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
ngOnDestroy(): void {
|
|
692
|
+
if (this.viewableTimer !== null) clearTimeout(this.viewableTimer);
|
|
693
|
+
if (this.batchTimer !== null) clearTimeout(this.batchTimer);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private recomputeMetrics(): void {
|
|
697
|
+
const count = this.getItemCount(this.data);
|
|
698
|
+
const gil = this.getItemLayout;
|
|
699
|
+
const data = this.data;
|
|
700
|
+
const fixedLayout =
|
|
701
|
+
gil !== undefined
|
|
702
|
+
? (index: number): ICellLayout => {
|
|
703
|
+
const layout = gil(data, index);
|
|
704
|
+
return { length: layout.length, offset: layout.offset };
|
|
705
|
+
}
|
|
706
|
+
: undefined;
|
|
707
|
+
const averageLength =
|
|
708
|
+
fixedLayout !== undefined
|
|
709
|
+
? count > FIRST_INDEX
|
|
710
|
+
? fixedLayout(FIRST_INDEX).length
|
|
711
|
+
: EMPTY_OFFSET
|
|
712
|
+
: averageMeasuredLength(this.measured);
|
|
713
|
+
const { offsets, lengths, total } = buildOffsets(
|
|
714
|
+
count,
|
|
715
|
+
this.measured,
|
|
716
|
+
fixedLayout,
|
|
717
|
+
averageLength,
|
|
718
|
+
);
|
|
719
|
+
const target = computeWindow(
|
|
720
|
+
count,
|
|
721
|
+
offsets,
|
|
722
|
+
lengths,
|
|
723
|
+
this.scrollOffset,
|
|
724
|
+
this.viewportLength,
|
|
725
|
+
this.windowSizeValue,
|
|
726
|
+
this.initialNumToRenderValue,
|
|
727
|
+
);
|
|
728
|
+
const throttled = throttleWindow(target, this.committedWindow, this.maxToRenderPerBatchValue);
|
|
729
|
+
this.committedWindow = throttled;
|
|
730
|
+
this.metricsCore = {
|
|
731
|
+
count,
|
|
732
|
+
offsets,
|
|
733
|
+
lengths,
|
|
734
|
+
total,
|
|
735
|
+
first: throttled.first,
|
|
736
|
+
last: throttled.last,
|
|
737
|
+
target,
|
|
738
|
+
averageLength,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private recomputeView(): void {
|
|
743
|
+
const m = this.metricsCore;
|
|
744
|
+
this.itemCount = m.count;
|
|
745
|
+
const hasHeader = this.headerDir !== undefined;
|
|
746
|
+
const hasSeparators = this.separatorDir !== undefined;
|
|
747
|
+
const stickySet =
|
|
748
|
+
this.stickyHeaderIndices !== undefined ? new Set(this.stickyHeaderIndices) : undefined;
|
|
749
|
+
|
|
750
|
+
this.resolvedContentContainerStyle = this.isHorizontal
|
|
751
|
+
? [this.contentContainerStyle, { width: m.total }]
|
|
752
|
+
: this.contentContainerStyle;
|
|
753
|
+
this.resolvedStyle = flattenStyle([
|
|
754
|
+
anchorHostStyle(this.elementRef),
|
|
755
|
+
this.isInverted
|
|
756
|
+
? [this.style, this.isHorizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
|
|
757
|
+
: this.style,
|
|
758
|
+
]);
|
|
759
|
+
this.cellStyle = this.isInverted
|
|
760
|
+
? this.isHorizontal
|
|
761
|
+
? INVERTED_X_STYLE
|
|
762
|
+
: INVERTED_Y_STYLE
|
|
763
|
+
: undefined;
|
|
764
|
+
this.resolvedMaintainVisibleContentPosition =
|
|
765
|
+
this.maintainVisibleContentPosition === undefined
|
|
766
|
+
? undefined
|
|
767
|
+
: {
|
|
768
|
+
...this.maintainVisibleContentPosition,
|
|
769
|
+
minIndexForVisible:
|
|
770
|
+
this.maintainVisibleContentPosition.minIndexForVisible + (hasHeader ? 1 : 0),
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
if (m.count === FIRST_INDEX) {
|
|
774
|
+
this.windowCells = [];
|
|
775
|
+
this.leadingSpacerStyle = null;
|
|
776
|
+
this.trailingSpacerStyle = null;
|
|
777
|
+
this.renderedStickyIndices = undefined;
|
|
778
|
+
dlog(`Angular VirtualizedList empty (viewport=${this.viewportLength})`);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const plan = buildListPlan({
|
|
783
|
+
count: m.count,
|
|
784
|
+
first: m.first,
|
|
785
|
+
last: m.last,
|
|
786
|
+
offsets: m.offsets,
|
|
787
|
+
lengths: m.lengths,
|
|
788
|
+
total: m.total,
|
|
789
|
+
keyFor: this.keyFor,
|
|
790
|
+
stickyIndices: stickySet,
|
|
791
|
+
hasHeader,
|
|
792
|
+
hasSeparators,
|
|
793
|
+
});
|
|
794
|
+
this.leadingSpacerStyle =
|
|
795
|
+
plan.leadingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.leadingExtent) : null;
|
|
796
|
+
this.trailingSpacerStyle =
|
|
797
|
+
plan.trailingExtent > EMPTY_OFFSET ? this.spacerStyle(plan.trailingExtent) : null;
|
|
798
|
+
this.renderedStickyIndices =
|
|
799
|
+
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
800
|
+
? plan.stickyChildPositions
|
|
801
|
+
: undefined;
|
|
802
|
+
|
|
803
|
+
const cells: IWindowCell<ItemT>[] = [];
|
|
804
|
+
for (const planned of plan.cells) {
|
|
805
|
+
const item = this.getItem(this.data, planned.index);
|
|
806
|
+
const context: IVListItemContext<ItemT> = {
|
|
807
|
+
$implicit: item,
|
|
808
|
+
index: planned.index,
|
|
809
|
+
separators: this.makeSeparators(planned.index),
|
|
810
|
+
};
|
|
811
|
+
let separatorContext: IVListSeparatorContext<ItemT> | undefined;
|
|
812
|
+
if (hasSeparators && planned.index < m.last) {
|
|
813
|
+
separatorContext = this.buildSeparatorContext(planned.index, item);
|
|
814
|
+
}
|
|
815
|
+
cells.push({
|
|
816
|
+
key: planned.key,
|
|
817
|
+
index: planned.index,
|
|
818
|
+
context,
|
|
819
|
+
measure: this.cellMeasure(planned.index),
|
|
820
|
+
separatorContext,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
this.windowCells = cells;
|
|
824
|
+
|
|
825
|
+
dlog(
|
|
826
|
+
`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
827
|
+
`(offset=${this.scrollOffset}, viewport=${this.viewportLength}, rendered=${cells.length})`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private buildSeparatorContext(index: number, item: ItemT): IVListSeparatorContext<ItemT> {
|
|
832
|
+
const overrides = this.separatorOverrides.get(index);
|
|
833
|
+
const highlighted = overrides?.highlighted ?? false;
|
|
834
|
+
const context: IVListSeparatorContext<ItemT> = {
|
|
835
|
+
$implicit: highlighted,
|
|
836
|
+
highlighted,
|
|
837
|
+
leadingItem: item,
|
|
838
|
+
trailingItem: this.getItem(this.data, index + 1),
|
|
839
|
+
};
|
|
840
|
+
// Fold any custom updateProps keys through the context's index signature (RN lets a row drive
|
|
841
|
+
// arbitrary separator props); the typed fields above stay authoritative.
|
|
842
|
+
if (overrides !== undefined) {
|
|
843
|
+
for (const key of Object.keys(overrides)) {
|
|
844
|
+
if (key === 'highlighted' || key === 'leadingItem' || key === 'trailingItem') continue;
|
|
845
|
+
context[key] = overrides[key];
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return context;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private spacerStyle(extent: number): IViewStyle {
|
|
852
|
+
return this.isHorizontal ? { width: extent } : { height: extent };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private cellMeasure(index: number): (event: ISymbioteEvent) => void {
|
|
856
|
+
const existing = this.cellMeasures.get(index);
|
|
857
|
+
if (existing !== undefined) return existing;
|
|
858
|
+
const measure = (event: ISymbioteEvent): void => {
|
|
859
|
+
if (this.getItemLayout !== undefined) return;
|
|
860
|
+
const length = readLayoutLength(event, this.isHorizontal);
|
|
861
|
+
if (length === undefined) return;
|
|
862
|
+
if (this.measured.get(index) === length) return;
|
|
863
|
+
this.measured.set(index, length);
|
|
864
|
+
dlog(`Angular VirtualizedList cell ${index} measured length=${length}`);
|
|
865
|
+
this.measureVersion += 1;
|
|
866
|
+
this.cdr.markForCheck();
|
|
867
|
+
};
|
|
868
|
+
this.cellMeasures.set(index, measure);
|
|
869
|
+
return measure;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Per-cell onLayout rides the engine's structural (layout) event channel — Angular forbids an
|
|
873
|
+
// [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
|
|
874
|
+
handleCellLayout(measure: (event: ISymbioteEvent) => void, event: unknown): void {
|
|
875
|
+
if (this.isSymbioteEvent(event)) measure(event);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
879
|
+
return typeof value === 'object' && value !== null && 'nativeEvent' in value;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private mergeSeparator(gapIndex: number, patch: Partial<ISeparatorProps<unknown>>): void {
|
|
883
|
+
const count = this.metricsCore.count;
|
|
884
|
+
if (gapIndex < FIRST_INDEX || gapIndex > count - 2) return;
|
|
885
|
+
this.separatorOverrides.set(gapIndex, { ...this.separatorOverrides.get(gapIndex), ...patch });
|
|
886
|
+
this.cdr.markForCheck();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private makeSeparators(index: number): ISeparators {
|
|
890
|
+
return {
|
|
891
|
+
highlight: (): void => {
|
|
892
|
+
dlog(`Angular VirtualizedList separator highlight cell=${index}`);
|
|
893
|
+
this.mergeSeparator(index - 1, { highlighted: true });
|
|
894
|
+
this.mergeSeparator(index, { highlighted: true });
|
|
895
|
+
},
|
|
896
|
+
unhighlight: (): void => {
|
|
897
|
+
dlog(`Angular VirtualizedList separator unhighlight cell=${index}`);
|
|
898
|
+
this.mergeSeparator(index - 1, { highlighted: false });
|
|
899
|
+
this.mergeSeparator(index, { highlighted: false });
|
|
900
|
+
},
|
|
901
|
+
updateProps: (select: 'leading' | 'trailing', newProps: Record<string, unknown>): void => {
|
|
902
|
+
this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ---- imperative handle (the shared IVirtualizedListHandle surface) ----
|
|
908
|
+
|
|
909
|
+
scrollToOffset(params: { offset: number; animated?: boolean }): void {
|
|
910
|
+
this.scrollToPixel(params.offset, params.animated ?? true);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
scrollToIndex(params: {
|
|
914
|
+
index: number;
|
|
915
|
+
animated?: boolean;
|
|
916
|
+
viewOffset?: number;
|
|
917
|
+
viewPosition?: number;
|
|
918
|
+
}): void {
|
|
919
|
+
const m = this.metricsCore;
|
|
920
|
+
// No getItemLayout AND the target is past the last measured cell: report the failure instead of
|
|
921
|
+
// scrolling to a fabricated estimate (RN VirtualizedList.js).
|
|
922
|
+
if (this.getItemLayout === undefined && params.index > highestMeasuredIndex(this.measured)) {
|
|
923
|
+
dlog(
|
|
924
|
+
`Angular VirtualizedList onScrollToIndexFailed index=${params.index} ` +
|
|
925
|
+
`highestMeasured=${highestMeasuredIndex(this.measured)} (no getItemLayout)`,
|
|
926
|
+
);
|
|
927
|
+
this.scrollToIndexFailed.emit({
|
|
928
|
+
index: params.index,
|
|
929
|
+
highestMeasuredFrameIndex: highestMeasuredIndex(this.measured),
|
|
930
|
+
averageItemLength: m.averageLength,
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.scrollToPixel(
|
|
935
|
+
this.offsetForIndexLocal(
|
|
936
|
+
params.index,
|
|
937
|
+
params.viewPosition ?? FIRST_INDEX,
|
|
938
|
+
params.viewOffset ?? EMPTY_OFFSET,
|
|
939
|
+
),
|
|
940
|
+
params.animated ?? true,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
scrollToItem(params: { item: unknown; animated?: boolean; viewPosition?: number }): void {
|
|
945
|
+
const count = this.metricsCore.count;
|
|
946
|
+
for (let index = FIRST_INDEX; index < count; index += 1) {
|
|
947
|
+
if (this.getItem(this.data, index) === params.item) {
|
|
948
|
+
this.scrollToPixel(
|
|
949
|
+
this.offsetForIndexLocal(index, params.viewPosition ?? FIRST_INDEX, EMPTY_OFFSET),
|
|
950
|
+
params.animated ?? true,
|
|
951
|
+
);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
dlog('Angular VirtualizedList scrollToItem: item not found');
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
scrollToEnd(params?: { animated?: boolean }): void {
|
|
959
|
+
this.scrollToPixel(
|
|
960
|
+
Math.max(EMPTY_OFFSET, this.metricsCore.total - this.viewportLength),
|
|
961
|
+
params?.animated ?? true,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
flashScrollIndicators(): void {
|
|
966
|
+
this.scrollView?.flashScrollIndicators();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
getNativeScrollRef(): IScrollViewHandle | null {
|
|
970
|
+
return this.scrollView ?? null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
getScrollableNode(): IScrollViewHandle | null {
|
|
974
|
+
return this.scrollView ?? null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
getScrollResponder(): IScrollViewHandle | null {
|
|
978
|
+
return this.scrollView ?? null;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
getScrollNode(): ISymbioteNode | null {
|
|
982
|
+
return this.scrollView?.getScrollNode() ?? null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
recordInteraction(): void {
|
|
986
|
+
this.hasInteracted = true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
private scrollToPixel(offset: number, animated: boolean): void {
|
|
990
|
+
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
991
|
+
const target = this.isHorizontal
|
|
992
|
+
? { x: clamped, y: EMPTY_OFFSET }
|
|
993
|
+
: { x: EMPTY_OFFSET, y: clamped };
|
|
994
|
+
if (this.scrollView !== undefined) {
|
|
995
|
+
dlog(
|
|
996
|
+
`Angular VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${this.isHorizontal})`,
|
|
997
|
+
);
|
|
998
|
+
this.scrollView.scrollTo({ x: target.x, y: target.y, animated });
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
dlog(`Angular VirtualizedList scrollTo offset=${clamped} pending-ref`);
|
|
1002
|
+
this.commandedOffset = target;
|
|
1003
|
+
this.cdr.markForCheck();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private offsetForIndexLocal(index: number, viewPosition: number, viewOffset: number): number {
|
|
1007
|
+
const m = this.metricsCore;
|
|
1008
|
+
return offsetForIndex(
|
|
1009
|
+
index,
|
|
1010
|
+
viewPosition,
|
|
1011
|
+
viewOffset,
|
|
1012
|
+
m.count,
|
|
1013
|
+
m.offsets,
|
|
1014
|
+
m.lengths,
|
|
1015
|
+
this.viewportLength,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ---- after-commit effects (run in ngAfterViewChecked, the post-render seam) ----
|
|
1020
|
+
|
|
1021
|
+
// Batch fill: when the throttled window has not reached the target, schedule a re-render so the
|
|
1022
|
+
// window keeps filling toward target over successive ticks (RN's incremental fill).
|
|
1023
|
+
private effectBatchFill(): void {
|
|
1024
|
+
const m = this.metricsCore;
|
|
1025
|
+
if (this.batchTimer !== null) {
|
|
1026
|
+
clearTimeout(this.batchTimer);
|
|
1027
|
+
this.batchTimer = null;
|
|
1028
|
+
}
|
|
1029
|
+
if (m.first <= m.target.first && m.last >= m.target.last) return;
|
|
1030
|
+
this.batchTimer = setTimeout(() => {
|
|
1031
|
+
this.batchTimer = null;
|
|
1032
|
+
this.measureVersion += 1;
|
|
1033
|
+
this.cdr.markForCheck();
|
|
1034
|
+
}, this.updateCellsBatchingPeriodValue);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// onEndReached: fire only when the actual last cell is rendered AND within threshold; dedup by
|
|
1038
|
+
// content length; re-arm on scroll away from the end (RN _maybeCallOnEdgeReached).
|
|
1039
|
+
private effectEndReached(): void {
|
|
1040
|
+
if (!this.endReached.observed || this.viewportLength <= EMPTY_OFFSET) return;
|
|
1041
|
+
const m = this.metricsCore;
|
|
1042
|
+
const { distanceFromEnd, withinThreshold } = computeEndReached(
|
|
1043
|
+
m.total,
|
|
1044
|
+
this.scrollOffset,
|
|
1045
|
+
this.viewportLength,
|
|
1046
|
+
this.onEndReachedThresholdValue,
|
|
1047
|
+
);
|
|
1048
|
+
const lastCellRendered = m.last === m.count - 1;
|
|
1049
|
+
if (withinThreshold && lastCellRendered && this.sentEndForContentLength !== m.total) {
|
|
1050
|
+
this.sentEndForContentLength = m.total;
|
|
1051
|
+
dlog(
|
|
1052
|
+
`Angular VirtualizedList endReached distanceFromEnd=${distanceFromEnd} ` +
|
|
1053
|
+
`(last=${m.last} of ${m.count}, contentLength=${m.total})`,
|
|
1054
|
+
);
|
|
1055
|
+
this.endReached.emit({ distanceFromEnd });
|
|
1056
|
+
}
|
|
1057
|
+
if (!withinThreshold) this.sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// startReached: the top-edge twin of endReached.
|
|
1061
|
+
private effectStartReached(): void {
|
|
1062
|
+
if (!this.startReached.observed || this.viewportLength <= EMPTY_OFFSET) return;
|
|
1063
|
+
const m = this.metricsCore;
|
|
1064
|
+
const { distanceFromStart, withinThreshold } = computeStartReached(
|
|
1065
|
+
this.scrollOffset,
|
|
1066
|
+
this.viewportLength,
|
|
1067
|
+
this.onStartReachedThresholdValue,
|
|
1068
|
+
);
|
|
1069
|
+
const firstCellRendered = m.first === FIRST_INDEX;
|
|
1070
|
+
if (withinThreshold && firstCellRendered && this.sentStartForContentLength !== m.total) {
|
|
1071
|
+
this.sentStartForContentLength = m.total;
|
|
1072
|
+
dlog(
|
|
1073
|
+
`Angular VirtualizedList startReached distanceFromStart=${distanceFromStart} ` +
|
|
1074
|
+
`(first=${m.first}, contentLength=${m.total})`,
|
|
1075
|
+
);
|
|
1076
|
+
this.startReached.emit({ distanceFromStart });
|
|
1077
|
+
}
|
|
1078
|
+
if (!withinThreshold) this.sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Viewability: recompute which rendered cells clear the threshold and, if the viewable set
|
|
1082
|
+
// changed, fire viewableItemsChanged + every config/callback pair, honoring minimumViewTime.
|
|
1083
|
+
private effectViewability(): void {
|
|
1084
|
+
const m = this.metricsCore;
|
|
1085
|
+
const pairs = buildViewabilityPairs(
|
|
1086
|
+
this.viewableItemsChanged.observed
|
|
1087
|
+
? (info: IViewableItemsChangedInfo<ItemT>): void => this.viewableItemsChanged.emit(info)
|
|
1088
|
+
: undefined,
|
|
1089
|
+
this.viewabilityConfig,
|
|
1090
|
+
this.viewabilityConfigCallbackPairs,
|
|
1091
|
+
);
|
|
1092
|
+
if (pairs.length === EMPTY_OFFSET || this.viewportLength <= EMPTY_OFFSET) return;
|
|
1093
|
+
if (m.count === FIRST_INDEX) return;
|
|
1094
|
+
|
|
1095
|
+
const { tokens, map } = computeViewableSet({
|
|
1096
|
+
first: m.first,
|
|
1097
|
+
last: m.last,
|
|
1098
|
+
count: m.count,
|
|
1099
|
+
offsets: m.offsets,
|
|
1100
|
+
lengths: m.lengths,
|
|
1101
|
+
scrollOffset: this.scrollOffset,
|
|
1102
|
+
viewportLength: this.viewportLength,
|
|
1103
|
+
data: this.data,
|
|
1104
|
+
getItem: this.getItem,
|
|
1105
|
+
keyExtractor: this.keyExtractor,
|
|
1106
|
+
pairs,
|
|
1107
|
+
hasInteracted: this.hasInteracted,
|
|
1108
|
+
});
|
|
1109
|
+
const diff = diffViewable(this.lastViewable, map, tokens);
|
|
1110
|
+
if (!diff.hasChanged) return;
|
|
1111
|
+
|
|
1112
|
+
const commitAndFire = (): void => {
|
|
1113
|
+
this.lastViewable = map;
|
|
1114
|
+
dlog(
|
|
1115
|
+
`Angular VirtualizedList viewable=${tokens.length} changed=${diff.changed.length} ` +
|
|
1116
|
+
`(window [${m.first}, ${m.last}])`,
|
|
1117
|
+
);
|
|
1118
|
+
const info: IViewableItemsChangedInfo<ItemT> = {
|
|
1119
|
+
viewableItems: tokens,
|
|
1120
|
+
changed: diff.changed,
|
|
1121
|
+
};
|
|
1122
|
+
for (const pair of pairs) pair.onViewableItemsChanged(info);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
const minimumViewTime = maxMinimumViewTime(pairs);
|
|
1126
|
+
if (this.viewableTimer !== null) {
|
|
1127
|
+
clearTimeout(this.viewableTimer);
|
|
1128
|
+
this.viewableTimer = null;
|
|
1129
|
+
}
|
|
1130
|
+
if (minimumViewTime > EMPTY_OFFSET) {
|
|
1131
|
+
dlog(
|
|
1132
|
+
`Angular VirtualizedList viewability debounce ${minimumViewTime}ms (window [${m.first}, ${m.last}])`,
|
|
1133
|
+
);
|
|
1134
|
+
this.viewableTimer = setTimeout(() => {
|
|
1135
|
+
this.viewableTimer = null;
|
|
1136
|
+
commitAndFire();
|
|
1137
|
+
}, minimumViewTime);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
commitAndFire();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// initialScrollIndex: once the first viewport is known, jump to that index a single time.
|
|
1144
|
+
private effectInitialScroll(): void {
|
|
1145
|
+
if (this.initialScrollIndex === undefined || this.appliedInitialScroll) return;
|
|
1146
|
+
if (this.viewportLength <= EMPTY_OFFSET || this.metricsCore.count === FIRST_INDEX) return;
|
|
1147
|
+
this.appliedInitialScroll = true;
|
|
1148
|
+
// The initial jump is instant (RN does not animate initialScrollIndex).
|
|
1149
|
+
this.scrollToPixel(
|
|
1150
|
+
this.offsetForIndexLocal(this.initialScrollIndex, FIRST_INDEX, EMPTY_OFFSET),
|
|
1151
|
+
false,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// maintainVisibleContentPosition JS anchor adjustment: track the key of the item at
|
|
1156
|
+
// minIndexForVisible; when a prepend moves it down, add the inserted extent in the leading SPACER
|
|
1157
|
+
// (the off-window items native MVCP cannot see) to scrollOffset so the anchored item does not jump
|
|
1158
|
+
// (RN getDerivedStateFromProps).
|
|
1159
|
+
private effectMaintainVisibleContentPosition(): void {
|
|
1160
|
+
const m = this.metricsCore;
|
|
1161
|
+
if (this.maintainVisibleContentPosition === undefined || m.count === FIRST_INDEX) {
|
|
1162
|
+
this.firstVisibleKey = null;
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const minIndexForVisible = this.maintainVisibleContentPosition.minIndexForVisible;
|
|
1166
|
+
const newFirstVisibleKey =
|
|
1167
|
+
m.count > minIndexForVisible ? this.keyFor(minIndexForVisible) : null;
|
|
1168
|
+
const prevKey = this.firstVisibleKey;
|
|
1169
|
+
|
|
1170
|
+
if (prevKey !== null && newFirstVisibleKey !== null && prevKey !== newFirstVisibleKey) {
|
|
1171
|
+
let anchorIndex = -1;
|
|
1172
|
+
for (let index = minIndexForVisible; index < m.count; index += 1) {
|
|
1173
|
+
if (this.keyFor(index) === prevKey) {
|
|
1174
|
+
anchorIndex = index;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (anchorIndex > minIndexForVisible) {
|
|
1179
|
+
const spacerEnd = Math.min(anchorIndex, this.committedWindow.first);
|
|
1180
|
+
const insertedExtent =
|
|
1181
|
+
spacerEnd > minIndexForVisible
|
|
1182
|
+
? m.offsets[spacerEnd] - m.offsets[minIndexForVisible]
|
|
1183
|
+
: EMPTY_OFFSET;
|
|
1184
|
+
if (insertedExtent > EMPTY_OFFSET) {
|
|
1185
|
+
const autoThreshold = this.maintainVisibleContentPosition.autoscrollToTopThreshold;
|
|
1186
|
+
const anchoredNearTop = autoThreshold !== undefined && this.scrollOffset <= autoThreshold;
|
|
1187
|
+
if (anchoredNearTop) {
|
|
1188
|
+
dlog(
|
|
1189
|
+
`Angular VirtualizedList MVCP autoscroll-to-top (offset=${this.scrollOffset} <= ${autoThreshold})`,
|
|
1190
|
+
);
|
|
1191
|
+
this.scrollToPixel(EMPTY_OFFSET, true);
|
|
1192
|
+
} else {
|
|
1193
|
+
dlog(
|
|
1194
|
+
`Angular VirtualizedList MVCP adjust +${insertedExtent}px ` +
|
|
1195
|
+
`(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`,
|
|
1196
|
+
);
|
|
1197
|
+
this.scrollToPixel(this.scrollOffset + insertedExtent, false);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
this.firstVisibleKey = newFirstVisibleKey;
|
|
1203
|
+
}
|
|
1204
|
+
}
|