ngx-kit 6.3.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/angle-selector/README.md +34 -34
- package/box-shadow/README.md +33 -33
- package/color-picker/README.md +52 -49
- package/data-table/README.md +120 -113
- package/date-picker/README.md +74 -69
- package/dialog/README.md +92 -92
- package/drag-resize/README.md +55 -0
- package/dropzone/README.md +422 -422
- package/fesm2022/ngx-kit-angle-selector.mjs +2 -2
- package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
- package/fesm2022/ngx-kit-box-shadow.mjs +2 -2
- package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
- package/fesm2022/ngx-kit-color-picker.mjs +265 -73
- package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-data-table.mjs +2 -2
- package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
- package/fesm2022/ngx-kit-date-picker.mjs +7 -9
- package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-dialog.mjs +3 -3
- package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
- package/fesm2022/ngx-kit-drag-resize.mjs +1664 -0
- package/fesm2022/ngx-kit-drag-resize.mjs.map +1 -0
- package/fesm2022/ngx-kit-drawer-menu.mjs +389 -285
- package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-dropzone.mjs +2 -2
- package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
- package/fesm2022/ngx-kit-form-field.mjs +3 -5
- package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
- package/fesm2022/ngx-kit-gallery.mjs +2 -2
- package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
- package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-grid-layout.mjs +994 -0
- package/fesm2022/ngx-kit-grid-layout.mjs.map +1 -0
- package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
- package/fesm2022/ngx-kit-menu.mjs +8 -8
- package/fesm2022/ngx-kit-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-message.mjs.map +1 -1
- package/fesm2022/ngx-kit-notify.mjs +2 -2
- package/fesm2022/ngx-kit-notify.mjs.map +1 -1
- package/fesm2022/ngx-kit-shared.mjs +54 -3
- package/fesm2022/ngx-kit-shared.mjs.map +1 -1
- package/fesm2022/ngx-kit-time-picker.mjs +2 -2
- package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-virtual-scroll.mjs +392 -0
- package/fesm2022/ngx-kit-virtual-scroll.mjs.map +1 -0
- package/fesm2022/ngx-kit-webflow.mjs +709 -0
- package/fesm2022/ngx-kit-webflow.mjs.map +1 -0
- package/gallery/README.md +71 -71
- package/gradient-picker/README.md +41 -41
- package/grid-layout/README.md +64 -0
- package/image-editor/README.md +12 -12
- package/menu/README.md +85 -85
- package/message/README.md +70 -70
- package/notify/README.md +56 -58
- package/package.json +17 -1
- package/shared/README.md +117 -117
- package/time-picker/README.md +89 -95
- package/types/ngx-kit-color-picker.d.ts +72 -16
- package/types/ngx-kit-drag-resize.d.ts +368 -0
- package/types/ngx-kit-drawer-menu.d.ts +135 -86
- package/types/ngx-kit-grid-layout.d.ts +311 -0
- package/types/ngx-kit-image-editor.d.ts +1 -1
- package/types/ngx-kit-shared.d.ts +12 -1
- package/types/ngx-kit-virtual-scroll.d.ts +179 -0
- package/types/ngx-kit-webflow.d.ts +286 -0
- package/virtual-scroll/README.md +199 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A "line" index range that is currently rendered.
|
|
5
|
+
* A "line" is one item for a plain list, or one row/column of N items
|
|
6
|
+
* for an auto-detected wrapping grid. `end` is exclusive.
|
|
7
|
+
*/
|
|
8
|
+
interface NgxVirtualScrollRange {
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
}
|
|
12
|
+
declare function rangesEqual(a: NgxVirtualScrollRange, b: NgxVirtualScrollRange): boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The scroll axis along which content is virtualized.
|
|
16
|
+
* This is never provided by the consumer — it is always derived from the
|
|
17
|
+
* real, rendered layout of the projected content (see `measureLayout`).
|
|
18
|
+
*/
|
|
19
|
+
type NgxVirtualScrollAxis = 'vertical' | 'horizontal';
|
|
20
|
+
/**
|
|
21
|
+
* Result of measuring the real DOM layout of a small probe batch of items.
|
|
22
|
+
*
|
|
23
|
+
* - `crossCount === 1` -> a plain list (the common case: vertical stack of
|
|
24
|
+
* rows, or a horizontal row of columns).
|
|
25
|
+
* - `crossCount > 1` -> a wrapping grid (e.g. cards laid out with
|
|
26
|
+
* `display: flex; flex-wrap: wrap`): `crossCount` items share a line
|
|
27
|
+
* before wrapping to the next one. The grid still scrolls along a single
|
|
28
|
+
* `axis`, while the cross axis lays out natively (this is how "mixed"
|
|
29
|
+
* horizontal + vertical layouts are supported without a 2D data model).
|
|
30
|
+
*/
|
|
31
|
+
interface NgxVirtualScrollMeasurement {
|
|
32
|
+
axis: NgxVirtualScrollAxis;
|
|
33
|
+
crossCount: number;
|
|
34
|
+
/** Size (px) of one line along `axis`, including margins/border/shadow. */
|
|
35
|
+
lineSize: number;
|
|
36
|
+
}
|
|
37
|
+
/** Escape hatch for consumers who need full control over layout detection. */
|
|
38
|
+
type NgxVirtualScrollMeasureFn = (probeElements: readonly Element[]) => NgxVirtualScrollMeasurement | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Headless, signal-based virtual scroll viewport.
|
|
42
|
+
*
|
|
43
|
+
* There is no `itemSize` and no `orientation` input by design: both are
|
|
44
|
+
* derived by measuring the real, rendered bounding boxes of a small probe
|
|
45
|
+
* batch (see `measure-layout.ts`), so margins/border/box-shadow on your own
|
|
46
|
+
* item styles are always accounted for correctly, and mixed layouts
|
|
47
|
+
* (vertical list of wrapping rows, i.e. a card grid) are supported out of
|
|
48
|
+
* the box.
|
|
49
|
+
*
|
|
50
|
+
* The component does not render your items itself — it exposes reactive
|
|
51
|
+
* signals (`visibleItems`, `range`, `baseIndex`, `axis`, ...) that you
|
|
52
|
+
* consume with a native `@for` block in your own template. This keeps the
|
|
53
|
+
* repeat/track logic entirely under your control (custom track expressions,
|
|
54
|
+
* `@empty` blocks, etc.) instead of hiding it behind a structural directive.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```html
|
|
58
|
+
* <ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="rows" style="height: 480px">
|
|
59
|
+
* @for (row of vs.visibleItems(); track row.id; let i = $index) {
|
|
60
|
+
* <div class="row">{{ vs.baseIndex() + i }} - {{ row.name }}</div>
|
|
61
|
+
* }
|
|
62
|
+
* </ngx-virtual-scroll-viewport>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare class NgxVirtualScrollViewport<T = unknown> {
|
|
66
|
+
/** The full, unvirtualized data set. */
|
|
67
|
+
readonly items: _angular_core.InputSignal<readonly T[]>;
|
|
68
|
+
/** Minimum buffer (px) left before the render range is recomputed. */
|
|
69
|
+
readonly minBufferPx: _angular_core.InputSignal<number>;
|
|
70
|
+
/** Extra buffer (px) rendered outside the visible area on each side. */
|
|
71
|
+
readonly maxBufferPx: _angular_core.InputSignal<number>;
|
|
72
|
+
/** Advanced escape hatch: replace the built-in layout probe entirely. */
|
|
73
|
+
readonly measureFn: _angular_core.InputSignal<NgxVirtualScrollMeasureFn | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* Safe ceiling (px) for the scrollable axis's real DOM size. Only relevant
|
|
76
|
+
* for very large data sets — see `DEFAULT_MAX_AXIS_SIZE_PX`. You should
|
|
77
|
+
* not need to change this; it's exposed only for advanced cases (e.g. you
|
|
78
|
+
* know your app never targets older WebKit and want to raise the ceiling).
|
|
79
|
+
*/
|
|
80
|
+
readonly maxAxisSizePx: _angular_core.InputSignal<number>;
|
|
81
|
+
/** Emits the index of the first fully-visible item as the user scrolls. */
|
|
82
|
+
readonly scrolledIndexChange: _angular_core.OutputEmitterRef<number>;
|
|
83
|
+
/** Emits whenever the rendered line range changes. */
|
|
84
|
+
readonly rangeChange: _angular_core.OutputEmitterRef<NgxVirtualScrollRange>;
|
|
85
|
+
private readonly scrollableRef;
|
|
86
|
+
private readonly wrapperRef;
|
|
87
|
+
private readonly destroyRef;
|
|
88
|
+
private readonly measurement;
|
|
89
|
+
private readonly scrollOffset;
|
|
90
|
+
private readonly viewportSize;
|
|
91
|
+
/** Whether the user is actively scrolling (used only to toggle `will-change`). */
|
|
92
|
+
readonly isScrolling: _angular_core.WritableSignal<boolean>;
|
|
93
|
+
/** Currently rendered line range. */
|
|
94
|
+
readonly range: _angular_core.WritableSignal<NgxVirtualScrollRange>;
|
|
95
|
+
readonly axis: _angular_core.Signal<NgxVirtualScrollAxis>;
|
|
96
|
+
readonly crossCount: _angular_core.Signal<number>;
|
|
97
|
+
readonly lineSize: _angular_core.Signal<number>;
|
|
98
|
+
readonly isMeasured: _angular_core.Signal<boolean>;
|
|
99
|
+
readonly lineCount: _angular_core.Signal<number>;
|
|
100
|
+
/** Real, uncompressed total size along the scroll axis — can exceed the browser's element-size cap. */
|
|
101
|
+
readonly naturalTotalSize: _angular_core.Signal<number>;
|
|
102
|
+
/**
|
|
103
|
+
* Size actually applied to the spacer element, clamped to `maxAxisSizePx`.
|
|
104
|
+
* Equal to `naturalTotalSize()` for any data set small enough to matter in
|
|
105
|
+
* practice — the compression only ever engages for extreme list sizes.
|
|
106
|
+
*/
|
|
107
|
+
readonly totalSize: _angular_core.Signal<number>;
|
|
108
|
+
/**
|
|
109
|
+
* Ratio between the (possibly capped) DOM scroll space and the real,
|
|
110
|
+
* uncompressed content size. `1` unless `naturalTotalSize()` exceeds
|
|
111
|
+
* `maxAxisSizePx()`, in which case it's `< 1` and the DOM scrollbar
|
|
112
|
+
* represents the whole data set in a compressed coordinate space.
|
|
113
|
+
*/
|
|
114
|
+
readonly scaleFactor: _angular_core.Signal<number>;
|
|
115
|
+
/** Absolute index of the first item in `visibleItems()`, for display purposes. */
|
|
116
|
+
readonly baseIndex: _angular_core.Signal<number>;
|
|
117
|
+
/** Slice of `items()` that should actually be rendered right now. */
|
|
118
|
+
readonly visibleItems: _angular_core.Signal<readonly T[]>;
|
|
119
|
+
private readonly renderedBlockSize;
|
|
120
|
+
private readonly contentOffset;
|
|
121
|
+
protected readonly wrapperTransform: _angular_core.Signal<string>;
|
|
122
|
+
protected readonly spacerWidth: _angular_core.Signal<number>;
|
|
123
|
+
protected readonly spacerHeight: _angular_core.Signal<number>;
|
|
124
|
+
private resizeObserver?;
|
|
125
|
+
private scrollingIdleTimer?;
|
|
126
|
+
private scrollRaf;
|
|
127
|
+
constructor();
|
|
128
|
+
/** Programmatically scroll to a given item index. */
|
|
129
|
+
scrollToIndex(index: number, behavior?: ScrollBehavior): void;
|
|
130
|
+
/**
|
|
131
|
+
* Programmatically scroll to a given pixel offset along the scroll axis.
|
|
132
|
+
* `offset` is expressed in real, uncompressed content px (i.e. the same
|
|
133
|
+
* space as `index * lineSize()`) — internally converted to the
|
|
134
|
+
* (possibly compressed) DOM scroll coordinate via `scaleFactor()`.
|
|
135
|
+
*/
|
|
136
|
+
scrollToOffset(offset: number, behavior?: ScrollBehavior): void;
|
|
137
|
+
/** Force a re-measure, e.g. after the container was resized by external layout logic. */
|
|
138
|
+
checkViewportSize(): void;
|
|
139
|
+
private setupListeners;
|
|
140
|
+
private readonly onScroll;
|
|
141
|
+
private markScrolling;
|
|
142
|
+
private measureViewportSize;
|
|
143
|
+
/**
|
|
144
|
+
* Recomputes the rendered line range. Mirrors the CDK "fixed size"
|
|
145
|
+
* strategy: only recompute once the scroll offset gets within
|
|
146
|
+
* `minBufferPx` of the current range's edge, instead of on every pixel.
|
|
147
|
+
*
|
|
148
|
+
* All math here happens in *real* (uncompressed) content px, obtained by
|
|
149
|
+
* dividing the raw DOM scroll offset/viewport size by `scaleFactor()`.
|
|
150
|
+
* This is what makes the very last items reachable even when the total
|
|
151
|
+
* content size has been compressed into a browser-safe DOM range: as
|
|
152
|
+
* `domScrollOffset` sweeps its full (capped) range, `realOffset` still
|
|
153
|
+
* sweeps the entire, real `naturalTotalSize()`.
|
|
154
|
+
*/
|
|
155
|
+
private recomputeRange;
|
|
156
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<NgxVirtualScrollViewport<any>, never>;
|
|
157
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<NgxVirtualScrollViewport<any>, "ngx-virtual-scroll-viewport, ngx-virtual-scroll", ["ngxVirtualScrollViewport"], { "items": { "alias": "items"; "required": false; "isSignal": true; }; "minBufferPx": { "alias": "minBufferPx"; "required": false; "isSignal": true; }; "maxBufferPx": { "alias": "maxBufferPx"; "required": false; "isSignal": true; }; "measureFn": { "alias": "measureFn"; "required": false; "isSignal": true; }; "maxAxisSizePx": { "alias": "maxAxisSizePx"; "required": false; "isSignal": true; }; }, { "scrolledIndexChange": "scrolledIndexChange"; "rangeChange": "rangeChange"; }, never, ["*"], true, never>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pure, framework-agnostic layout probe.
|
|
162
|
+
*
|
|
163
|
+
* Reads the real bounding boxes of a small batch of already-rendered
|
|
164
|
+
* elements and infers, purely from actual layout:
|
|
165
|
+
* - the scroll axis (vertical vs horizontal)
|
|
166
|
+
* - how many items share a single line (1 for a plain list, N for a
|
|
167
|
+
* wrapping grid)
|
|
168
|
+
* - the real size of one line, INCLUDING margins/border/box-shadow,
|
|
169
|
+
* because it is derived from the delta between two real sibling
|
|
170
|
+
* positions in normal document flow rather than from a hardcoded
|
|
171
|
+
* itemSize input.
|
|
172
|
+
*
|
|
173
|
+
* No orientation/itemSize is ever supplied by the consumer — this is the
|
|
174
|
+
* single source of truth for both.
|
|
175
|
+
*/
|
|
176
|
+
declare function measureLayout(probeElements: readonly Element[]): NgxVirtualScrollMeasurement | null;
|
|
177
|
+
|
|
178
|
+
export { NgxVirtualScrollViewport, measureLayout, rangesEqual };
|
|
179
|
+
export type { NgxVirtualScrollAxis, NgxVirtualScrollMeasureFn, NgxVirtualScrollMeasurement, NgxVirtualScrollRange };
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { OnDestroy, EventEmitter, OnInit } from '@angular/core';
|
|
3
|
+
import * as ngx_kit_webflow from 'ngx-kit/webflow';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ngx-flowbuilder — مدلهای پایه
|
|
7
|
+
*/
|
|
8
|
+
type FlowAxis = 'row' | 'column';
|
|
9
|
+
/** موقعیت درج نسبت به آیتمی که ماوس روی آن است */
|
|
10
|
+
type DropZone = 'before' | 'after' | 'above' | 'below' | 'inside';
|
|
11
|
+
interface DropResult {
|
|
12
|
+
targetId: string;
|
|
13
|
+
zone: DropZone;
|
|
14
|
+
}
|
|
15
|
+
interface Rect {
|
|
16
|
+
top: number;
|
|
17
|
+
left: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
right: number;
|
|
21
|
+
bottom: number;
|
|
22
|
+
}
|
|
23
|
+
interface IndicatorState {
|
|
24
|
+
visible: boolean;
|
|
25
|
+
/** خط باریک: 'h' یعنی خط افقی (جدا کردن ردیفها)، 'v' یعنی خط عمودی (جدا کردن کنار هم) */
|
|
26
|
+
orientation: 'h' | 'v' | null;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
length: number;
|
|
30
|
+
/** حالت nest: بهجای خط، یک کادر دور کل container هایلایت میشود */
|
|
31
|
+
highlightRect: Rect | null;
|
|
32
|
+
}
|
|
33
|
+
interface DragStartEvent {
|
|
34
|
+
id: string;
|
|
35
|
+
pointerId: number;
|
|
36
|
+
originRect: Rect;
|
|
37
|
+
}
|
|
38
|
+
interface DragMoveEvent {
|
|
39
|
+
id: string;
|
|
40
|
+
clientX: number;
|
|
41
|
+
clientY: number;
|
|
42
|
+
deltaX: number;
|
|
43
|
+
deltaY: number;
|
|
44
|
+
}
|
|
45
|
+
interface DragEndEvent {
|
|
46
|
+
id: string;
|
|
47
|
+
drop: DropResult | null;
|
|
48
|
+
}
|
|
49
|
+
type ResizeHandlePosition = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
|
50
|
+
interface ResizeMoveEvent {
|
|
51
|
+
id: string;
|
|
52
|
+
handle: ResizeHandlePosition;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
/** فقط برای هندلهای شمال/غرب که مبدأ عنصر هم جابجا میشود */
|
|
56
|
+
offsetX: number;
|
|
57
|
+
offsetY: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ngxDraggable
|
|
62
|
+
* -------------------------------------------------------------
|
|
63
|
+
* تنها دایرکتیو مسئول درگ. کل منطق حرکت با CSS transform (translate3d)
|
|
64
|
+
* روی خودِ عنصر درگشونده انجام میشود — نه با جابجایی واقعی در DOM لیست.
|
|
65
|
+
* جابجاییِ واقعیِ داده فقط یکبار، در لحظهٔ drop، از طریق خروجی
|
|
66
|
+
* (flowDrop) اتفاق میافتد و آنوقت مصرفکننده (کامپوننت کانتینر) با
|
|
67
|
+
* *ngFor + trackBy مدل را آپدیت میکند.
|
|
68
|
+
*
|
|
69
|
+
* دلیل مقیاسپذیری با هزاران آیتم:
|
|
70
|
+
* - در حین pointermove هیچ آیتم دیگری لمس/محاسبه نمیشود.
|
|
71
|
+
* - listener مربوط به تشخیص هدف روی کانتینر (event delegation) است،
|
|
72
|
+
* نه روی تکتک آیتمها (این بخش در ContainerDropzoneDirective است).
|
|
73
|
+
* - آپدیت transform با requestAnimationFrame throttle میشود تا از
|
|
74
|
+
* چندبار layout/paint در هر فریم جلوگیری شود.
|
|
75
|
+
*/
|
|
76
|
+
declare class DraggableDirective implements OnDestroy {
|
|
77
|
+
private readonly el;
|
|
78
|
+
private readonly renderer;
|
|
79
|
+
private readonly zone;
|
|
80
|
+
private readonly dragDrop;
|
|
81
|
+
private readonly dropPos;
|
|
82
|
+
/** شناسهٔ یکتای بلاک — برای شناسایی هنگام درگ لازم است */
|
|
83
|
+
ngxDraggableId: string;
|
|
84
|
+
/** غیرفعال کردن موقت درگ (مثلاً حین ادیت متن داخل بلاک) */
|
|
85
|
+
ngxDraggableDisabled: boolean;
|
|
86
|
+
/** هندلِ اختصاصی؛ اگر ندهی، کل عنصر هندل میشود */
|
|
87
|
+
ngxDragHandleSelector: string | null;
|
|
88
|
+
flowDragStart: EventEmitter<DragStartEvent>;
|
|
89
|
+
flowDragEnd: EventEmitter<DragEndEvent>;
|
|
90
|
+
readonly dragging: i0.WritableSignal<boolean>;
|
|
91
|
+
get draggingClass(): boolean;
|
|
92
|
+
get touchAction(): string;
|
|
93
|
+
private startX;
|
|
94
|
+
private startY;
|
|
95
|
+
private originLeft;
|
|
96
|
+
private originTop;
|
|
97
|
+
private rafId;
|
|
98
|
+
private pendingTransform;
|
|
99
|
+
private cleanupFns;
|
|
100
|
+
constructor();
|
|
101
|
+
private onPointerDown;
|
|
102
|
+
private onPointerMove;
|
|
103
|
+
private onPointerUp;
|
|
104
|
+
ngOnDestroy(): void;
|
|
105
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DraggableDirective, never>;
|
|
106
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<DraggableDirective, "[ngxDraggable]", ["ngxDraggable"], { "ngxDraggableId": { "alias": "ngxDraggableId"; "required": true; }; "ngxDraggableDisabled": { "alias": "ngxDraggableDisabled"; "required": false; }; "ngxDragHandleSelector": { "alias": "ngxDragHandleSelector"; "required": false; }; }, { "flowDragStart": "flowDragStart"; "flowDragEnd": "flowDragEnd"; }, never, never, true, never>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* ngxResizable
|
|
111
|
+
* -------------------------------------------------------------
|
|
112
|
+
* تنها دایرکتیو مسئول ریسایز. مستقل از DraggableDirective.
|
|
113
|
+
* روی عنصر اصلی قرار میگیرد و به همراه ۸ هندل (که خودت در تمپلیت با
|
|
114
|
+
* class مشخص میکنی، مثلاً <span class="rz-handle" data-handle="se">)
|
|
115
|
+
* کار میکند.
|
|
116
|
+
*
|
|
117
|
+
* فقط CSS width/height/left/top آپدیت میشود (از طریق خروجی flowResize)
|
|
118
|
+
* و مصرفکننده تصمیم میگیرد این مقادیر را چطور به مدل/استایل بایند کند —
|
|
119
|
+
* دایرکتیو خودش مستقیماً مدل داده را تغییر نمیدهد.
|
|
120
|
+
*/
|
|
121
|
+
declare class ResizableDirective implements OnDestroy {
|
|
122
|
+
private readonly el;
|
|
123
|
+
private readonly renderer;
|
|
124
|
+
private readonly zone;
|
|
125
|
+
ngxResizableId: string;
|
|
126
|
+
ngxResizableMinWidth: number;
|
|
127
|
+
ngxResizableMinHeight: number;
|
|
128
|
+
ngxResizableDisabled: boolean;
|
|
129
|
+
/** انتخابگر هندلها؛ باید attribute یا data-handle داشته باشند */
|
|
130
|
+
ngxResizeHandleSelector: string;
|
|
131
|
+
flowResizeStart: EventEmitter<{
|
|
132
|
+
id: string;
|
|
133
|
+
handle: ResizeHandlePosition;
|
|
134
|
+
}>;
|
|
135
|
+
flowResizeMove: EventEmitter<ResizeMoveEvent>;
|
|
136
|
+
flowResizeEnd: EventEmitter<ResizeMoveEvent>;
|
|
137
|
+
readonly resizing: i0.WritableSignal<boolean>;
|
|
138
|
+
private startX;
|
|
139
|
+
private startY;
|
|
140
|
+
private startW;
|
|
141
|
+
private startH;
|
|
142
|
+
private handle;
|
|
143
|
+
private rafId;
|
|
144
|
+
private pending;
|
|
145
|
+
private unlisten;
|
|
146
|
+
constructor();
|
|
147
|
+
private onDown;
|
|
148
|
+
private onMove;
|
|
149
|
+
private onUp;
|
|
150
|
+
ngOnDestroy(): void;
|
|
151
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ResizableDirective, never>;
|
|
152
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<ResizableDirective, "[ngxResizable]", ["ngxResizable"], { "ngxResizableId": { "alias": "ngxResizableId"; "required": true; }; "ngxResizableMinWidth": { "alias": "ngxResizableMinWidth"; "required": false; }; "ngxResizableMinHeight": { "alias": "ngxResizableMinHeight"; "required": false; }; "ngxResizableDisabled": { "alias": "ngxResizableDisabled"; "required": false; }; "ngxResizeHandleSelector": { "alias": "ngxResizeHandleSelector"; "required": false; }; }, { "flowResizeStart": "flowResizeStart"; "flowResizeMove": "flowResizeMove"; "flowResizeEnd": "flowResizeEnd"; }, never, never, true, never>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* ngxDropzoneContainer
|
|
157
|
+
* -------------------------------------------------------------
|
|
158
|
+
* روی هر کانتینری که فرزندانش قابلدرگانددراپ هستند قرار میگیرد
|
|
159
|
+
* (چه ریشهٔ صفحه، چه یک بلاک تودرتو).
|
|
160
|
+
*
|
|
161
|
+
* فقط یک pointermove-listener سراسری (روی document، حین یک درگ فعال)
|
|
162
|
+
* دارد و با event.target.closest('[data-flow-id]') هدف را پیدا میکند.
|
|
163
|
+
* این یعنی صرفنظر از اینکه چند هزار فرزند وجود دارد، هزینهٔ هر حرکتِ
|
|
164
|
+
* ماوس ثابت (O(1)) است — نه O(n).
|
|
165
|
+
*
|
|
166
|
+
* هر فرزند مستقیمِ قابلدرگ باید attribute زیر را داشته باشد
|
|
167
|
+
* (بهصورت خودکار توسط DraggableDirective/تمپلیت ست میشود):
|
|
168
|
+
* [attr.data-flow-id]="item.id"
|
|
169
|
+
* [attr.data-flow-container]="isContainer ? '' : null"
|
|
170
|
+
*/
|
|
171
|
+
declare class DropzoneContainerDirective implements OnInit, OnDestroy {
|
|
172
|
+
private readonly el;
|
|
173
|
+
private readonly zone;
|
|
174
|
+
private readonly renderer;
|
|
175
|
+
private readonly dragDrop;
|
|
176
|
+
private readonly dropPos;
|
|
177
|
+
/** جهت چیدمان فرزندان مستقیم این کانتینر */
|
|
178
|
+
ngxDropzoneAxis: FlowAxis;
|
|
179
|
+
/** آیا خودِ این کانتینر میتواند میزبان nest شدن باشد */
|
|
180
|
+
ngxDropzoneNestable: boolean;
|
|
181
|
+
private unlistenMove;
|
|
182
|
+
private unlistenLeave;
|
|
183
|
+
ngOnInit(): void;
|
|
184
|
+
private handleMove;
|
|
185
|
+
ngOnDestroy(): void;
|
|
186
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DropzoneContainerDirective, never>;
|
|
187
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<DropzoneContainerDirective, "[ngxDropzoneContainer]", ["ngxDropzoneContainer"], { "ngxDropzoneAxis": { "alias": "ngxDropzoneAxis"; "required": false; }; "ngxDropzoneNestable": { "alias": "ngxDropzoneNestable"; "required": false; }; }, {}, never, never, true, never>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* FlowDropIndicatorComponent
|
|
192
|
+
* -------------------------------------------------------------
|
|
193
|
+
* یکبار در ریشهٔ صفحهساز قرار میگیرد (position: fixed).
|
|
194
|
+
* تنها المنتی است که در طول درگ مدام آپدیت میشود؛ بقیهٔ آیتمهای
|
|
195
|
+
* لیست دستنخورده باقی میمانند. کاملاً با computed از signal سرویس
|
|
196
|
+
* تغذیه میشود، بدون هیچ منطق اضافه در کامپوننت.
|
|
197
|
+
*/
|
|
198
|
+
declare class FlowDropIndicatorComponent {
|
|
199
|
+
private readonly dropPos;
|
|
200
|
+
readonly indicator: i0.Signal<ngx_kit_webflow.IndicatorState>;
|
|
201
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FlowDropIndicatorComponent, never>;
|
|
202
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<FlowDropIndicatorComponent, "flow-drop-indicator", never, {}, {}, never, never, true, never>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* DragDropService
|
|
207
|
+
* -------------------------------------------------------------
|
|
208
|
+
* وضعیت سراسریِ «چه چیزی الان در حال درگ است» را نگه میدارد.
|
|
209
|
+
* فقط signal، بدون هیچ Subject/Observable اضافه — سبک و قابلپیشبینی.
|
|
210
|
+
*/
|
|
211
|
+
declare class DragDropService {
|
|
212
|
+
/** id آیتمی که هماکنون درگ میشود، یا null */
|
|
213
|
+
readonly activeId: i0.WritableSignal<string | null>;
|
|
214
|
+
/** آخرین نتیجهٔ محاسبهشدهٔ drop توسط DropPositionService */
|
|
215
|
+
readonly pendingDrop: i0.WritableSignal<DropResult | null>;
|
|
216
|
+
startDrag(id: string): void;
|
|
217
|
+
setPendingDrop(result: DropResult | null): void;
|
|
218
|
+
endDrag(): DropResult | null;
|
|
219
|
+
get isDragging(): boolean;
|
|
220
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DragDropService, never>;
|
|
221
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<DragDropService>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
declare class DropPositionService {
|
|
225
|
+
private readonly _indicator;
|
|
226
|
+
/** استیت فقطخواندنیِ اندیکاتور برای بایند در تمپلیت */
|
|
227
|
+
readonly indicator: i0.Signal<IndicatorState>;
|
|
228
|
+
private lastTargetId;
|
|
229
|
+
private lastZone;
|
|
230
|
+
/**
|
|
231
|
+
* فراخوانی در هر pointermove حین درگ.
|
|
232
|
+
* targetEl باید همان element.closest('[data-flow-id]') باشد که در
|
|
233
|
+
* دایرکتیو کانتینر با event delegation پیدا شده — نه یک لوپ روی آیتمها.
|
|
234
|
+
*/
|
|
235
|
+
evaluate(params: {
|
|
236
|
+
targetEl: HTMLElement;
|
|
237
|
+
targetId: string;
|
|
238
|
+
isContainer: boolean;
|
|
239
|
+
parentAxis: 'row' | 'column';
|
|
240
|
+
clientX: number;
|
|
241
|
+
clientY: number;
|
|
242
|
+
}): {
|
|
243
|
+
targetId: string;
|
|
244
|
+
zone: DropZone;
|
|
245
|
+
};
|
|
246
|
+
/** رسم خط/کادر راهنما - فقط نوشتن به یک signal، بدون لمس بقیهٔ DOM */
|
|
247
|
+
private paintIndicator;
|
|
248
|
+
clear(): void;
|
|
249
|
+
get current(): {
|
|
250
|
+
targetId: string;
|
|
251
|
+
zone: DropZone;
|
|
252
|
+
} | null;
|
|
253
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DropPositionService, never>;
|
|
254
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<DropPositionService>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
interface FlowNode {
|
|
258
|
+
id: string;
|
|
259
|
+
type: string;
|
|
260
|
+
axis: 'row' | 'column';
|
|
261
|
+
isContainer: boolean;
|
|
262
|
+
props?: Record<string, unknown>;
|
|
263
|
+
children: FlowNode[];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* FlowTreeService
|
|
267
|
+
* -------------------------------------------------------------
|
|
268
|
+
* مدل درختیِ صفحه (دقیقاً همان چیزی که Webflow "Element Panel" اسمش
|
|
269
|
+
* میگذارد). تغییر مدل فقط در لحظهٔ drop رخ میدهد (نه در هر
|
|
270
|
+
* pointermove) — به همین دلیل حتی با هزاران بلاک، re-render فقط
|
|
271
|
+
* یکبار در پایان درگ اتفاق میافتد.
|
|
272
|
+
*
|
|
273
|
+
* پیادهسازی immutable-ish: فقط مسیر تغییریافته کپی میشود، نه کل
|
|
274
|
+
* درخت (structural sharing) تا با درختهای بزرگ هم CPU/GC اذیت نشود.
|
|
275
|
+
*/
|
|
276
|
+
declare class FlowTreeService {
|
|
277
|
+
private readonly _root;
|
|
278
|
+
readonly root: i0.Signal<FlowNode>;
|
|
279
|
+
setRoot(node: FlowNode): void;
|
|
280
|
+
applyDrop(draggedId: string, drop: DropResult): void;
|
|
281
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FlowTreeService, never>;
|
|
282
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<FlowTreeService>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export { DragDropService, DraggableDirective, DropPositionService, DropzoneContainerDirective, FlowDropIndicatorComponent, FlowTreeService, ResizableDirective };
|
|
286
|
+
export type { DragEndEvent, DragMoveEvent, DragStartEvent, DropResult, DropZone, FlowAxis, FlowNode, IndicatorState, Rect, ResizeHandlePosition, ResizeMoveEvent };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @ngx-kit/virtual-scroll
|
|
2
|
+
|
|
3
|
+
A zoneless, signal-based virtual scroll viewport for Angular. No `@angular/material`
|
|
4
|
+
or `@angular/cdk` dependency, no `NgZone`, no `ngOnChanges`, no `@Input() itemSize`,
|
|
5
|
+
no `@Input() orientation` — everything that can be derived from the real DOM is
|
|
6
|
+
derived from the real DOM.
|
|
7
|
+
|
|
8
|
+
## Why this rewrite
|
|
9
|
+
|
|
10
|
+
| Old design | This design |
|
|
11
|
+
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
12
|
+
| `NgZone` injected, `zone.run` everywhere | No `NgZone` at all — works natively under `provideZonelessChangeDetection()` |
|
|
13
|
+
| `ngOnChanges` + `SimpleChanges` | `input()` signals + `computed()` / `effect()` |
|
|
14
|
+
| `@Input() itemSize` (broke as soon as your item had margin/border/box-shadow) | Real item size is **measured** from `getBoundingClientRect()` deltas between actually rendered siblings, so margins/borders/shadows are always accounted for |
|
|
15
|
+
| `@Input() orientation` | Axis is **auto-detected** from the real layout of your own item markup |
|
|
16
|
+
| Custom `*ngxVirtualFor` structural directive | Plain native `@for` over a `visibleItems()` signal — you keep full control of `track`, `@empty`, etc. |
|
|
17
|
+
|
|
18
|
+
## Install & import
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { NgxVirtualScrollViewport } from '@ngx-kit/virtual-scroll';
|
|
22
|
+
|
|
23
|
+
@Component({
|
|
24
|
+
standalone: true,
|
|
25
|
+
imports: [NgxVirtualScrollViewport],
|
|
26
|
+
...
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Basic usage — vertical list (auto-detected)
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="items" style="height: 400px">
|
|
34
|
+
@for (item of vs.visibleItems(); track $index; let i = $index) {
|
|
35
|
+
<div class="row">{{ vs.baseIndex() + i | number }} - {{ item.name }}</div>
|
|
36
|
+
}
|
|
37
|
+
</ngx-virtual-scroll-viewport>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is exactly the case that used to break: `.row` can have `margin`, `border`,
|
|
41
|
+
`box-shadow`, `padding` — anything. The viewport measures the real distance between
|
|
42
|
+
two consecutively rendered rows and uses that, not a number you have to keep in sync
|
|
43
|
+
with your CSS.
|
|
44
|
+
|
|
45
|
+
```css
|
|
46
|
+
.row {
|
|
47
|
+
height: 48px;
|
|
48
|
+
border: 1px rgb(166 166 166 / 51%) solid;
|
|
49
|
+
box-shadow: 0px 0px 20px rgb(122 122 122 / 20%);
|
|
50
|
+
border-radius: 5px;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
margin: 10px;
|
|
54
|
+
padding: 15px;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Horizontal lists & wrapping grids (auto-detected)
|
|
59
|
+
|
|
60
|
+
The axis is inferred purely from how your own items lay out:
|
|
61
|
+
|
|
62
|
+
- Items stack top-to-bottom (default block flow) → **vertical** list.
|
|
63
|
+
- Items lay out left-to-right in a single line (e.g. `display: inline-flex` on the
|
|
64
|
+
item, or a `flex-wrap: nowrap` row) → **horizontal** list.
|
|
65
|
+
- Items lay out left-to-right and _wrap_ to a new line (e.g. cards with
|
|
66
|
+
`display: flex; flex-wrap: wrap`) → treated as a **vertical** list of "lines",
|
|
67
|
+
where each line holds N items. This is how mixed horizontal + vertical layouts
|
|
68
|
+
(a card grid that scrolls vertically) are supported without a separate 2D data
|
|
69
|
+
model — the cross axis (row width) lays out and scrolls natively, only the line
|
|
70
|
+
axis is virtualized.
|
|
71
|
+
|
|
72
|
+
You don't configure any of this — you only make sure your `.item` CSS actually lays
|
|
73
|
+
out the way you want, and the viewport detects it.
|
|
74
|
+
|
|
75
|
+
## Mixed content (e.g. wide, virtualized table)
|
|
76
|
+
|
|
77
|
+
Both axes of the scroll container are always natively scrollable
|
|
78
|
+
(`overflow: auto` on both). Only the detected axis is virtualized; the cross axis
|
|
79
|
+
(for example, a table wider than its container) scrolls with the browser's native
|
|
80
|
+
scrollbar, so wide tables with virtualized rows work out of the box:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<div class="table-shell">
|
|
84
|
+
<table><thead>...</thead></table>
|
|
85
|
+
<!-- kept outside, sticky/fixed header -->
|
|
86
|
+
|
|
87
|
+
<ngx-virtual-scroll-viewport #vs="ngxVirtualScrollViewport" [items]="rows" style="height: 480px">
|
|
88
|
+
<table>
|
|
89
|
+
<tbody>
|
|
90
|
+
@for (row of vs.visibleItems(); track row.id; let i = $index) {
|
|
91
|
+
<tr>
|
|
92
|
+
<td>{{ vs.baseIndex() + i }}</td>
|
|
93
|
+
...
|
|
94
|
+
</tr>
|
|
95
|
+
}
|
|
96
|
+
</tbody>
|
|
97
|
+
</table>
|
|
98
|
+
</ngx-virtual-scroll-viewport>
|
|
99
|
+
</div>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Very large data sets (1M+ rows)
|
|
103
|
+
|
|
104
|
+
Browsers clamp any element's real CSS `height`/`width` to a hard maximum
|
|
105
|
+
(~33.5M px in Chromium, far less in older WebKit/Gecko). Once a naive
|
|
106
|
+
"spacer" element crosses that ceiling, the browser silently clamps it,
|
|
107
|
+
`scrollHeight` stops growing, and everything past that point becomes
|
|
108
|
+
unreachable — the classic "scroll gets stuck around ~500k rows" symptom.
|
|
109
|
+
|
|
110
|
+
This viewport handles it automatically: the spacer's real DOM size is capped
|
|
111
|
+
at `maxAxisSizePx` (6,000,000px by default — safely under every known
|
|
112
|
+
browser limit), and the DOM scrollbar operates in a _compressed_ coordinate
|
|
113
|
+
space via `scaleFactor()`. All range/index math is done against the real,
|
|
114
|
+
uncompressed content size (`naturalTotalSize()`), so scrolling the
|
|
115
|
+
(compressed) native scrollbar all the way to the end still lands you exactly
|
|
116
|
+
on the last item, no matter how many millions of rows you have. The only
|
|
117
|
+
user-visible trade-off for truly massive lists is that the scrollbar thumb's
|
|
118
|
+
motion is no longer perfectly 1:1 with "one row per pixel" — an unavoidable
|
|
119
|
+
consequence of representing an arbitrarily large data set inside a bounded
|
|
120
|
+
native scrollbar, and the same trade-off every large-scale virtual scroll
|
|
121
|
+
implementation makes.
|
|
122
|
+
|
|
123
|
+
You don't need to configure anything for this — it only engages once
|
|
124
|
+
`naturalTotalSize()` actually exceeds `maxAxisSizePx`, and is a no-op
|
|
125
|
+
(`scaleFactor() === 1`) for the vast majority of real-world lists.
|
|
126
|
+
|
|
127
|
+
## API
|
|
128
|
+
|
|
129
|
+
### Inputs
|
|
130
|
+
|
|
131
|
+
| Input | Type | Default | Notes |
|
|
132
|
+
| --------------- | --------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
133
|
+
| `items` | `readonly T[]` | `[]` | Full data set |
|
|
134
|
+
| `minBufferPx` | `number` | `150` | Buffer left before recomputing the range |
|
|
135
|
+
| `maxBufferPx` | `number` | `300` | Extra px rendered outside the viewport |
|
|
136
|
+
| `measureFn` | `NgxVirtualScrollMeasureFn` | `undefined` | Advanced override for the built-in layout probe |
|
|
137
|
+
| `maxAxisSizePx` | `number` | `6_000_000` | Safe ceiling for the real DOM scroll size — see "Very large data sets" below |
|
|
138
|
+
|
|
139
|
+
### Outputs
|
|
140
|
+
|
|
141
|
+
| Output | Type |
|
|
142
|
+
| --------------------- | ----------------------------------------- |
|
|
143
|
+
| `scrolledIndexChange` | `OutputEmitterRef<number>` |
|
|
144
|
+
| `rangeChange` | `OutputEmitterRef<NgxVirtualScrollRange>` |
|
|
145
|
+
|
|
146
|
+
### Public signals (via `#vs="ngxVirtualScrollViewport"`)
|
|
147
|
+
|
|
148
|
+
| Signal | Type | Meaning |
|
|
149
|
+
| -------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
|
150
|
+
| `visibleItems()` | `readonly T[]` | Slice of `items()` to render right now |
|
|
151
|
+
| `range()` | `{ start, end }` | Currently rendered line range |
|
|
152
|
+
| `baseIndex()` | `number` | Absolute index of `visibleItems()[0]` |
|
|
153
|
+
| `axis()` | `'vertical' \| 'horizontal'` | Auto-detected scroll axis |
|
|
154
|
+
| `crossCount()` | `number` | Items per line (1 for a plain list) |
|
|
155
|
+
| `lineSize()` | `number` | Measured px size of one line |
|
|
156
|
+
| `isMeasured()` | `boolean` | Whether the initial layout probe has completed |
|
|
157
|
+
| `isScrolling()` | `boolean` | True while an active scroll gesture is in progress |
|
|
158
|
+
| `naturalTotalSize()` | `number` | Real, uncompressed total size along the scroll axis |
|
|
159
|
+
| `totalSize()` | `number` | Size actually applied to the spacer (capped at `maxAxisSizePx`) |
|
|
160
|
+
| `scaleFactor()` | `number` | `totalSize() / naturalTotalSize()`; `1` unless the data set is extremely large |
|
|
161
|
+
|
|
162
|
+
### Methods
|
|
163
|
+
|
|
164
|
+
| Method | Description |
|
|
165
|
+
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
166
|
+
| `scrollToIndex(index, behavior?)` | Scroll to a given item index |
|
|
167
|
+
| `scrollToOffset(offsetPx, behavior?)` | Scroll to a given pixel offset |
|
|
168
|
+
| `checkViewportSize()` | Force a re-measure (rarely needed — a `ResizeObserver` already handles container resizes) |
|
|
169
|
+
|
|
170
|
+
## Performance notes
|
|
171
|
+
|
|
172
|
+
- Fully `ChangeDetectionStrategy.OnPush` and zoneless-compatible — no `NgZone`
|
|
173
|
+
import anywhere; signal writes are what schedule change detection.
|
|
174
|
+
- Scroll handling is `requestAnimationFrame`-throttled and the listener is
|
|
175
|
+
registered with `{ passive: true }`.
|
|
176
|
+
- The range is only recomputed once the scroll offset gets within
|
|
177
|
+
`minBufferPx` of the current range's edge — not on every scroll event.
|
|
178
|
+
- Layout is only ever **read** (`getBoundingClientRect`) inside an
|
|
179
|
+
`afterRenderEffect({ read: ... })` block, so DOM measurement never
|
|
180
|
+
interleaves with — and never forces — a layout/style write.
|
|
181
|
+
- `will-change: transform` is only applied to the content wrapper while
|
|
182
|
+
`isScrolling()` is true, and removed ~150ms after the last scroll event, to
|
|
183
|
+
avoid holding a compositor layer alive for the component's entire lifetime.
|
|
184
|
+
- `contain: strict` / `contain: layout style` isolate the viewport's layout
|
|
185
|
+
and paint from the rest of the page.
|
|
186
|
+
|
|
187
|
+
## How auto-measurement works
|
|
188
|
+
|
|
189
|
+
On first render, up to 24 items are rendered "in the open" (un-transformed) purely
|
|
190
|
+
to be measured — see `measure-layout.ts`. Their real `getBoundingClientRect()`
|
|
191
|
+
deltas are used to infer axis, items-per-line, and line size in one pass, all pure
|
|
192
|
+
functions with no Angular dependency (so they are trivially unit-testable). Once
|
|
193
|
+
measured, the real virtualized range replaces the probe range and rendering
|
|
194
|
+
proceeds normally. If your rows can change size at runtime (e.g. responsive
|
|
195
|
+
breakpoints), a `ResizeObserver` on the scroll container triggers `checkViewportSize()`
|
|
196
|
+
automatically to re-fit the viewport dimension; if the _line size itself_ changes at
|
|
197
|
+
runtime you can force a full re-measure by re-creating the component (e.g. keyed with
|
|
198
|
+
`@if`/`*ngIf` + a changing key), which is the same limitation `cdk-virtual-scroll-viewport`
|
|
199
|
+
has with its own `FixedSizeVirtualScrollStrategy`.
|