@epoch32/visage 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +4 -4
- package/dist/charts/index.js +5 -5
- package/dist/devtools/index.js +7 -7
- package/dist/dom/index.js +3 -3
- package/dist/form/index.js +2 -2
- package/dist/{index-h6tjqk6e.js → index-1jcd8gjf.js} +28 -17
- package/dist/{index-6kfe1nr4.js → index-1yhbg34h.js} +41 -9
- package/dist/{index-9fvarkrx.js → index-3y73zgbk.js} +1 -1
- package/dist/{index-y9bq7h9g.js → index-5jhzwtdz.js} +2 -2
- package/dist/{index-a4xns6cc.js → index-65j7a9et.js} +1 -1
- package/dist/{index-dyvgtp43.js → index-9bez1tmf.js} +2 -2
- package/dist/{index-df5e614s.js → index-awcrz19g.js} +1 -1
- package/dist/{index-061jf06a.js → index-cgepvvd4.js} +1 -1
- package/dist/{index-1xtrtz1t.js → index-dmhmde2z.js} +2 -2
- package/dist/{index-zr6cvset.js → index-dz6n88aq.js} +5 -5
- package/dist/{index-0madxn60.js → index-enc8cmea.js} +1 -1
- package/dist/{index-js199er4.js → index-g1drdx7k.js} +12 -1
- package/dist/{index-grhdtcb5.js → index-k7zd3awa.js} +14 -5
- package/dist/{index-8x8xz80v.js → index-kykzcs7p.js} +5 -4
- package/dist/{index-dvq5f3s8.js → index-vbsc8z8h.js} +121 -52
- package/dist/{index-8czhpea8.js → index-x6whpv54.js} +30 -19
- package/dist/{index-5s255b3w.js → index-xby428n5.js} +23 -22
- package/dist/{index-x2rs3vab.js → index-y3nprgn7.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -18
- package/dist/jsx-runtime/index.js +4 -4
- package/dist/query/index.js +3 -3
- package/dist/router/index.js +5 -5
- package/dist/router/router.d.ts +1 -0
- package/dist/rpc/index.js +4 -4
- package/dist/server/index.js +2 -2
- package/dist/signals/index.js +1 -1
- package/dist/signals/system.d.ts +2 -0
- package/dist/style/index.js +6 -6
- package/dist/template/index.js +2 -2
- package/dist/ui/dropdown-menu.d.ts +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +9 -7
- package/dist/ui/overlay.d.ts +53 -0
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/utils.d.ts +1 -1
- package/dist/virtual/index.js +5 -5
- package/package.json +1 -1
- package/src/dom/control-flow.ts +31 -21
- package/src/form/form.ts +29 -17
- package/src/index.ts +2 -0
- package/src/router/router.tsx +12 -2
- package/src/signals/effect.ts +2 -2
- package/src/signals/scope.ts +10 -8
- package/src/signals/store.ts +32 -1
- package/src/signals/system.ts +8 -0
- package/src/template/template.ts +12 -1
- package/src/ui/command-dialog.tsx +18 -1
- package/src/ui/dialog.tsx +16 -14
- package/src/ui/dropdown-menu.tsx +17 -14
- package/src/ui/index.ts +4 -0
- package/src/ui/overlay.ts +130 -0
- package/src/ui/popover.tsx +20 -22
- package/src/ui/utils.ts +8 -2
- package/src/virtual/virtualizer.ts +31 -18
|
@@ -2,6 +2,7 @@ import { Portal } from "../dom/control-flow";
|
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
3
|
import type { CommandProps } from "./command";
|
|
4
4
|
import { CommandRoot } from "./command";
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
5
6
|
import { createControllableSignal } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface CommandDialogProps extends CommandProps {
|
|
@@ -19,6 +20,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
|
|
|
19
20
|
onChange: props.onOpenChange
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
const contentRef: { current: HTMLElement | null } = { current: null };
|
|
24
|
+
|
|
25
|
+
const dismiss = useDismiss(() => contentRef.current, {
|
|
26
|
+
enabled: () => open(),
|
|
27
|
+
onDismiss: () => setOpen(false),
|
|
28
|
+
escapeKey: true,
|
|
29
|
+
outsidePointer: false
|
|
30
|
+
});
|
|
31
|
+
|
|
22
32
|
const { overlayClass, contentClass, style, ...rest } = props;
|
|
23
33
|
|
|
24
34
|
return (
|
|
@@ -39,8 +49,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
|
|
|
39
49
|
onClick={(e: MouseEvent) => {
|
|
40
50
|
if (e.target === e.currentTarget) setOpen(false);
|
|
41
51
|
}}
|
|
52
|
+
onKeyDown={dismiss.handleKeyDown}
|
|
42
53
|
>
|
|
43
|
-
<div
|
|
54
|
+
<div
|
|
55
|
+
ref={(el: HTMLElement | null) => {
|
|
56
|
+
contentRef.current = el;
|
|
57
|
+
}}
|
|
58
|
+
style={{ width: "100%", maxWidth: "560px" }}
|
|
59
|
+
class={contentClass}
|
|
60
|
+
>
|
|
44
61
|
<CommandRoot {...rest} />
|
|
45
62
|
</div>
|
|
46
63
|
</div>
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { Portal } from "../dom/control-flow";
|
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
3
|
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface DialogContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -207,25 +208,26 @@ export interface DialogContentProps {
|
|
|
207
208
|
export function DialogContent(props: DialogContentProps): Child {
|
|
208
209
|
const ctx = useContext(DialogContext);
|
|
209
210
|
|
|
211
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
212
|
+
enabled: () => ctx?.open() ?? false,
|
|
213
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
214
|
+
escapeKey: true,
|
|
215
|
+
trapFocus: true,
|
|
216
|
+
outsidePointer: false, // backdrop clicks are handled by DialogOverlay
|
|
217
|
+
closeSelector: "[data-dialog-close]",
|
|
218
|
+
onEscapeKeyDown: (e) => {
|
|
219
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
210
223
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
211
224
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (e.key === "Escape") {
|
|
215
|
-
ctx?.setOpen(false);
|
|
216
|
-
e.stopPropagation();
|
|
217
|
-
} else if (e.key === "Tab" && ctx?.contentRef.current) {
|
|
218
|
-
trapFocus(ctx.contentRef.current, e);
|
|
219
|
-
}
|
|
225
|
+
dismiss.handleKeyDown(e);
|
|
220
226
|
};
|
|
221
227
|
|
|
222
228
|
const handleClick = (e: MouseEvent) => {
|
|
223
229
|
if (props.onClick) props.onClick(e);
|
|
224
|
-
|
|
225
|
-
const target = e.target as HTMLElement | null;
|
|
226
|
-
if (target?.closest("[data-dialog-close]")) {
|
|
227
|
-
ctx?.setOpen(false);
|
|
228
|
-
}
|
|
230
|
+
dismiss.handleClick(e);
|
|
229
231
|
};
|
|
230
232
|
|
|
231
233
|
const {
|
package/src/ui/dropdown-menu.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Portal } from "../dom/control-flow";
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
|
-
import {
|
|
3
|
+
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface DropdownMenuContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -207,6 +208,19 @@ export interface DropdownMenuContentProps {
|
|
|
207
208
|
export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
208
209
|
const ctx = useContext(DropdownMenuContext);
|
|
209
210
|
|
|
211
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
212
|
+
enabled: () => ctx?.open() ?? false,
|
|
213
|
+
onDismiss: () => {
|
|
214
|
+
if (ctx) {
|
|
215
|
+
ctx.setOpen(false);
|
|
216
|
+
ctx.triggerRef.current?.focus();
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
escapeKey: true,
|
|
220
|
+
outsidePointer: true,
|
|
221
|
+
outsideIgnore: () => [ctx?.triggerRef.current]
|
|
222
|
+
});
|
|
223
|
+
|
|
210
224
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
211
225
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
212
226
|
if (e.defaultPrevented) return;
|
|
@@ -233,10 +247,7 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
|
233
247
|
items[items.length - 1]?.focus();
|
|
234
248
|
e.preventDefault();
|
|
235
249
|
} else if (e.key === "Escape") {
|
|
236
|
-
|
|
237
|
-
ctx.setOpen(false);
|
|
238
|
-
ctx.triggerRef.current?.focus();
|
|
239
|
-
}
|
|
250
|
+
dismiss.handleKeyDown(e);
|
|
240
251
|
e.preventDefault();
|
|
241
252
|
} else if (e.key === "Tab") {
|
|
242
253
|
if (ctx) ctx.setOpen(false);
|
|
@@ -259,14 +270,6 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
|
259
270
|
ref={(el: HTMLElement | null) => {
|
|
260
271
|
if (ctx) {
|
|
261
272
|
ctx.contentRef.current = el;
|
|
262
|
-
if (el) {
|
|
263
|
-
const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
|
|
264
|
-
if (ctx.open()) {
|
|
265
|
-
ctx.setOpen(false);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
onCleanup(cleanup);
|
|
269
|
-
}
|
|
270
273
|
}
|
|
271
274
|
}}
|
|
272
275
|
id={ctx?.menuId}
|
package/src/ui/index.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { onCleanup } from "../signals";
|
|
2
|
+
import { onClickOutside, trapFocus } from "./utils";
|
|
3
|
+
|
|
4
|
+
export interface UseDismissOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether dismiss listeners are active.
|
|
7
|
+
* Defaults to true.
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean | (() => boolean);
|
|
10
|
+
/**
|
|
11
|
+
* Callback fired when dismissal is requested (Escape, click outside, or close button).
|
|
12
|
+
*/
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Close on 'Escape' key press.
|
|
16
|
+
* Defaults to true.
|
|
17
|
+
*/
|
|
18
|
+
escapeKey?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Close on pointer interaction outside the target element(s).
|
|
21
|
+
* Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
outsidePointer?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Additional element(s) to exclude from outside click detection (e.g. trigger button).
|
|
26
|
+
*/
|
|
27
|
+
outsideIgnore?:
|
|
28
|
+
| Array<HTMLElement | null | undefined>
|
|
29
|
+
| HTMLElement
|
|
30
|
+
| null
|
|
31
|
+
| undefined
|
|
32
|
+
| (() => Array<HTMLElement | null | undefined>);
|
|
33
|
+
/**
|
|
34
|
+
* Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
|
|
35
|
+
* Defaults to false.
|
|
36
|
+
*/
|
|
37
|
+
trapFocus?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
|
|
40
|
+
*/
|
|
41
|
+
closeSelector?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Callback fired when an outside pointer interaction occurs.
|
|
44
|
+
*/
|
|
45
|
+
onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Callback fired when an Escape keydown occurs.
|
|
48
|
+
*/
|
|
49
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Headless hook to manage overlay dismissal:
|
|
54
|
+
* - Outside pointer dismissal (click outside)
|
|
55
|
+
* - Escape key dismissal
|
|
56
|
+
* - Keyboard focus trapping (Tab cycle)
|
|
57
|
+
* - Close button selector dismissal delegation
|
|
58
|
+
*/
|
|
59
|
+
export function useDismiss(
|
|
60
|
+
element: () => HTMLElement | null | undefined,
|
|
61
|
+
options: UseDismissOptions
|
|
62
|
+
): {
|
|
63
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
64
|
+
handleClick: (e: MouseEvent) => void;
|
|
65
|
+
} {
|
|
66
|
+
const isEnabled = () => {
|
|
67
|
+
if (typeof options.enabled === "function") return options.enabled();
|
|
68
|
+
return options.enabled ?? true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
72
|
+
if (!isEnabled() || e.defaultPrevented) return;
|
|
73
|
+
|
|
74
|
+
if (e.key === "Escape" && (options.escapeKey ?? true)) {
|
|
75
|
+
if (options.onEscapeKeyDown) {
|
|
76
|
+
options.onEscapeKeyDown(e);
|
|
77
|
+
}
|
|
78
|
+
if (!e.defaultPrevented) {
|
|
79
|
+
options.onDismiss();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
}
|
|
82
|
+
} else if (e.key === "Tab" && options.trapFocus) {
|
|
83
|
+
const el = element();
|
|
84
|
+
if (el) {
|
|
85
|
+
trapFocus(el, e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClick = (e: MouseEvent) => {
|
|
91
|
+
if (!isEnabled() || e.defaultPrevented) return;
|
|
92
|
+
if (options.closeSelector) {
|
|
93
|
+
const target = e.target as HTMLElement | null;
|
|
94
|
+
if (target?.closest(options.closeSelector)) {
|
|
95
|
+
options.onDismiss();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Setup outside pointer listener
|
|
101
|
+
if (typeof document !== "undefined") {
|
|
102
|
+
const unbind = onClickOutside(
|
|
103
|
+
() => {
|
|
104
|
+
const el = element();
|
|
105
|
+
const rawIgnores =
|
|
106
|
+
typeof options.outsideIgnore === "function"
|
|
107
|
+
? options.outsideIgnore()
|
|
108
|
+
: options.outsideIgnore;
|
|
109
|
+
const ignores = Array.isArray(rawIgnores) ? rawIgnores : rawIgnores ? [rawIgnores] : [];
|
|
110
|
+
return [el, ...ignores];
|
|
111
|
+
},
|
|
112
|
+
(event) => {
|
|
113
|
+
if (!isEnabled()) return;
|
|
114
|
+
if (options.outsidePointer === false) return;
|
|
115
|
+
if (options.onOutsidePointer) {
|
|
116
|
+
options.onOutsidePointer(event);
|
|
117
|
+
}
|
|
118
|
+
if (!event.defaultPrevented) {
|
|
119
|
+
options.onDismiss();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
onCleanup(unbind);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
handleKeyDown,
|
|
128
|
+
handleClick
|
|
129
|
+
};
|
|
130
|
+
}
|
package/src/ui/popover.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Portal } from "../dom/control-flow";
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
|
-
import {
|
|
3
|
+
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface PopoverContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -138,24 +139,29 @@ export interface PopoverContentProps {
|
|
|
138
139
|
export function PopoverContent(props: PopoverContentProps): Child {
|
|
139
140
|
const ctx = useContext(PopoverContext);
|
|
140
141
|
|
|
142
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
143
|
+
enabled: () => ctx?.open() ?? false,
|
|
144
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
145
|
+
escapeKey: true,
|
|
146
|
+
outsidePointer: true,
|
|
147
|
+
outsideIgnore: () => [ctx?.triggerRef.current],
|
|
148
|
+
closeSelector: "[data-popover-close]",
|
|
149
|
+
onEscapeKeyDown: (e) => {
|
|
150
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
151
|
+
if (!e.defaultPrevented) {
|
|
152
|
+
ctx?.triggerRef.current?.focus();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
141
157
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
142
158
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (e.key === "Escape" && ctx) {
|
|
146
|
-
ctx.setOpen(false);
|
|
147
|
-
ctx.triggerRef.current?.focus();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
}
|
|
159
|
+
dismiss.handleKeyDown(e);
|
|
150
160
|
};
|
|
151
161
|
|
|
152
162
|
const handleClick = (e: MouseEvent) => {
|
|
153
163
|
if (props.onClick) props.onClick(e);
|
|
154
|
-
|
|
155
|
-
const target = e.target as HTMLElement | null;
|
|
156
|
-
if (target?.closest("[data-popover-close]")) {
|
|
157
|
-
ctx?.setOpen(false);
|
|
158
|
-
}
|
|
164
|
+
dismiss.handleClick(e);
|
|
159
165
|
};
|
|
160
166
|
|
|
161
167
|
const {
|
|
@@ -175,14 +181,6 @@ export function PopoverContent(props: PopoverContentProps): Child {
|
|
|
175
181
|
ref={(el: HTMLElement | null) => {
|
|
176
182
|
if (ctx) {
|
|
177
183
|
ctx.contentRef.current = el;
|
|
178
|
-
if (el) {
|
|
179
|
-
const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
|
|
180
|
-
if (ctx.open()) {
|
|
181
|
-
ctx.setOpen(false);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
onCleanup(cleanup);
|
|
185
|
-
}
|
|
186
184
|
}
|
|
187
185
|
}}
|
|
188
186
|
id={ctx?.contentId}
|
package/src/ui/utils.ts
CHANGED
|
@@ -79,14 +79,20 @@ export function trapFocus(container: HTMLElement, event: KeyboardEvent) {
|
|
|
79
79
|
* Listen for click outside a target element
|
|
80
80
|
*/
|
|
81
81
|
export function onClickOutside(
|
|
82
|
-
elements:
|
|
82
|
+
elements:
|
|
83
|
+
| Array<HTMLElement | null | undefined>
|
|
84
|
+
| HTMLElement
|
|
85
|
+
| null
|
|
86
|
+
| undefined
|
|
87
|
+
| (() => Array<HTMLElement | null | undefined>),
|
|
83
88
|
handler: (event: MouseEvent | TouchEvent) => void
|
|
84
89
|
): () => void {
|
|
85
90
|
const listener = (event: MouseEvent | TouchEvent) => {
|
|
86
91
|
const target = event.target as Node | null;
|
|
87
92
|
if (!target) return;
|
|
88
93
|
|
|
89
|
-
const
|
|
94
|
+
const raw = typeof elements === "function" ? elements() : elements;
|
|
95
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
90
96
|
const isInside = list.some((el) => el?.contains(target));
|
|
91
97
|
|
|
92
98
|
if (!isInside) {
|
|
@@ -56,30 +56,41 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
|
|
|
56
56
|
return getItemHeight(index);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for (let i = 0; i < count; i++) {
|
|
67
|
-
total += getItemSize(i);
|
|
68
|
-
}
|
|
69
|
-
return total;
|
|
70
|
-
});
|
|
59
|
+
// Cached prefix offsets for O(1) offset lookups and true O(log N) binary search
|
|
60
|
+
const offsetsCache: number[] = [0];
|
|
61
|
+
let offsetsValidUpTo = 0;
|
|
62
|
+
|
|
63
|
+
const invalidateOffsetsFrom = (index: number) => {
|
|
64
|
+
offsetsValidUpTo = Math.min(offsetsValidUpTo, index);
|
|
65
|
+
};
|
|
71
66
|
|
|
72
67
|
const getOffsetForIndex = (targetIndex: number): number => {
|
|
68
|
+
if (targetIndex <= 0) return 0;
|
|
73
69
|
if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
|
|
74
70
|
return targetIndex * options.itemHeight;
|
|
75
71
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
offset += getItemSize(i);
|
|
72
|
+
if (targetIndex <= offsetsValidUpTo) {
|
|
73
|
+
return offsetsCache[targetIndex]!;
|
|
79
74
|
}
|
|
80
|
-
|
|
75
|
+
for (let i = offsetsValidUpTo; i < targetIndex; i++) {
|
|
76
|
+
offsetsCache[i + 1] = (offsetsCache[i] ?? 0) + getItemSize(i);
|
|
77
|
+
}
|
|
78
|
+
offsetsValidUpTo = targetIndex;
|
|
79
|
+
return offsetsCache[targetIndex]!;
|
|
81
80
|
};
|
|
82
81
|
|
|
82
|
+
const measurementVersion = signal(0, { internal: true });
|
|
83
|
+
|
|
84
|
+
// Compute prefix offsets for fast range calculation
|
|
85
|
+
const totalSize = computed(() => {
|
|
86
|
+
measurementVersion();
|
|
87
|
+
const count = getCount();
|
|
88
|
+
if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
|
|
89
|
+
return count * options.itemHeight;
|
|
90
|
+
}
|
|
91
|
+
return getOffsetForIndex(count);
|
|
92
|
+
});
|
|
93
|
+
|
|
83
94
|
const findIndexAtOffset = (offset: number): number => {
|
|
84
95
|
const count = getCount();
|
|
85
96
|
if (count === 0) return 0;
|
|
@@ -110,12 +121,14 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
|
|
|
110
121
|
};
|
|
111
122
|
|
|
112
123
|
const startIndex = computed(() => {
|
|
124
|
+
measurementVersion();
|
|
113
125
|
const offset = scrollOffset();
|
|
114
126
|
const rawStart = findIndexAtOffset(offset);
|
|
115
127
|
return Math.max(0, rawStart - overscan);
|
|
116
128
|
});
|
|
117
129
|
|
|
118
130
|
const endIndex = computed(() => {
|
|
131
|
+
measurementVersion();
|
|
119
132
|
const count = getCount();
|
|
120
133
|
if (count === 0) return 0;
|
|
121
134
|
const endOffset = scrollOffset() + (containerSize() || 500);
|
|
@@ -255,8 +268,8 @@ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
|
|
|
255
268
|
: element.getBoundingClientRect().height;
|
|
256
269
|
if (measuredSize > 0 && dynamicSizeCache.get(index) !== measuredSize) {
|
|
257
270
|
dynamicSizeCache.set(index, measuredSize);
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
invalidateOffsetsFrom(index);
|
|
272
|
+
measurementVersion.update((v) => v + 1);
|
|
260
273
|
}
|
|
261
274
|
};
|
|
262
275
|
|