@vuetify/v0 0.1.3 → 0.1.4
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/adapter-BN-h5JbD.d.mts +344 -0
- package/dist/adapter-CJIj5ySf.d.mts +11 -0
- package/dist/adapter-D_0ptsPm.mjs +229 -0
- package/dist/adapters-BQ-Lr3PW.mjs +1 -0
- package/dist/adapters-DEXpW9If.mjs +1 -0
- package/dist/adapters-Q9WaOsJp.mjs +1 -0
- package/dist/adapters-SSDVth5e.mjs +1 -0
- package/dist/browser/index.js +359 -933
- package/dist/components/index.d.mts +19 -6
- package/dist/components/index.mjs +37 -5
- package/dist/{components-C0zEvP7w.mjs → components-CMuzTJdK.mjs} +11 -4
- package/dist/composables/index.d.mts +17 -4
- package/dist/composables/index.mjs +36 -4
- package/dist/consola-DiUk9j3y.d.mts +15 -0
- package/dist/consola-GBy8XRuL.mjs +31 -0
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +3 -3
- package/dist/constants-B8pMSkcW.mjs +1 -0
- package/dist/createDataTable-BJnmvxfb.mjs +387 -0
- package/dist/createGroup-CPhp6FUh.mjs +363 -0
- package/dist/createPagination-cyOWlpfH.mjs +234 -0
- package/dist/createSelection-uqEbFgha.mjs +261 -0
- package/dist/createSingle-B59ya3aa.mjs +179 -0
- package/dist/createTokens-BlhLdHXE.mjs +682 -0
- package/dist/createTrinity-EeHQSsb6.mjs +149 -0
- package/dist/data-table/adapters/client/index.d.mts +3 -0
- package/dist/data-table/adapters/client/index.mjs +9 -0
- package/dist/data-table/adapters/index.d.mts +5 -0
- package/dist/data-table/adapters/index.mjs +12 -0
- package/dist/data-table/adapters/server/index.d.mts +3 -0
- package/dist/data-table/adapters/server/index.mjs +7 -0
- package/dist/data-table/adapters/virtual/index.d.mts +3 -0
- package/dist/data-table/adapters/virtual/index.mjs +9 -0
- package/dist/data-table/index.d.mts +7 -0
- package/dist/data-table/index.mjs +25 -0
- package/dist/date/index.d.mts +1 -1
- package/dist/date/index.mjs +2 -2
- package/dist/features/adapters/flagsmith/index.d.mts +16 -0
- package/dist/features/adapters/flagsmith/index.mjs +3 -0
- package/dist/features/adapters/launchdarkly/index.d.mts +15 -0
- package/dist/features/adapters/launchdarkly/index.mjs +3 -0
- package/dist/features/adapters/posthog/index.d.mts +15 -0
- package/dist/features/adapters/posthog/index.mjs +4 -0
- package/dist/features/index.d.mts +17 -0
- package/dist/features/index.mjs +18 -0
- package/dist/flagsmith-CHmpPGR7.mjs +40 -0
- package/dist/{globals-BwYsH-rt.mjs → globals-B_GVfG06.mjs} +2 -2
- package/dist/index-7mWYFHA6.d.mts +251 -0
- package/dist/index-BDTbJClL.d.mts +11 -0
- package/dist/index-BuOz3Rgm.d.mts +32 -0
- package/dist/{index-Bmem4l8m.d.mts → index-Bv1VN_j5.d.mts} +1 -1
- package/dist/{index-DFM5iBqh.d.mts → index-CQrDeqsK.d.mts} +280 -279
- package/dist/{index-2wh69fk5.d.mts → index-Dd0m0LIJ.d.mts} +135 -1407
- package/dist/{index-DBZTizq2.d.mts → index-UNwEBkQT.d.mts} +16 -2
- package/dist/index.d.mts +20 -7
- package/dist/index.mjs +39 -7
- package/dist/json/web-types.json +1 -1
- package/dist/launchdarkly-W8t8ZANX.mjs +28 -0
- package/dist/locale/adapters/index.d.mts +3 -0
- package/dist/locale/adapters/index.mjs +6 -0
- package/dist/locale/adapters/v0/index.d.mts +3 -0
- package/dist/locale/adapters/v0/index.mjs +5 -0
- package/dist/locale/index.d.mts +17 -0
- package/dist/locale/index.mjs +17 -0
- package/dist/logger/adapters/consola/index.d.mts +3 -0
- package/dist/logger/adapters/consola/index.mjs +3 -0
- package/dist/logger/adapters/index.d.mts +5 -0
- package/dist/logger/adapters/index.mjs +8 -0
- package/dist/logger/adapters/pino/index.d.mts +3 -0
- package/dist/logger/adapters/pino/index.mjs +5 -0
- package/dist/logger/adapters/v0/index.d.mts +3 -0
- package/dist/logger/adapters/v0/index.mjs +5 -0
- package/dist/logger/index.d.mts +17 -0
- package/dist/logger/index.mjs +10 -0
- package/dist/memory-BsZGbGnG.mjs +27 -0
- package/dist/memory-DVjquu_v.d.mts +25 -0
- package/dist/permissions/adapters/index.d.mts +18 -0
- package/dist/permissions/adapters/index.mjs +6 -0
- package/dist/permissions/adapters/v0/index.d.mts +18 -0
- package/dist/permissions/adapters/v0/index.mjs +5 -0
- package/dist/permissions/index.d.mts +17 -0
- package/dist/permissions/index.mjs +15 -0
- package/dist/pino-01x_NmWp.mjs +49 -0
- package/dist/pino-p-jDkNQd.d.mts +24 -0
- package/dist/posthog-BrGBPPir.mjs +50 -0
- package/dist/server-6cs14bj1.d.mts +20 -0
- package/dist/server-ClCwiLMA.mjs +45 -0
- package/dist/storage/adapters/index.d.mts +3 -0
- package/dist/storage/adapters/index.mjs +3 -0
- package/dist/storage/adapters/memory/index.d.mts +2 -0
- package/dist/storage/adapters/memory/index.mjs +3 -0
- package/dist/storage/index.d.mts +17 -0
- package/dist/storage/index.mjs +37 -0
- package/dist/theme/adapters/index.d.mts +17 -0
- package/dist/theme/adapters/index.mjs +6 -0
- package/dist/theme/adapters/v0/index.d.mts +17 -0
- package/dist/theme/adapters/v0/index.mjs +5 -0
- package/dist/theme/index.d.mts +17 -0
- package/dist/theme/index.mjs +16 -0
- package/dist/toArray-DIeXVX1C.mjs +38 -0
- package/dist/types/index.d.mts +1 -1
- package/dist/useClickOutside-p8HP07At.mjs +4680 -0
- package/dist/useFeatures-C4JF3Hgr.mjs +122 -0
- package/dist/useLocale-DlbShzF-.mjs +85 -0
- package/dist/useLogger-CO9JwbSJ.mjs +283 -0
- package/dist/usePermissions-DQVT_4nD.mjs +128 -0
- package/dist/useTheme-Bcw1Ebyt.mjs +135 -0
- package/dist/utilities/index.d.mts +3 -3
- package/dist/utilities/index.mjs +3 -3
- package/dist/{utilities-BbKjbXTx.mjs → utilities-BqS1sC1W.mjs} +20 -3
- package/dist/v0-B40YkIQ-.mjs +21 -0
- package/dist/v0-BOxb3nD1.d.mts +33 -0
- package/dist/v0-BogbIzpM.mjs +41 -0
- package/dist/v0-BtQCSBOG.mjs +37 -0
- package/dist/v0-C-9dv_1S.d.mts +10 -0
- package/dist/v0-CtjuUUPl.mjs +73 -0
- package/dist/v0-DHMoZ7dS.d.mts +22 -0
- package/dist/v0-ONBWyQYQ.mjs +83 -0
- package/dist/v0-RepwEY1F.d.mts +9 -0
- package/dist/virtual-B3IPkHC0.d.mts +9 -0
- package/dist/virtual-BP9WXRzN.mjs +44 -0
- package/package.json +34 -2
- package/dist/useClickOutside-DrkNYxDe.mjs +0 -8871
- /package/dist/{adapter-D95FHAtt.d.mts → adapter-DcwxFryd.d.mts} +0 -0
- /package/dist/{constants-DHKqjdjH.mjs → adapters-BGJKsBrw.mjs} +0 -0
- /package/dist/{htmlElements-DO9n35bD.mjs → htmlElements-d8iBCGpO.mjs} +0 -0
- /package/dist/{index-BLK67J_-.d.mts → index-BFBN5Zl3.d.mts} +0 -0
|
@@ -0,0 +1,4680 @@
|
|
|
1
|
+
import { a as SUPPORTS_OBSERVER, i as SUPPORTS_MUTATION_OBSERVER, n as SUPPORTS_INTERSECTION_OBSERVER, r as SUPPORTS_MATCH_MEDIA, t as IN_BROWSER } from "./globals-B_GVfG06.mjs";
|
|
2
|
+
import { a as isElement, c as isNull, g as mergeDeep, h as isUndefined, l as isNullOrUndefined, o as isFunction, p as isString, t as clamp, u as isNumber, v as useId$1, y as instanceExists } from "./utilities-BqS1sC1W.mjs";
|
|
3
|
+
import { i as useContext, n as createContext, t as createTrinity } from "./createTrinity-EeHQSsb6.mjs";
|
|
4
|
+
import { a as createPlugin, i as useLogger, o as createPluginContext } from "./useLogger-CO9JwbSJ.mjs";
|
|
5
|
+
import { i as createRegistry } from "./createTokens-BlhLdHXE.mjs";
|
|
6
|
+
import { t as createSelection } from "./createSelection-uqEbFgha.mjs";
|
|
7
|
+
import { t as createSingle } from "./createSingle-B59ya3aa.mjs";
|
|
8
|
+
import { t as createGroup } from "./createGroup-CPhp6FUh.mjs";
|
|
9
|
+
import { t as toArray } from "./toArray-DIeXVX1C.mjs";
|
|
10
|
+
import { a as useLocale } from "./useLocale-DlbShzF-.mjs";
|
|
11
|
+
import { t as MemoryAdapter } from "./memory-BsZGbGnG.mjs";
|
|
12
|
+
import { computed, effectScope, isRef, nextTick, onScopeDispose, reactive, readonly, ref, shallowReactive, shallowReadonly, shallowRef, toRaw, toRef, toValue, unref, watch, watchEffect } from "vue";
|
|
13
|
+
|
|
14
|
+
//#region src/composables/createBreadcrumbs/index.ts
|
|
15
|
+
/**
|
|
16
|
+
* @module createBreadcrumbs
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Breadcrumb navigation composable built on createSingle.
|
|
20
|
+
*
|
|
21
|
+
* Key features:
|
|
22
|
+
* - Extends createSingle for consistent registry patterns
|
|
23
|
+
* - Navigation methods: first, prev, select (with truncation)
|
|
24
|
+
* - Derived state: depth, isRoot, isEmpty
|
|
25
|
+
* - Trinity pattern for dependency injection
|
|
26
|
+
*
|
|
27
|
+
* Inheritance chain: createRegistry → createSelection → createSingle → createBreadcrumbs
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Creates a breadcrumbs navigation model.
|
|
31
|
+
*
|
|
32
|
+
* @param options The options for the breadcrumbs instance.
|
|
33
|
+
* @returns A breadcrumbs context with navigation methods.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createBreadcrumbs } from '@vuetify/v0'
|
|
38
|
+
*
|
|
39
|
+
* const breadcrumbs = createBreadcrumbs()
|
|
40
|
+
*
|
|
41
|
+
* breadcrumbs.register({ text: 'Home' })
|
|
42
|
+
* breadcrumbs.register({ text: 'Products' })
|
|
43
|
+
* breadcrumbs.register({ text: 'Electronics' })
|
|
44
|
+
*
|
|
45
|
+
* // Navigate back
|
|
46
|
+
* breadcrumbs.prev() // removes Electronics
|
|
47
|
+
* breadcrumbs.first() // truncates to Home
|
|
48
|
+
*
|
|
49
|
+
* // Derived state
|
|
50
|
+
* breadcrumbs.depth.value // 1
|
|
51
|
+
* breadcrumbs.isRoot.value // true
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
function createBreadcrumbs(options = {}) {
|
|
55
|
+
const single = createSingle({
|
|
56
|
+
...options,
|
|
57
|
+
reactive: true,
|
|
58
|
+
enroll: true
|
|
59
|
+
});
|
|
60
|
+
const depth = toRef(() => single.size);
|
|
61
|
+
const isRoot = toRef(() => depth.value <= 1);
|
|
62
|
+
const isEmpty = toRef(() => depth.value === 0);
|
|
63
|
+
/**
|
|
64
|
+
* Select an item and truncate everything after it.
|
|
65
|
+
*/
|
|
66
|
+
function select(id) {
|
|
67
|
+
const ticket = single.get(id);
|
|
68
|
+
if (!ticket) return;
|
|
69
|
+
const index = ticket.index;
|
|
70
|
+
const toRemove = single.values().filter((item) => item.index > index).map((item) => item.id);
|
|
71
|
+
if (toRemove.length > 0) single.offboard(toRemove);
|
|
72
|
+
single.select(id);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Navigate to root (first item).
|
|
76
|
+
*/
|
|
77
|
+
function first() {
|
|
78
|
+
const root = single.seek("first");
|
|
79
|
+
if (root) select(root.id);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Navigate up one level (select previous item, removing current).
|
|
83
|
+
*/
|
|
84
|
+
function prev() {
|
|
85
|
+
if (single.size <= 1) return;
|
|
86
|
+
const currentIndex = single.selectedIndex.value;
|
|
87
|
+
if (currentIndex <= 0) return;
|
|
88
|
+
const prevId = single.lookup(currentIndex - 1);
|
|
89
|
+
if (prevId) select(prevId);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...single,
|
|
93
|
+
select,
|
|
94
|
+
first,
|
|
95
|
+
prev,
|
|
96
|
+
depth,
|
|
97
|
+
isRoot,
|
|
98
|
+
isEmpty,
|
|
99
|
+
get size() {
|
|
100
|
+
return single.size;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Creates a breadcrumbs context for dependency injection.
|
|
106
|
+
*
|
|
107
|
+
* @param options The options including namespace.
|
|
108
|
+
* @returns A trinity: [useBreadcrumbs, provideBreadcrumbs, defaultContext]
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const [useBreadcrumbs, provideBreadcrumbs] = createBreadcrumbsContext()
|
|
113
|
+
*
|
|
114
|
+
* // Parent component
|
|
115
|
+
* provideBreadcrumbs()
|
|
116
|
+
*
|
|
117
|
+
* // Child component
|
|
118
|
+
* const breadcrumbs = useBreadcrumbs()
|
|
119
|
+
* breadcrumbs.register({ text: 'Details' })
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
function createBreadcrumbsContext(_options = {}) {
|
|
123
|
+
const { namespace = "v0:breadcrumbs", ...options } = _options;
|
|
124
|
+
const [useBreadcrumbsContext, _provideBreadcrumbsContext] = createContext(namespace);
|
|
125
|
+
const context = createBreadcrumbs(options);
|
|
126
|
+
function provideBreadcrumbsContext(_context = context, app) {
|
|
127
|
+
return _provideBreadcrumbsContext(_context, app);
|
|
128
|
+
}
|
|
129
|
+
return createTrinity(useBreadcrumbsContext, provideBreadcrumbsContext, context);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Returns the current breadcrumbs instance from context.
|
|
133
|
+
*
|
|
134
|
+
* @param namespace The namespace. @default 'v0:breadcrumbs'
|
|
135
|
+
* @returns The breadcrumbs context.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```vue
|
|
139
|
+
* <script setup lang="ts">
|
|
140
|
+
* import { useBreadcrumbs } from '@vuetify/v0'
|
|
141
|
+
*
|
|
142
|
+
* const breadcrumbs = useBreadcrumbs()
|
|
143
|
+
* <\/script>
|
|
144
|
+
*
|
|
145
|
+
* <template>
|
|
146
|
+
* <nav>
|
|
147
|
+
* <template v-for="ticket in breadcrumbs.values()" :key="ticket.id">
|
|
148
|
+
* <a @click="breadcrumbs.select(ticket.id)">{{ ticket.text }}</a>
|
|
149
|
+
* </template>
|
|
150
|
+
* </nav>
|
|
151
|
+
* </template>
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
function useBreadcrumbs(namespace = "v0:breadcrumbs") {
|
|
155
|
+
return useContext(namespace);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/composables/useHydration/index.ts
|
|
160
|
+
/**
|
|
161
|
+
* @module useHydration
|
|
162
|
+
*
|
|
163
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-hydration
|
|
164
|
+
*
|
|
165
|
+
* @remarks
|
|
166
|
+
* SSR hydration state management composable.
|
|
167
|
+
*
|
|
168
|
+
* Key features:
|
|
169
|
+
* - Hydration state detection (browser vs SSR)
|
|
170
|
+
* - Root component detection
|
|
171
|
+
* - Readonly hydration state refs
|
|
172
|
+
* - Plugin installation support
|
|
173
|
+
* - Perfect for hydration-safe rendering
|
|
174
|
+
*
|
|
175
|
+
* Essential for composables that need to behave differently during SSR vs client-side.
|
|
176
|
+
*/
|
|
177
|
+
/**
|
|
178
|
+
* Creates a new hydration instance.
|
|
179
|
+
*
|
|
180
|
+
* @returns A new hydration instance.
|
|
181
|
+
*
|
|
182
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-hydration
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* import { createHydration } from '@vuetify/v0'
|
|
187
|
+
*
|
|
188
|
+
* const hydration = createHydration()
|
|
189
|
+
* console.log(hydration.isHydrated.value) // false
|
|
190
|
+
* hydration.hydrate()
|
|
191
|
+
* console.log(hydration.isHydrated.value) // true
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
function createHydration() {
|
|
195
|
+
const isHydrated = shallowRef(false);
|
|
196
|
+
const isSettled = shallowRef(false);
|
|
197
|
+
function hydrate() {
|
|
198
|
+
isHydrated.value = true;
|
|
199
|
+
}
|
|
200
|
+
function settle() {
|
|
201
|
+
isSettled.value = true;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
isHydrated: shallowReadonly(isHydrated),
|
|
205
|
+
isSettled: shallowReadonly(isSettled),
|
|
206
|
+
hydrate,
|
|
207
|
+
settle
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function createFallbackHydration() {
|
|
211
|
+
return {
|
|
212
|
+
isHydrated: shallowReadonly(shallowRef(true)),
|
|
213
|
+
isSettled: shallowReadonly(shallowRef(true)),
|
|
214
|
+
hydrate: () => {},
|
|
215
|
+
settle: () => {}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Creates a new hydration context trinity.
|
|
220
|
+
*
|
|
221
|
+
* @param options Options for creating the hydration context.
|
|
222
|
+
* @template E The type of the hydration context.
|
|
223
|
+
* @returns A new hydration context trinity.
|
|
224
|
+
*
|
|
225
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-hydration
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* import { createHydrationContext } from '@vuetify/v0'
|
|
230
|
+
*
|
|
231
|
+
* export const [useHydrationContext, provideHydrationContext, context] = createHydrationContext({
|
|
232
|
+
* namespace: 'app:hydration',
|
|
233
|
+
* })
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
const [createHydrationContext, createHydrationPlugin, useHydration] = createPluginContext("v0:hydration", () => createHydration(), {
|
|
237
|
+
fallback: () => createFallbackHydration(),
|
|
238
|
+
setup: (context, app, _options) => {
|
|
239
|
+
app.mixin({ async mounted() {
|
|
240
|
+
if (!/* @__PURE__ */ isNull(this.$parent)) return;
|
|
241
|
+
context.hydrate();
|
|
242
|
+
await nextTick();
|
|
243
|
+
context.settle();
|
|
244
|
+
} });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/composables/createObserver/index.ts
|
|
250
|
+
/**
|
|
251
|
+
* @module createObserver
|
|
252
|
+
*
|
|
253
|
+
* @remarks
|
|
254
|
+
* Internal factory for browser Observer API composables. Encapsulates the
|
|
255
|
+
* shared lifecycle, hydration, pause/resume/stop, and target-watch logic
|
|
256
|
+
* used by useResizeObserver, useIntersectionObserver, and useMutationObserver.
|
|
257
|
+
*
|
|
258
|
+
* The `observer` ref uses a three-state sentinel:
|
|
259
|
+
* - `undefined` — not yet created
|
|
260
|
+
* - `null` — permanently stopped (`stop()` was called; `setup()` is a no-op)
|
|
261
|
+
* - instance — active observer
|
|
262
|
+
*/
|
|
263
|
+
function createObserver(target, userCallback, config) {
|
|
264
|
+
const { isHydrated } = useHydration();
|
|
265
|
+
const targetRef = toRef(target);
|
|
266
|
+
const observer = shallowRef(void 0);
|
|
267
|
+
const isPaused = shallowRef(false);
|
|
268
|
+
const isActive = toRef(() => !!observer.value);
|
|
269
|
+
function invoke(entries) {
|
|
270
|
+
config.onEntry?.(entries);
|
|
271
|
+
userCallback(entries);
|
|
272
|
+
if (config.once) {
|
|
273
|
+
if (config.shouldStop ? config.shouldStop(entries) : true) stop();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function setup() {
|
|
277
|
+
if (/* @__PURE__ */ isNull(observer.value)) return;
|
|
278
|
+
if (!isHydrated.value || !config.supports || !/* @__PURE__ */ isElement(targetRef.value) || isPaused.value) return;
|
|
279
|
+
observer.value = config.create(invoke);
|
|
280
|
+
config.observe(observer.value, targetRef.value);
|
|
281
|
+
if (config.immediate) {
|
|
282
|
+
const entries = config.immediate(targetRef.value);
|
|
283
|
+
if (config.onceIncludesImmediate) invoke(entries);
|
|
284
|
+
else {
|
|
285
|
+
config.onEntry?.(entries);
|
|
286
|
+
userCallback(entries);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
watch(() => targetRef.value, (el, oldEl) => {
|
|
291
|
+
if (oldEl || observer.value) cleanup();
|
|
292
|
+
if (isHydrated.value && /* @__PURE__ */ isElement(el)) setup();
|
|
293
|
+
}, { immediate: true });
|
|
294
|
+
let stopHydrationWatch;
|
|
295
|
+
if (!isHydrated.value) stopHydrationWatch = watch(() => isHydrated.value, (hydrated) => {
|
|
296
|
+
if (hydrated && /* @__PURE__ */ isElement(targetRef.value) && !observer.value) {
|
|
297
|
+
setup();
|
|
298
|
+
stopHydrationWatch?.();
|
|
299
|
+
stopHydrationWatch = void 0;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
function cleanup() {
|
|
303
|
+
if (observer.value) {
|
|
304
|
+
observer.value.disconnect();
|
|
305
|
+
observer.value = void 0;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function pause() {
|
|
309
|
+
isPaused.value = true;
|
|
310
|
+
config.onPause?.();
|
|
311
|
+
observer.value?.disconnect();
|
|
312
|
+
}
|
|
313
|
+
function resume() {
|
|
314
|
+
isPaused.value = false;
|
|
315
|
+
setup();
|
|
316
|
+
}
|
|
317
|
+
function stop() {
|
|
318
|
+
stopHydrationWatch?.();
|
|
319
|
+
stopHydrationWatch = void 0;
|
|
320
|
+
cleanup();
|
|
321
|
+
observer.value = null;
|
|
322
|
+
}
|
|
323
|
+
onScopeDispose(stop, true);
|
|
324
|
+
return {
|
|
325
|
+
isActive: shallowReadonly(isActive),
|
|
326
|
+
isPaused: shallowReadonly(isPaused),
|
|
327
|
+
pause,
|
|
328
|
+
resume,
|
|
329
|
+
stop
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/composables/useResizeObserver/index.ts
|
|
335
|
+
/**
|
|
336
|
+
* @module useResizeObserver
|
|
337
|
+
*
|
|
338
|
+
* @remarks
|
|
339
|
+
* ResizeObserver composable with lifecycle management.
|
|
340
|
+
*
|
|
341
|
+
* Key features:
|
|
342
|
+
* - ResizeObserver API wrapper
|
|
343
|
+
* - Pause/resume/stop functionality
|
|
344
|
+
* - Automatic cleanup on unmount
|
|
345
|
+
* - SSR-safe (checks SUPPORTS_OBSERVER)
|
|
346
|
+
* - Hydration-aware
|
|
347
|
+
* - Box model options (content-box/border-box)
|
|
348
|
+
*
|
|
349
|
+
* Perfect for responsive components and size-based rendering.
|
|
350
|
+
*/
|
|
351
|
+
/**
|
|
352
|
+
* A composable that uses the Resize Observer API to detect when an element's
|
|
353
|
+
* size changes.
|
|
354
|
+
*
|
|
355
|
+
* @param target The element to observe.
|
|
356
|
+
* @param callback The callback to execute when the element's size changes.
|
|
357
|
+
* @param options The options for the Resize Observer.
|
|
358
|
+
* @returns An object with methods to control the observer.
|
|
359
|
+
*
|
|
360
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
|
|
361
|
+
* @see https://0.vuetifyjs.com/composables/system/use-resize-observer
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* import { ref } from 'vue'
|
|
366
|
+
* import { useResizeObserver } from '@vuetify/v0'
|
|
367
|
+
*
|
|
368
|
+
* const el = ref<HTMLElement>()
|
|
369
|
+
* const width = ref(0)
|
|
370
|
+
* const height = ref(0)
|
|
371
|
+
*
|
|
372
|
+
* const { pause, resume, isPaused } = useResizeObserver(
|
|
373
|
+
* el,
|
|
374
|
+
* (entries) => {
|
|
375
|
+
* const entry = entries[0]
|
|
376
|
+
* if (entry) {
|
|
377
|
+
* width.value = entry.contentRect.width
|
|
378
|
+
* height.value = entry.contentRect.height
|
|
379
|
+
* console.log('Size changed:', width.value, 'x', height.value)
|
|
380
|
+
* }
|
|
381
|
+
* },
|
|
382
|
+
* { immediate: true }
|
|
383
|
+
* )
|
|
384
|
+
*
|
|
385
|
+
* // Pause observation
|
|
386
|
+
* pause()
|
|
387
|
+
*
|
|
388
|
+
* // Resume observation
|
|
389
|
+
* resume()
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
function useResizeObserver(target, callback, options = {}) {
|
|
393
|
+
return createObserver(target, callback, {
|
|
394
|
+
supports: SUPPORTS_OBSERVER,
|
|
395
|
+
once: options.once,
|
|
396
|
+
create: (cb) => new ResizeObserver((entries) => {
|
|
397
|
+
cb(entries.map((e) => ({
|
|
398
|
+
contentRect: {
|
|
399
|
+
width: e.contentRect.width,
|
|
400
|
+
height: e.contentRect.height,
|
|
401
|
+
top: e.contentRect.top,
|
|
402
|
+
left: e.contentRect.left
|
|
403
|
+
},
|
|
404
|
+
target: e.target
|
|
405
|
+
})));
|
|
406
|
+
}),
|
|
407
|
+
observe: (obs, el) => obs.observe(el, { box: options.box ?? "content-box" }),
|
|
408
|
+
immediate: options.immediate ? (el) => {
|
|
409
|
+
const rect = el.getBoundingClientRect();
|
|
410
|
+
return [{
|
|
411
|
+
contentRect: {
|
|
412
|
+
width: rect.width,
|
|
413
|
+
height: rect.height,
|
|
414
|
+
top: rect.top,
|
|
415
|
+
left: rect.left
|
|
416
|
+
},
|
|
417
|
+
target: el
|
|
418
|
+
}];
|
|
419
|
+
} : void 0
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A convenience composable that uses the Resize Observer API to track an
|
|
424
|
+
* element's size.
|
|
425
|
+
*
|
|
426
|
+
* @param target The element to observe.
|
|
427
|
+
* @returns An object with the element's width and height.
|
|
428
|
+
*
|
|
429
|
+
* @see https://0.vuetifyjs.com/composables/system/use-resize-observer#use-element-size
|
|
430
|
+
*
|
|
431
|
+
* @example
|
|
432
|
+
* ```ts
|
|
433
|
+
* import { ref, watchEffect } from 'vue'
|
|
434
|
+
* import { useElementSize } from '@vuetify/v0'
|
|
435
|
+
*
|
|
436
|
+
* const box = ref<HTMLElement>()
|
|
437
|
+
* const { width, height } = useElementSize(box)
|
|
438
|
+
*
|
|
439
|
+
* // Width and height are reactive refs
|
|
440
|
+
* watchEffect(() => {
|
|
441
|
+
* console.log('Box size:', width.value, 'x', height.value)
|
|
442
|
+
* })
|
|
443
|
+
* ```
|
|
444
|
+
*/
|
|
445
|
+
function useElementSize(target) {
|
|
446
|
+
const width = shallowRef(0);
|
|
447
|
+
const height = shallowRef(0);
|
|
448
|
+
const { pause: _pause, resume, stop, isActive, isPaused } = useResizeObserver(target, (entries) => {
|
|
449
|
+
const entry = entries[0];
|
|
450
|
+
if (entry) {
|
|
451
|
+
width.value = entry.contentRect.width;
|
|
452
|
+
height.value = entry.contentRect.height;
|
|
453
|
+
}
|
|
454
|
+
}, { immediate: true });
|
|
455
|
+
function pause() {
|
|
456
|
+
width.value = 0;
|
|
457
|
+
height.value = 0;
|
|
458
|
+
_pause();
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
width,
|
|
462
|
+
height,
|
|
463
|
+
isActive,
|
|
464
|
+
isPaused: shallowReadonly(isPaused),
|
|
465
|
+
pause,
|
|
466
|
+
resume,
|
|
467
|
+
stop
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/composables/createOverflow/index.ts
|
|
473
|
+
/**
|
|
474
|
+
* @module useOverflow
|
|
475
|
+
*
|
|
476
|
+
* @remarks
|
|
477
|
+
* Composable for computing how many items fit in a container based on available width.
|
|
478
|
+
* Enables responsive truncation logic for Pagination, Breadcrumbs, and similar components.
|
|
479
|
+
*
|
|
480
|
+
* Key features:
|
|
481
|
+
* - Container width tracking via ResizeObserver
|
|
482
|
+
* - Two modes: variable-width (per-item) or uniform-width (sample-based)
|
|
483
|
+
* - Computes capacity (how many items fit)
|
|
484
|
+
* - SSR-safe with Infinity fallback
|
|
485
|
+
* - Supports reserved space for nav buttons, ellipsis, etc.
|
|
486
|
+
*
|
|
487
|
+
* Use variable mode (default) for items with different widths like Breadcrumbs.
|
|
488
|
+
* Use uniform mode (itemWidth option) for same-width items like Pagination buttons.
|
|
489
|
+
*/
|
|
490
|
+
/**
|
|
491
|
+
* Creates a new overflow context for computing how many items fit in a container.
|
|
492
|
+
*
|
|
493
|
+
* @param options Configuration options
|
|
494
|
+
* @returns Overflow context with container ref, capacity, and measurement functions
|
|
495
|
+
*
|
|
496
|
+
* @example Variable-width mode (Breadcrumbs)
|
|
497
|
+
* ```vue
|
|
498
|
+
* <script lang="ts" setup>
|
|
499
|
+
* import { useTemplateRef } from 'vue'
|
|
500
|
+
* import { createOverflow } from '@vuetify/v0'
|
|
501
|
+
*
|
|
502
|
+
* const containerRef = useTemplateRef('container')
|
|
503
|
+
* const overflow = createOverflow({
|
|
504
|
+
* container: containerRef,
|
|
505
|
+
* gap: 8,
|
|
506
|
+
* reserved: 40,
|
|
507
|
+
* })
|
|
508
|
+
* <\/script>
|
|
509
|
+
*
|
|
510
|
+
* <template>
|
|
511
|
+
* <div ref="container">
|
|
512
|
+
* <span
|
|
513
|
+
* v-for="(item, i) in items.slice(0, overflow.capacity.value)"
|
|
514
|
+
* :key="i"
|
|
515
|
+
* :ref="el => overflow.measure(i, el)"
|
|
516
|
+
* >
|
|
517
|
+
* {{ item }}
|
|
518
|
+
* </span>
|
|
519
|
+
* <span v-if="overflow.isOverflowing.value">...</span>
|
|
520
|
+
* </div>
|
|
521
|
+
* </template>
|
|
522
|
+
* ```
|
|
523
|
+
*
|
|
524
|
+
* @example Uniform-width mode (Pagination)
|
|
525
|
+
* ```ts
|
|
526
|
+
* const overflow = createOverflow({
|
|
527
|
+
* container: () => atom.value?.element,
|
|
528
|
+
* itemWidth: buttonWidth,
|
|
529
|
+
* reserved: () => buttonWidth.value * 4,
|
|
530
|
+
* })
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
function createOverflow(options = {}) {
|
|
534
|
+
const { container: _container, gap = 0, reserved = 0, itemWidth, reverse } = options;
|
|
535
|
+
const container = /* @__PURE__ */ isUndefined(_container) ? shallowRef() : toRef(_container);
|
|
536
|
+
const widths = shallowRef(/* @__PURE__ */ new Map());
|
|
537
|
+
const { width } = useElementSize(container);
|
|
538
|
+
function measure(index, el) {
|
|
539
|
+
if (!el) {
|
|
540
|
+
if (widths.value.has(index)) {
|
|
541
|
+
const next = new Map(widths.value);
|
|
542
|
+
next.delete(index);
|
|
543
|
+
widths.value = next;
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
/* v8 ignore start -- browser-only measurement code */
|
|
548
|
+
if (!IN_BROWSER) return;
|
|
549
|
+
const style = getComputedStyle(el);
|
|
550
|
+
const marginX = Number.parseFloat(style.marginLeft) + Number.parseFloat(style.marginRight);
|
|
551
|
+
const w = el.offsetWidth + marginX;
|
|
552
|
+
if (widths.value.get(index) !== w) widths.value = new Map(widths.value).set(index, w);
|
|
553
|
+
/* v8 ignore stop */
|
|
554
|
+
}
|
|
555
|
+
function reset() {
|
|
556
|
+
widths.value = /* @__PURE__ */ new Map();
|
|
557
|
+
}
|
|
558
|
+
const total = computed(() => {
|
|
559
|
+
const g = toValue(gap);
|
|
560
|
+
let sum = 0;
|
|
561
|
+
let count = 0;
|
|
562
|
+
for (const w of widths.value.values()) {
|
|
563
|
+
sum += w + (count > 0 ? g : 0);
|
|
564
|
+
count++;
|
|
565
|
+
}
|
|
566
|
+
return sum;
|
|
567
|
+
});
|
|
568
|
+
return {
|
|
569
|
+
container,
|
|
570
|
+
width,
|
|
571
|
+
capacity: computed(() => {
|
|
572
|
+
const available = width.value - toValue(reserved);
|
|
573
|
+
if (width.value === 0) return Infinity;
|
|
574
|
+
if (available <= 0) return 0;
|
|
575
|
+
const g = toValue(gap);
|
|
576
|
+
const uniformWidth = toValue(itemWidth) ?? 0;
|
|
577
|
+
if (itemWidth !== void 0 && uniformWidth <= 0) return Infinity;
|
|
578
|
+
if (uniformWidth > 0) {
|
|
579
|
+
const subsequent = uniformWidth + g;
|
|
580
|
+
if (available < uniformWidth) return 0;
|
|
581
|
+
return Math.max(1, Math.floor((available - uniformWidth) / subsequent) + 1);
|
|
582
|
+
}
|
|
583
|
+
const entries = [...widths.value.entries()].toSorted((a, b) => a[0] - b[0]);
|
|
584
|
+
if (toValue(reverse)) entries.reverse();
|
|
585
|
+
let sum = 0;
|
|
586
|
+
let count = 0;
|
|
587
|
+
for (const [, w] of entries) {
|
|
588
|
+
const next = sum + w + (count > 0 ? g : 0);
|
|
589
|
+
if (next > available) break;
|
|
590
|
+
sum = next;
|
|
591
|
+
count++;
|
|
592
|
+
}
|
|
593
|
+
return count;
|
|
594
|
+
}),
|
|
595
|
+
total,
|
|
596
|
+
isOverflowing: toRef(() => {
|
|
597
|
+
return total.value > width.value - toValue(reserved);
|
|
598
|
+
}),
|
|
599
|
+
measure,
|
|
600
|
+
reset
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Creates an overflow context with dependency injection support.
|
|
605
|
+
*
|
|
606
|
+
* @param options Configuration options including namespace
|
|
607
|
+
* @template E The type of the overflow context
|
|
608
|
+
* @returns Trinity tuple: [useOverflow, provideOverflow, defaultOverflow]
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```ts
|
|
612
|
+
* // Create injectable context
|
|
613
|
+
* const [useOverflow, provideOverflow, overflow] = createOverflowContext({
|
|
614
|
+
* namespace: 'my-overflow',
|
|
615
|
+
* gap: 8,
|
|
616
|
+
* reserved: 160,
|
|
617
|
+
* })
|
|
618
|
+
*
|
|
619
|
+
* // In parent component
|
|
620
|
+
* provideOverflow()
|
|
621
|
+
*
|
|
622
|
+
* // In child component
|
|
623
|
+
* const overflow = useOverflow()
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
626
|
+
function createOverflowContext(_options = {}) {
|
|
627
|
+
const { namespace = "v0:overflow", ...options } = _options;
|
|
628
|
+
const [useOverflowContext, _provideOverflowContext] = createContext(namespace);
|
|
629
|
+
const context = createOverflow(options);
|
|
630
|
+
function provideOverflowContext(_context = context, app) {
|
|
631
|
+
return _provideOverflowContext(_context, app);
|
|
632
|
+
}
|
|
633
|
+
return createTrinity(useOverflowContext, provideOverflowContext, context);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Returns the current overflow context from dependency injection.
|
|
637
|
+
*
|
|
638
|
+
* @param namespace The namespace for the overflow context. Defaults to `v0:overflow`.
|
|
639
|
+
* @template E The type of the overflow context
|
|
640
|
+
* @returns The current overflow context.
|
|
641
|
+
*
|
|
642
|
+
* @throws An error if the overflow context is not found and no default is provided.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```vue
|
|
646
|
+
* <script lang="ts" setup>
|
|
647
|
+
* import { useOverflow } from '@vuetify/v0'
|
|
648
|
+
*
|
|
649
|
+
* // Inject overflow context provided by parent
|
|
650
|
+
* const overflow = useOverflow()
|
|
651
|
+
* <\/script>
|
|
652
|
+
*
|
|
653
|
+
* <template>
|
|
654
|
+
* <div>
|
|
655
|
+
* <p>Capacity: {{ overflow.capacity.value }}</p>
|
|
656
|
+
* </div>
|
|
657
|
+
* </template>
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
function useOverflow(namespace = "v0:overflow") {
|
|
661
|
+
return useContext(namespace);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/composables/useProxyModel/index.ts
|
|
666
|
+
/**
|
|
667
|
+
* @module useProxyModel
|
|
668
|
+
*
|
|
669
|
+
* @remarks
|
|
670
|
+
* Proxy composable for bidirectional sync between selection registry and v-model.
|
|
671
|
+
*
|
|
672
|
+
* Key features:
|
|
673
|
+
* - Bidirectional synchronization
|
|
674
|
+
* - Array and single-value modes
|
|
675
|
+
* - Automatic cleanup on scope disposal
|
|
676
|
+
* - Perfect for form controls with selection backing
|
|
677
|
+
*
|
|
678
|
+
* Bridges the gap between selection composables and Vue's v-model.
|
|
679
|
+
*/
|
|
680
|
+
/**
|
|
681
|
+
* Syncs a ref with a selection registry bidirectionally.
|
|
682
|
+
*
|
|
683
|
+
* @param registry The selection registry to bind to.
|
|
684
|
+
* @param model The ref to sync.
|
|
685
|
+
* @param options The options for the proxy model.
|
|
686
|
+
* @template Z The type of the selection ticket.
|
|
687
|
+
* @returns A function to stop the sync.
|
|
688
|
+
*
|
|
689
|
+
* @see https://0.vuetifyjs.com/composables/forms/use-proxy-model
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* import { createSelection, useProxyModel } from '@vuetify/v0'
|
|
694
|
+
*
|
|
695
|
+
* const model = ref()
|
|
696
|
+
* const registry = createSelection({ events: true })
|
|
697
|
+
* registry.onboard([
|
|
698
|
+
* { id: 'item-1', value: 'Item 1' },
|
|
699
|
+
* { id: 'item-2', value: 'Item 2' },
|
|
700
|
+
* ])
|
|
701
|
+
*
|
|
702
|
+
* const stop = useProxyModel(registry, model)
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
function useProxyModel(registry, model, options) {
|
|
706
|
+
const multiple = options?.multiple ?? false;
|
|
707
|
+
const _transformIn = options?.transformIn;
|
|
708
|
+
const _transformOut = options?.transformOut;
|
|
709
|
+
function transformIn(val) {
|
|
710
|
+
const value = toValue(val);
|
|
711
|
+
return toArray(/* @__PURE__ */ isFunction(_transformIn) ? _transformIn(value) : value);
|
|
712
|
+
}
|
|
713
|
+
function transformOut(val) {
|
|
714
|
+
if (/* @__PURE__ */ isFunction(_transformOut)) return _transformOut(val);
|
|
715
|
+
return toValue(multiple) ? val : val[0];
|
|
716
|
+
}
|
|
717
|
+
const modelAsArray = transformIn(model);
|
|
718
|
+
const pending = new Set(modelAsArray);
|
|
719
|
+
for (const value of modelAsArray) {
|
|
720
|
+
const ids = registry.browse(value);
|
|
721
|
+
/* v8 ignore start -- edge case: value exists in registry at init */
|
|
722
|
+
if (ids) {
|
|
723
|
+
for (const id of ids) registry.select(id);
|
|
724
|
+
pending.delete(value);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const registryWatch = watch(registry.selectedValues, (val) => {
|
|
728
|
+
modelWatch.pause();
|
|
729
|
+
model.value = transformOut(Array.from(toValue(val)));
|
|
730
|
+
modelWatch.resume();
|
|
731
|
+
}, { flush: "sync" });
|
|
732
|
+
const modelWatch = watch(model, (val) => {
|
|
733
|
+
registryWatch.pause();
|
|
734
|
+
const currentIds = new Set(toValue(registry.selectedIds));
|
|
735
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
736
|
+
for (const value of transformIn(val)) {
|
|
737
|
+
const ids = registry.browse(toRaw(value));
|
|
738
|
+
if (ids) for (const id of ids) targetIds.add(id);
|
|
739
|
+
}
|
|
740
|
+
if (toValue(multiple)) {
|
|
741
|
+
for (const id of currentIds.difference(targetIds)) registry.selectedIds.delete(id);
|
|
742
|
+
for (const id of targetIds.difference(currentIds)) registry.selectedIds.add(id);
|
|
743
|
+
} else {
|
|
744
|
+
const next = targetIds.values().next().value;
|
|
745
|
+
const last = currentIds.values().next().value;
|
|
746
|
+
if (!/* @__PURE__ */ isUndefined(last)) registry.unselect(last);
|
|
747
|
+
if (!/* @__PURE__ */ isUndefined(next)) registry.select(next);
|
|
748
|
+
}
|
|
749
|
+
registryWatch.resume();
|
|
750
|
+
}, {
|
|
751
|
+
flush: "sync",
|
|
752
|
+
deep: toValue(multiple)
|
|
753
|
+
});
|
|
754
|
+
function onRegister(data) {
|
|
755
|
+
const ticket = data;
|
|
756
|
+
if (!pending.has(ticket.value) || toValue(ticket.disabled)) return;
|
|
757
|
+
registryWatch.pause();
|
|
758
|
+
modelWatch.pause();
|
|
759
|
+
registry.select(ticket.id);
|
|
760
|
+
pending.delete(ticket.value);
|
|
761
|
+
modelWatch.resume();
|
|
762
|
+
registryWatch.resume();
|
|
763
|
+
}
|
|
764
|
+
registry.on("register:ticket", onRegister);
|
|
765
|
+
function stop() {
|
|
766
|
+
registryWatch();
|
|
767
|
+
modelWatch();
|
|
768
|
+
registry.off("register:ticket", onRegister);
|
|
769
|
+
}
|
|
770
|
+
onScopeDispose(stop, true);
|
|
771
|
+
return stop;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/composables/toReactive/index.ts
|
|
776
|
+
/**
|
|
777
|
+
* @module toReactive
|
|
778
|
+
*
|
|
779
|
+
* @remarks
|
|
780
|
+
* Utility function to convert values and refs into reactive proxies with ref unwrapping.
|
|
781
|
+
*
|
|
782
|
+
* Key features:
|
|
783
|
+
* - Automatic ref unwrapping
|
|
784
|
+
* - Deep reactive proxying
|
|
785
|
+
* - Map and Set support with ref unwrapping
|
|
786
|
+
* - Nested object/array reactivity
|
|
787
|
+
* - Type preservation
|
|
788
|
+
*
|
|
789
|
+
* Perfect for creating reactive versions of plain objects while automatically unwrapping refs.
|
|
790
|
+
*/
|
|
791
|
+
/**
|
|
792
|
+
* Converts a `MaybeRef` to a `UnwrapNestedRefs`.
|
|
793
|
+
*
|
|
794
|
+
* @param objectRef The object to convert.
|
|
795
|
+
* @template Z The type of the object.
|
|
796
|
+
* @returns The converted object.
|
|
797
|
+
*
|
|
798
|
+
* @see https://0.vuetifyjs.com/composables/transformers/to-reactive
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* import { ref } from 'vue'
|
|
803
|
+
* import { toReactive } from '@vuetify/v0'
|
|
804
|
+
*
|
|
805
|
+
* const state = ref({ name: 'John', age: 30 })
|
|
806
|
+
* const rstate = toReactive(state)
|
|
807
|
+
*
|
|
808
|
+
* console.log(rstate.name) // John
|
|
809
|
+
* ```
|
|
810
|
+
*/
|
|
811
|
+
function toReactive(objectRef) {
|
|
812
|
+
if (!isRef(objectRef)) return reactive(objectRef);
|
|
813
|
+
const target = objectRef.value;
|
|
814
|
+
if (target instanceof Map) {
|
|
815
|
+
const mapProxy = new Proxy(/* @__PURE__ */ new Map(), { get(_, p) {
|
|
816
|
+
const map = objectRef.value;
|
|
817
|
+
if (p === "get") return (key) => unref(map.get(key));
|
|
818
|
+
if (p === "set") return (key, value) => {
|
|
819
|
+
const existingValue = map.get(key);
|
|
820
|
+
if (isRef(existingValue)) existingValue.value = unref(value);
|
|
821
|
+
else map.set(key, value);
|
|
822
|
+
return mapProxy;
|
|
823
|
+
};
|
|
824
|
+
if (p === "has") return (key) => map.has(key);
|
|
825
|
+
if (p === "delete") return (key) => map.delete(key);
|
|
826
|
+
if (p === "clear") return () => map.clear();
|
|
827
|
+
if (p === "size") return map.size;
|
|
828
|
+
if (p === "keys") return () => map.keys();
|
|
829
|
+
if (p === "values") return function* () {
|
|
830
|
+
for (const value of map.values()) yield unref(value);
|
|
831
|
+
};
|
|
832
|
+
if (p === "entries") return function* () {
|
|
833
|
+
for (const [key, value] of map.entries()) yield [key, unref(value)];
|
|
834
|
+
};
|
|
835
|
+
if (p === "forEach") return (callback, thisArg) => {
|
|
836
|
+
for (const [key, value] of map.entries()) callback.call(thisArg, unref(value), key, mapProxy);
|
|
837
|
+
};
|
|
838
|
+
if (p === Symbol.iterator) return function* () {
|
|
839
|
+
for (const [key, value] of map.entries()) yield [key, unref(value)];
|
|
840
|
+
};
|
|
841
|
+
return Reflect.get(map, p);
|
|
842
|
+
} });
|
|
843
|
+
return reactive(mapProxy);
|
|
844
|
+
}
|
|
845
|
+
if (target instanceof Set) {
|
|
846
|
+
const setProxy = new Proxy(/* @__PURE__ */ new Set(), { get(_, p) {
|
|
847
|
+
const set = objectRef.value;
|
|
848
|
+
if (p === "add") return (value) => {
|
|
849
|
+
set.add(value);
|
|
850
|
+
return setProxy;
|
|
851
|
+
};
|
|
852
|
+
if (p === "has") return (value) => set.has(value);
|
|
853
|
+
if (p === "delete") return (value) => set.delete(value);
|
|
854
|
+
if (p === "clear") return () => set.clear();
|
|
855
|
+
if (p === "size") return set.size;
|
|
856
|
+
if (p === "keys" || p === "values") return function* () {
|
|
857
|
+
for (const value of set.values()) yield unref(value);
|
|
858
|
+
};
|
|
859
|
+
if (p === "entries") return function* () {
|
|
860
|
+
for (const value of set.values()) {
|
|
861
|
+
const unreffedValue = unref(value);
|
|
862
|
+
yield [unreffedValue, unreffedValue];
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
if (p === "forEach") return (callback, thisArg) => {
|
|
866
|
+
for (const value of set) {
|
|
867
|
+
const unreffedValue = unref(value);
|
|
868
|
+
callback.call(thisArg, unreffedValue, unreffedValue, setProxy);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
if (p === Symbol.iterator) return function* () {
|
|
872
|
+
for (const value of set.values()) yield unref(value);
|
|
873
|
+
};
|
|
874
|
+
return Reflect.get(set, p);
|
|
875
|
+
} });
|
|
876
|
+
return reactive(setProxy);
|
|
877
|
+
}
|
|
878
|
+
return reactive(new Proxy({}, {
|
|
879
|
+
get(_, p, receiver) {
|
|
880
|
+
return unref(Reflect.get(objectRef.value, p, receiver));
|
|
881
|
+
},
|
|
882
|
+
set(_, p, value) {
|
|
883
|
+
const currentTarget = objectRef.value;
|
|
884
|
+
currentTarget[p] = value;
|
|
885
|
+
return true;
|
|
886
|
+
},
|
|
887
|
+
deleteProperty(_, p) {
|
|
888
|
+
return Reflect.deleteProperty(objectRef.value, p);
|
|
889
|
+
},
|
|
890
|
+
has(_, p) {
|
|
891
|
+
return Reflect.has(objectRef.value, p);
|
|
892
|
+
},
|
|
893
|
+
ownKeys() {
|
|
894
|
+
return Object.keys(objectRef.value);
|
|
895
|
+
},
|
|
896
|
+
getOwnPropertyDescriptor(_, p) {
|
|
897
|
+
const desc = Reflect.getOwnPropertyDescriptor(objectRef.value, p);
|
|
898
|
+
if (!desc) return;
|
|
899
|
+
const newDesc = {
|
|
900
|
+
...desc,
|
|
901
|
+
configurable: true
|
|
902
|
+
};
|
|
903
|
+
if ("value" in newDesc) newDesc.value = unref(newDesc.value);
|
|
904
|
+
return newDesc;
|
|
905
|
+
}
|
|
906
|
+
}));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
//#endregion
|
|
910
|
+
//#region src/composables/useBreakpoints/index.ts
|
|
911
|
+
/**
|
|
912
|
+
* @module useBreakpoints
|
|
913
|
+
*
|
|
914
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
|
|
915
|
+
*
|
|
916
|
+
* @remarks
|
|
917
|
+
* Responsive breakpoint detection composable with window resize handling.
|
|
918
|
+
*
|
|
919
|
+
* Key features:
|
|
920
|
+
* - Window matchMedia integration
|
|
921
|
+
* - Six built-in breakpoints (xs, sm, md, lg, xl, xxl)
|
|
922
|
+
* - Automatic resize listener with cleanup
|
|
923
|
+
* - SSR-safe (checks IN_BROWSER)
|
|
924
|
+
* - Hydration-aware
|
|
925
|
+
* - Custom breakpoint configuration
|
|
926
|
+
*
|
|
927
|
+
* Perfect for responsive layouts and conditional rendering based on screen size.
|
|
928
|
+
*/
|
|
929
|
+
/**
|
|
930
|
+
* Creates default breakpoint configuration.
|
|
931
|
+
*
|
|
932
|
+
* @returns The default breakpoint configuration object.
|
|
933
|
+
*/
|
|
934
|
+
function createDefaultBreakpoints() {
|
|
935
|
+
return {
|
|
936
|
+
mobileBreakpoint: "lg",
|
|
937
|
+
breakpoints: {
|
|
938
|
+
xs: 0,
|
|
939
|
+
sm: 600,
|
|
940
|
+
md: 840,
|
|
941
|
+
lg: 1145,
|
|
942
|
+
xl: 1545,
|
|
943
|
+
xxl: 2138
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Creates a new breakpoints instance.
|
|
949
|
+
*
|
|
950
|
+
* @param options The options for the breakpoints instance.
|
|
951
|
+
* @template E The type of the breakpoints context.
|
|
952
|
+
* @returns A new breakpoints instance.
|
|
953
|
+
*
|
|
954
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
|
|
955
|
+
*
|
|
956
|
+
* @example
|
|
957
|
+
* ```ts
|
|
958
|
+
* import { createBreakpoints } from '@vuetify/v0'
|
|
959
|
+
*
|
|
960
|
+
* export const [useBreakpoints, provideBreakpoints] = createBreakpoints({
|
|
961
|
+
* namespace: 'v0:breakpoints',
|
|
962
|
+
* mobileBreakpoint: 'md',
|
|
963
|
+
* breakpoints: {
|
|
964
|
+
* xs: 0,
|
|
965
|
+
* sm: 600,
|
|
966
|
+
* md: 840,
|
|
967
|
+
* lg: 1145,
|
|
968
|
+
* xl: 1545,
|
|
969
|
+
* xxl: 2138,
|
|
970
|
+
* },
|
|
971
|
+
* })
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
function createBreakpoints(_options = {}) {
|
|
975
|
+
const { mobileBreakpoint, breakpoints } = /* @__PURE__ */ mergeDeep(createDefaultBreakpoints(), _options);
|
|
976
|
+
const sorted = Object.entries(breakpoints).toSorted((a, b) => a[1] - b[1]);
|
|
977
|
+
const names = sorted.map(([n]) => n);
|
|
978
|
+
const mb = /* @__PURE__ */ isNumber(mobileBreakpoint) ? mobileBreakpoint : breakpoints[mobileBreakpoint] ?? breakpoints.md;
|
|
979
|
+
const name = shallowRef("xs");
|
|
980
|
+
const width = shallowRef(0);
|
|
981
|
+
const height = shallowRef(0);
|
|
982
|
+
const isMobile = shallowRef(true);
|
|
983
|
+
const xs = shallowRef(true);
|
|
984
|
+
const sm = shallowRef(false);
|
|
985
|
+
const md = shallowRef(false);
|
|
986
|
+
const lg = shallowRef(false);
|
|
987
|
+
const xl = shallowRef(false);
|
|
988
|
+
const xxl = shallowRef(false);
|
|
989
|
+
const smAndUp = shallowRef(false);
|
|
990
|
+
const mdAndUp = shallowRef(false);
|
|
991
|
+
const lgAndUp = shallowRef(false);
|
|
992
|
+
const xlAndUp = shallowRef(false);
|
|
993
|
+
const xxlAndUp = shallowRef(false);
|
|
994
|
+
const smAndDown = shallowRef(true);
|
|
995
|
+
const mdAndDown = shallowRef(true);
|
|
996
|
+
const lgAndDown = shallowRef(true);
|
|
997
|
+
const xlAndDown = shallowRef(true);
|
|
998
|
+
const xxlAndDown = shallowRef(true);
|
|
999
|
+
function update() {
|
|
1000
|
+
if (!IN_BROWSER) return;
|
|
1001
|
+
width.value = window.innerWidth;
|
|
1002
|
+
height.value = window.innerHeight;
|
|
1003
|
+
let current = "xs";
|
|
1004
|
+
for (let i = sorted.length - 1; i >= 0; i--) if (width.value >= sorted[i][1]) {
|
|
1005
|
+
current = sorted[i][0];
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
name.value = current;
|
|
1009
|
+
const index = names.indexOf(current);
|
|
1010
|
+
isMobile.value = width.value < mb;
|
|
1011
|
+
xs.value = index === 0;
|
|
1012
|
+
sm.value = index === 1;
|
|
1013
|
+
md.value = index === 2;
|
|
1014
|
+
lg.value = index === 3;
|
|
1015
|
+
xl.value = index === 4;
|
|
1016
|
+
xxl.value = index === 5;
|
|
1017
|
+
smAndUp.value = index >= 1;
|
|
1018
|
+
mdAndUp.value = index >= 2;
|
|
1019
|
+
lgAndUp.value = index >= 3;
|
|
1020
|
+
xlAndUp.value = index >= 4;
|
|
1021
|
+
xxlAndUp.value = index >= 5;
|
|
1022
|
+
smAndDown.value = index <= 1;
|
|
1023
|
+
mdAndDown.value = index <= 2;
|
|
1024
|
+
lgAndDown.value = index <= 3;
|
|
1025
|
+
xlAndDown.value = index <= 4;
|
|
1026
|
+
xxlAndDown.value = index <= 5;
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
breakpoints,
|
|
1030
|
+
name: readonly(name),
|
|
1031
|
+
width: readonly(width),
|
|
1032
|
+
height: readonly(height),
|
|
1033
|
+
isMobile: readonly(isMobile),
|
|
1034
|
+
xs: readonly(xs),
|
|
1035
|
+
sm: readonly(sm),
|
|
1036
|
+
md: readonly(md),
|
|
1037
|
+
lg: readonly(lg),
|
|
1038
|
+
xl: readonly(xl),
|
|
1039
|
+
xxl: readonly(xxl),
|
|
1040
|
+
smAndUp: readonly(smAndUp),
|
|
1041
|
+
mdAndUp: readonly(mdAndUp),
|
|
1042
|
+
lgAndUp: readonly(lgAndUp),
|
|
1043
|
+
xlAndUp: readonly(xlAndUp),
|
|
1044
|
+
xxlAndUp: readonly(xxlAndUp),
|
|
1045
|
+
smAndDown: readonly(smAndDown),
|
|
1046
|
+
mdAndDown: readonly(mdAndDown),
|
|
1047
|
+
lgAndDown: readonly(lgAndDown),
|
|
1048
|
+
xlAndDown: readonly(xlAndDown),
|
|
1049
|
+
xxlAndDown: readonly(xxlAndDown),
|
|
1050
|
+
update
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function createBreakpointsFallback() {
|
|
1054
|
+
return {
|
|
1055
|
+
breakpoints: createDefaultBreakpoints().breakpoints,
|
|
1056
|
+
name: readonly(shallowRef("xs")),
|
|
1057
|
+
width: readonly(shallowRef(0)),
|
|
1058
|
+
height: readonly(shallowRef(0)),
|
|
1059
|
+
isMobile: readonly(shallowRef(false)),
|
|
1060
|
+
xs: readonly(shallowRef(false)),
|
|
1061
|
+
sm: readonly(shallowRef(false)),
|
|
1062
|
+
md: readonly(shallowRef(false)),
|
|
1063
|
+
lg: readonly(shallowRef(false)),
|
|
1064
|
+
xl: readonly(shallowRef(false)),
|
|
1065
|
+
xxl: readonly(shallowRef(false)),
|
|
1066
|
+
smAndUp: readonly(shallowRef(false)),
|
|
1067
|
+
mdAndUp: readonly(shallowRef(false)),
|
|
1068
|
+
lgAndUp: readonly(shallowRef(false)),
|
|
1069
|
+
xlAndUp: readonly(shallowRef(false)),
|
|
1070
|
+
xxlAndUp: readonly(shallowRef(false)),
|
|
1071
|
+
smAndDown: readonly(shallowRef(false)),
|
|
1072
|
+
mdAndDown: readonly(shallowRef(false)),
|
|
1073
|
+
lgAndDown: readonly(shallowRef(false)),
|
|
1074
|
+
xlAndDown: readonly(shallowRef(false)),
|
|
1075
|
+
xxlAndDown: readonly(shallowRef(false)),
|
|
1076
|
+
update: () => {}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const [createBreakpointsContext, createBreakpointsPlugin, useBreakpoints] = createPluginContext("v0:breakpoints", (options) => createBreakpoints(options), {
|
|
1080
|
+
fallback: () => createBreakpointsFallback(),
|
|
1081
|
+
setup: (context, app, _options) => {
|
|
1082
|
+
app.mixin({ mounted() {
|
|
1083
|
+
if (!/* @__PURE__ */ isNull(this.$parent)) return;
|
|
1084
|
+
const hydration = useHydration();
|
|
1085
|
+
function listener() {
|
|
1086
|
+
context.update();
|
|
1087
|
+
}
|
|
1088
|
+
const unwatch = watch(hydration.isHydrated, (hydrated) => {
|
|
1089
|
+
if (hydrated) listener();
|
|
1090
|
+
}, { immediate: true });
|
|
1091
|
+
const cleanup = useWindowEventListener("resize", listener, { passive: true });
|
|
1092
|
+
onScopeDispose(() => {
|
|
1093
|
+
cleanup();
|
|
1094
|
+
unwatch();
|
|
1095
|
+
}, true);
|
|
1096
|
+
} });
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
//#endregion
|
|
1101
|
+
//#region src/composables/useDate/index.ts
|
|
1102
|
+
/**
|
|
1103
|
+
* @module useDate
|
|
1104
|
+
*
|
|
1105
|
+
* @remarks
|
|
1106
|
+
* Date manipulation composable with adapter pattern for date operations.
|
|
1107
|
+
*
|
|
1108
|
+
* Key features:
|
|
1109
|
+
* - Adapter pattern for date library abstraction
|
|
1110
|
+
* - Locale-aware formatting via Intl.DateTimeFormat
|
|
1111
|
+
* - Integration with useLocale for automatic locale sync
|
|
1112
|
+
*
|
|
1113
|
+
* @example Using the built-in Temporal adapter
|
|
1114
|
+
* ```ts
|
|
1115
|
+
* import { Vuetify0DateAdapter } from '@vuetify/v0/date'
|
|
1116
|
+
* import { createDatePlugin } from '@vuetify/v0'
|
|
1117
|
+
*
|
|
1118
|
+
* const app = createApp(App)
|
|
1119
|
+
* app.use(createDatePlugin({ adapter: new Vuetify0DateAdapter() }))
|
|
1120
|
+
*
|
|
1121
|
+
* // In a component:
|
|
1122
|
+
* const { adapter } = useDate()
|
|
1123
|
+
* const today = adapter.date()
|
|
1124
|
+
* const formatted = adapter.format(today, 'fullDate')
|
|
1125
|
+
* ```
|
|
1126
|
+
*
|
|
1127
|
+
* @example Custom adapter
|
|
1128
|
+
* ```ts
|
|
1129
|
+
* import type { DateAdapter } from '@vuetify/v0'
|
|
1130
|
+
*
|
|
1131
|
+
* class DateFnsAdapter implements DateAdapter<Date> {
|
|
1132
|
+
* // ... implementation
|
|
1133
|
+
* }
|
|
1134
|
+
*
|
|
1135
|
+
* app.use(createDatePlugin({ adapter: new DateFnsAdapter() }))
|
|
1136
|
+
* ```
|
|
1137
|
+
*/
|
|
1138
|
+
/**
|
|
1139
|
+
* Default short locale codes mapped to full Intl locale strings.
|
|
1140
|
+
* Used when useLocale provides a short code without region (e.g., 'en' instead of 'en-US').
|
|
1141
|
+
*/
|
|
1142
|
+
const defaultLocales = {
|
|
1143
|
+
en: "en-US",
|
|
1144
|
+
es: "es-ES",
|
|
1145
|
+
fr: "fr-FR",
|
|
1146
|
+
de: "de-DE",
|
|
1147
|
+
it: "it-IT",
|
|
1148
|
+
pt: "pt-BR",
|
|
1149
|
+
ja: "ja-JP",
|
|
1150
|
+
ko: "ko-KR",
|
|
1151
|
+
zh: "zh-CN",
|
|
1152
|
+
ru: "ru-RU",
|
|
1153
|
+
ar: "ar-SA"
|
|
1154
|
+
};
|
|
1155
|
+
/**
|
|
1156
|
+
* Creates a new date context.
|
|
1157
|
+
*
|
|
1158
|
+
* @param options Adapter and locale configuration.
|
|
1159
|
+
* @template Z The date type used by the adapter.
|
|
1160
|
+
* @template E The date context type.
|
|
1161
|
+
* @returns A date context.
|
|
1162
|
+
*
|
|
1163
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
1164
|
+
*
|
|
1165
|
+
* @example
|
|
1166
|
+
* ```ts
|
|
1167
|
+
* import { Vuetify0DateAdapter } from '@vuetify/v0/date'
|
|
1168
|
+
*
|
|
1169
|
+
* const { adapter } = createDate({ adapter: new Vuetify0DateAdapter() })
|
|
1170
|
+
* const today = adapter.date()
|
|
1171
|
+
*
|
|
1172
|
+
* // With locale options
|
|
1173
|
+
* const { adapter } = createDate({
|
|
1174
|
+
* adapter: new Vuetify0DateAdapter(),
|
|
1175
|
+
* locale: 'de-DE',
|
|
1176
|
+
* })
|
|
1177
|
+
* ```
|
|
1178
|
+
*/
|
|
1179
|
+
function createDate(options) {
|
|
1180
|
+
const { locales = defaultLocales, adapter, locale: initialLocale } = options;
|
|
1181
|
+
let selectedId;
|
|
1182
|
+
try {
|
|
1183
|
+
if (instanceExists()) selectedId = useLocale().selectedId;
|
|
1184
|
+
} catch {}
|
|
1185
|
+
const locale = computed(() => {
|
|
1186
|
+
const selected = selectedId?.value;
|
|
1187
|
+
if (!/* @__PURE__ */ isNullOrUndefined(selected)) {
|
|
1188
|
+
const str = String(selected);
|
|
1189
|
+
return str.includes("-") ? str : locales[str] ?? str;
|
|
1190
|
+
}
|
|
1191
|
+
const fallback = initialLocale ?? adapter.locale ?? "en-US";
|
|
1192
|
+
return fallback.includes("-") ? fallback : locales[fallback] ?? fallback;
|
|
1193
|
+
});
|
|
1194
|
+
if (instanceExists()) onScopeDispose(watchEffect(() => {
|
|
1195
|
+
const loc = locale.value;
|
|
1196
|
+
if (loc && adapter.locale !== loc) adapter.locale = loc;
|
|
1197
|
+
}));
|
|
1198
|
+
else {
|
|
1199
|
+
const loc = locale.value;
|
|
1200
|
+
if (loc && adapter.locale !== loc) adapter.locale = loc;
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
adapter,
|
|
1204
|
+
locale
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Creates a new date context trinity.
|
|
1209
|
+
*
|
|
1210
|
+
* @param options Adapter, locale, and namespace configuration.
|
|
1211
|
+
* @template Z The date type used by the adapter.
|
|
1212
|
+
* @template E The date context type.
|
|
1213
|
+
* @returns A trinity [useContext, provideContext, defaultContext].
|
|
1214
|
+
*
|
|
1215
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
1216
|
+
*
|
|
1217
|
+
* @example
|
|
1218
|
+
* ```ts
|
|
1219
|
+
* import { Vuetify0DateAdapter } from '@vuetify/v0/date'
|
|
1220
|
+
*
|
|
1221
|
+
* const [useAppDate, provideAppDate] = createDateContext({
|
|
1222
|
+
* adapter: new Vuetify0DateAdapter(),
|
|
1223
|
+
* namespace: 'app:date',
|
|
1224
|
+
* })
|
|
1225
|
+
* ```
|
|
1226
|
+
*/
|
|
1227
|
+
function createDateContext(options) {
|
|
1228
|
+
const { namespace = "v0:date", ...dateOptions } = options;
|
|
1229
|
+
const [useDateContext, _provideDateContext] = createContext(namespace);
|
|
1230
|
+
const context = createDate(dateOptions);
|
|
1231
|
+
function provideDateContext(_context = context, app) {
|
|
1232
|
+
return _provideDateContext(_context, app);
|
|
1233
|
+
}
|
|
1234
|
+
return createTrinity(useDateContext, provideDateContext, context);
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Creates a new date plugin.
|
|
1238
|
+
*
|
|
1239
|
+
* @param options Adapter, locale, and namespace configuration.
|
|
1240
|
+
* @template Z The date type used by the adapter.
|
|
1241
|
+
* @template E The date context type.
|
|
1242
|
+
* @returns A Vue plugin.
|
|
1243
|
+
*
|
|
1244
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
1245
|
+
*
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```ts
|
|
1248
|
+
* import { Vuetify0DateAdapter } from '@vuetify/v0/date'
|
|
1249
|
+
*
|
|
1250
|
+
* const app = createApp(App)
|
|
1251
|
+
* app.use(createDatePlugin({ adapter: new Vuetify0DateAdapter() }))
|
|
1252
|
+
*
|
|
1253
|
+
* // With locale options
|
|
1254
|
+
* app.use(createDatePlugin({
|
|
1255
|
+
* adapter: new Vuetify0DateAdapter(),
|
|
1256
|
+
* locale: 'de-DE',
|
|
1257
|
+
* }))
|
|
1258
|
+
* ```
|
|
1259
|
+
*/
|
|
1260
|
+
function createDatePlugin(options) {
|
|
1261
|
+
const { namespace = "v0:date", ...dateOptions } = options;
|
|
1262
|
+
const [, provideDateContext, context] = createDateContext({
|
|
1263
|
+
namespace,
|
|
1264
|
+
...dateOptions
|
|
1265
|
+
});
|
|
1266
|
+
return createPlugin({
|
|
1267
|
+
namespace,
|
|
1268
|
+
provide: (app) => {
|
|
1269
|
+
provideDateContext(context, app);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Returns the current date context.
|
|
1275
|
+
*
|
|
1276
|
+
* Requires `createDatePlugin` to be installed with an adapter.
|
|
1277
|
+
*
|
|
1278
|
+
* @param namespace The namespace to look up (defaults to 'v0:date').
|
|
1279
|
+
* @template Z The date type used by the adapter.
|
|
1280
|
+
* @template E The date context type.
|
|
1281
|
+
* @returns The current date context.
|
|
1282
|
+
* @throws If called outside a component or without a date plugin installed.
|
|
1283
|
+
*
|
|
1284
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* ```ts
|
|
1288
|
+
* // main.ts
|
|
1289
|
+
* import { Vuetify0DateAdapter } from '@vuetify/v0/date'
|
|
1290
|
+
* import { createDatePlugin } from '@vuetify/v0'
|
|
1291
|
+
*
|
|
1292
|
+
* app.use(createDatePlugin({ adapter: new Vuetify0DateAdapter() }))
|
|
1293
|
+
* ```
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```vue
|
|
1297
|
+
* <script setup lang="ts">
|
|
1298
|
+
* import { useDate } from '@vuetify/v0'
|
|
1299
|
+
*
|
|
1300
|
+
* const { adapter, locale } = useDate()
|
|
1301
|
+
* const today = adapter.date()
|
|
1302
|
+
* <\/script>
|
|
1303
|
+
* ```
|
|
1304
|
+
*/
|
|
1305
|
+
function useDate(namespace = "v0:date") {
|
|
1306
|
+
if (!instanceExists()) throw new Error("[v0] useDate() must be called inside a Vue component with createDatePlugin installed.\n\nExample:\n import { Vuetify0DateAdapter } from '@vuetify/v0/date'\n import { createDatePlugin } from '@vuetify/v0'\n\n app.use(createDatePlugin({ adapter: new Vuetify0DateAdapter() }))");
|
|
1307
|
+
return useContext(namespace);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/composables/createForm/index.ts
|
|
1312
|
+
/**
|
|
1313
|
+
* @module createForm
|
|
1314
|
+
*
|
|
1315
|
+
* @remarks
|
|
1316
|
+
* Form validation composable with async rule support and multiple validation modes.
|
|
1317
|
+
*
|
|
1318
|
+
* Key features:
|
|
1319
|
+
* - Sync and async validation rules
|
|
1320
|
+
* - Multiple validation modes (submit, change, combined)
|
|
1321
|
+
* - Tri-state isValid (null/true/false)
|
|
1322
|
+
* - isPristine tracking
|
|
1323
|
+
* - Silent validation mode
|
|
1324
|
+
* - Form-level validation and reset
|
|
1325
|
+
*
|
|
1326
|
+
* Each field is registered with validation rules and tracks its own state independently.
|
|
1327
|
+
*/
|
|
1328
|
+
/**
|
|
1329
|
+
* Creates a new form instance.
|
|
1330
|
+
*
|
|
1331
|
+
* @param options The options for the form instance.
|
|
1332
|
+
* @template Z The type of the form ticket.
|
|
1333
|
+
* @template E The type of the form context.
|
|
1334
|
+
* @returns A new form instance.
|
|
1335
|
+
*
|
|
1336
|
+
* @see https://0.vuetifyjs.com/composables/forms/create-form
|
|
1337
|
+
*
|
|
1338
|
+
* @example
|
|
1339
|
+
* ```ts
|
|
1340
|
+
* import { createForm } from '@vuetify/v0'
|
|
1341
|
+
*
|
|
1342
|
+
* const form = createForm()
|
|
1343
|
+
*
|
|
1344
|
+
* const username = form.register({
|
|
1345
|
+
* id: 'username',
|
|
1346
|
+
* value: '',
|
|
1347
|
+
* rules: [(v) => v.length > 0 || 'Username is required'],
|
|
1348
|
+
* })
|
|
1349
|
+
*
|
|
1350
|
+
* await form.submit()
|
|
1351
|
+
*
|
|
1352
|
+
* console.log(username.errors.value) // ['Username is required']
|
|
1353
|
+
*
|
|
1354
|
+
* form.reset()
|
|
1355
|
+
* ```
|
|
1356
|
+
*/
|
|
1357
|
+
function createForm(options) {
|
|
1358
|
+
const registry = createRegistry(options);
|
|
1359
|
+
const validateOn = options?.validateOn || "submit";
|
|
1360
|
+
function parse(value) {
|
|
1361
|
+
return value.toLowerCase().split(/\s+/);
|
|
1362
|
+
}
|
|
1363
|
+
const isValidating = computed(() => {
|
|
1364
|
+
for (const ticket of registry.values()) if (ticket.isValidating.value) return true;
|
|
1365
|
+
return false;
|
|
1366
|
+
});
|
|
1367
|
+
const isValid = computed(() => {
|
|
1368
|
+
let hasFields = false;
|
|
1369
|
+
for (const ticket of registry.values()) {
|
|
1370
|
+
hasFields = true;
|
|
1371
|
+
if (ticket.isValid.value === false) return false;
|
|
1372
|
+
if (/* @__PURE__ */ isNull(ticket.isValid.value)) return null;
|
|
1373
|
+
}
|
|
1374
|
+
return hasFields ? true : null;
|
|
1375
|
+
});
|
|
1376
|
+
function reset() {
|
|
1377
|
+
for (const ticket of registry.values()) ticket.reset();
|
|
1378
|
+
}
|
|
1379
|
+
async function submit() {
|
|
1380
|
+
return validate([...registry.keys()]);
|
|
1381
|
+
}
|
|
1382
|
+
async function validate(id) {
|
|
1383
|
+
const validating = toArray(id);
|
|
1384
|
+
return (await Promise.all(validating.map(async (id$1) => await registry.get(id$1)?.validate() ?? true))).every(Boolean);
|
|
1385
|
+
}
|
|
1386
|
+
function register(registration) {
|
|
1387
|
+
const model = shallowRef(/* @__PURE__ */ isNullOrUndefined(registration.value) ? "" : toValue(registration.value));
|
|
1388
|
+
const rules = registration.rules || [];
|
|
1389
|
+
const errors = shallowRef([]);
|
|
1390
|
+
const isValidating$1 = shallowRef(false);
|
|
1391
|
+
const initialValue = model.value;
|
|
1392
|
+
const triggers = registration.validateOn || validateOn;
|
|
1393
|
+
const isPristine = shallowRef(true);
|
|
1394
|
+
const isValid$1 = shallowRef(null);
|
|
1395
|
+
function _validatesOn(event) {
|
|
1396
|
+
return parse(triggers).includes(event);
|
|
1397
|
+
}
|
|
1398
|
+
function _reset() {
|
|
1399
|
+
model.value = initialValue;
|
|
1400
|
+
errors.value = [];
|
|
1401
|
+
isPristine.value = true;
|
|
1402
|
+
isValid$1.value = null;
|
|
1403
|
+
isValidating$1.value = false;
|
|
1404
|
+
validationGeneration++;
|
|
1405
|
+
}
|
|
1406
|
+
let validationGeneration = 0;
|
|
1407
|
+
async function validate$1(silent = false) {
|
|
1408
|
+
if (rules.length === 0) return isValid$1.value = true;
|
|
1409
|
+
const generation = ++validationGeneration;
|
|
1410
|
+
isValidating$1.value = true;
|
|
1411
|
+
try {
|
|
1412
|
+
const results = await Promise.all(rules.map((rule) => rule(model.value)));
|
|
1413
|
+
if (generation !== validationGeneration) return isValid$1.value ?? false;
|
|
1414
|
+
const errorMessages = results.filter((result) => /* @__PURE__ */ isString(result));
|
|
1415
|
+
if (!silent) {
|
|
1416
|
+
errors.value = errorMessages;
|
|
1417
|
+
isValid$1.value = errorMessages.length === 0;
|
|
1418
|
+
isPristine.value = toValue(model) === initialValue;
|
|
1419
|
+
}
|
|
1420
|
+
return errorMessages.length === 0;
|
|
1421
|
+
} finally {
|
|
1422
|
+
if (generation === validationGeneration) isValidating$1.value = false;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const item = {
|
|
1426
|
+
...registration,
|
|
1427
|
+
rules,
|
|
1428
|
+
errors,
|
|
1429
|
+
disabled: registration.disabled || false,
|
|
1430
|
+
validateOn: triggers,
|
|
1431
|
+
isValidating: isValidating$1,
|
|
1432
|
+
isPristine,
|
|
1433
|
+
isValid: isValid$1,
|
|
1434
|
+
reset: _reset,
|
|
1435
|
+
validate: validate$1
|
|
1436
|
+
};
|
|
1437
|
+
const ticket = registry.register(item);
|
|
1438
|
+
Object.defineProperty(ticket, "value", {
|
|
1439
|
+
get() {
|
|
1440
|
+
return model.value;
|
|
1441
|
+
},
|
|
1442
|
+
set(val) {
|
|
1443
|
+
model.value = val;
|
|
1444
|
+
isPristine.value = val === initialValue;
|
|
1445
|
+
isValid$1.value = null;
|
|
1446
|
+
if (_validatesOn("change")) validate$1();
|
|
1447
|
+
},
|
|
1448
|
+
enumerable: true,
|
|
1449
|
+
configurable: true
|
|
1450
|
+
});
|
|
1451
|
+
return ticket;
|
|
1452
|
+
}
|
|
1453
|
+
function onboard(registrations) {
|
|
1454
|
+
return registry.batch(() => registrations.map((r) => register(r)));
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
...registry,
|
|
1458
|
+
register,
|
|
1459
|
+
onboard,
|
|
1460
|
+
reset,
|
|
1461
|
+
submit,
|
|
1462
|
+
validateOn,
|
|
1463
|
+
isValid,
|
|
1464
|
+
isValidating,
|
|
1465
|
+
get size() {
|
|
1466
|
+
return registry.size;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Creates a new form context.
|
|
1472
|
+
*
|
|
1473
|
+
* @param options The options for the form context.
|
|
1474
|
+
* @template Z The type of the form ticket.
|
|
1475
|
+
* @template E The type of the form context.
|
|
1476
|
+
* @returns A new form context.
|
|
1477
|
+
*
|
|
1478
|
+
* @see https://0.vuetifyjs.com/composables/forms/create-form
|
|
1479
|
+
*
|
|
1480
|
+
* @example
|
|
1481
|
+
* ```ts
|
|
1482
|
+
* import { createFormContext } from '@vuetify/v0'
|
|
1483
|
+
*
|
|
1484
|
+
* // With default namespace 'v0:form'
|
|
1485
|
+
* export const [useMyForm, provideMyForm, myForm] = createFormContext({ validateOn: 'change' })
|
|
1486
|
+
*
|
|
1487
|
+
* // Or with custom namespace
|
|
1488
|
+
* export const [useMyForm, provideMyForm, myForm] = createFormContext({
|
|
1489
|
+
* namespace: 'my-form',
|
|
1490
|
+
* validateOn: 'change',
|
|
1491
|
+
* })
|
|
1492
|
+
*
|
|
1493
|
+
* // In a parent component:
|
|
1494
|
+
* provideMyForm()
|
|
1495
|
+
*
|
|
1496
|
+
* // In a child component:
|
|
1497
|
+
* const form = useMyForm()
|
|
1498
|
+
* form.register({ id: 'field', value: ref(''), rules: [...] })
|
|
1499
|
+
* ```
|
|
1500
|
+
*/
|
|
1501
|
+
function createFormContext(_options = {}) {
|
|
1502
|
+
const { namespace = "v0:form", ...options } = _options;
|
|
1503
|
+
const [useFormContext, _provideFormContext] = createContext(namespace);
|
|
1504
|
+
const context = createForm(options);
|
|
1505
|
+
function provideFormContext(_context = context, app) {
|
|
1506
|
+
return _provideFormContext(_context, app);
|
|
1507
|
+
}
|
|
1508
|
+
return createTrinity(useFormContext, provideFormContext, context);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Returns the current form instance.
|
|
1512
|
+
*
|
|
1513
|
+
* @param namespace The namespace for the form context. Defaults to `'v0:form'`.
|
|
1514
|
+
* @returns The current form instance.
|
|
1515
|
+
*
|
|
1516
|
+
* @see https://0.vuetifyjs.com/composables/forms/create-form
|
|
1517
|
+
*
|
|
1518
|
+
* @example
|
|
1519
|
+
* ```vue
|
|
1520
|
+
* <script setup lang="ts">
|
|
1521
|
+
* import { useForm } from '@vuetify/v0'
|
|
1522
|
+
*
|
|
1523
|
+
* const form = useForm()
|
|
1524
|
+
* <\/script>
|
|
1525
|
+
*
|
|
1526
|
+
* <template>
|
|
1527
|
+
* <div>
|
|
1528
|
+
* <p>Form is {{ form.isValid.value ? 'valid' : 'invalid' }}</p>
|
|
1529
|
+
* </div>
|
|
1530
|
+
* </template>
|
|
1531
|
+
* ```
|
|
1532
|
+
*/
|
|
1533
|
+
function useForm(namespace = "v0:form") {
|
|
1534
|
+
return useContext(namespace);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
//#endregion
|
|
1538
|
+
//#region src/composables/useHotkey/aliases.ts
|
|
1539
|
+
/**
|
|
1540
|
+
* Key alias mapping for consistent key normalization across the hotkey system.
|
|
1541
|
+
*
|
|
1542
|
+
* Maps user-friendly aliases to canonical key names that match
|
|
1543
|
+
* KeyboardEvent.key values (in lowercase) where possible.
|
|
1544
|
+
*/
|
|
1545
|
+
const keyAliasMap = {
|
|
1546
|
+
control: "ctrl",
|
|
1547
|
+
command: "cmd",
|
|
1548
|
+
option: "alt",
|
|
1549
|
+
up: "arrowup",
|
|
1550
|
+
down: "arrowdown",
|
|
1551
|
+
left: "arrowleft",
|
|
1552
|
+
right: "arrowright",
|
|
1553
|
+
esc: "escape",
|
|
1554
|
+
spacebar: " ",
|
|
1555
|
+
space: " ",
|
|
1556
|
+
return: "enter",
|
|
1557
|
+
del: "delete",
|
|
1558
|
+
minus: "-",
|
|
1559
|
+
hyphen: "-"
|
|
1560
|
+
};
|
|
1561
|
+
function normalizeKey(key) {
|
|
1562
|
+
const lowerKey = key.toLowerCase();
|
|
1563
|
+
return keyAliasMap[lowerKey] ?? lowerKey;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/composables/useHotkey/parsing.ts
|
|
1568
|
+
const logger = useLogger();
|
|
1569
|
+
const MODIFIERS = [
|
|
1570
|
+
"ctrl",
|
|
1571
|
+
"shift",
|
|
1572
|
+
"alt",
|
|
1573
|
+
"meta",
|
|
1574
|
+
"cmd"
|
|
1575
|
+
];
|
|
1576
|
+
/**
|
|
1577
|
+
* Splits a single combination string into individual key parts.
|
|
1578
|
+
*
|
|
1579
|
+
* A combination is a set of keys that must be pressed simultaneously.
|
|
1580
|
+
* e.g. `ctrl+k`, `shift+-`
|
|
1581
|
+
*
|
|
1582
|
+
* @param combination - The combination string to parse
|
|
1583
|
+
* @param isInternal - Whether this is an internal call (suppresses warnings)
|
|
1584
|
+
* @returns Object with keys array and separators array
|
|
1585
|
+
*/
|
|
1586
|
+
function splitKeyCombination(combination, isInternal = false) {
|
|
1587
|
+
const emptyResult = {
|
|
1588
|
+
keys: [],
|
|
1589
|
+
separators: []
|
|
1590
|
+
};
|
|
1591
|
+
if (!combination) {
|
|
1592
|
+
if (!isInternal) logger.warn("Invalid hotkey combination: empty string provided");
|
|
1593
|
+
return emptyResult;
|
|
1594
|
+
}
|
|
1595
|
+
if (combination.length > 1 && [
|
|
1596
|
+
"+",
|
|
1597
|
+
"/",
|
|
1598
|
+
"_"
|
|
1599
|
+
].some((v) => combination.startsWith(v)) && ![
|
|
1600
|
+
"++",
|
|
1601
|
+
"//",
|
|
1602
|
+
"__"
|
|
1603
|
+
].some((v) => combination.startsWith(v)) || combination.includes("++") || combination.includes("__") || combination === "+" || combination === "_" || combination.length > 1 && (combination.endsWith("+") || combination.endsWith("_")) && combination.at(-2) !== combination.at(-1) || combination === "++" || combination === "--" || combination === "__") {
|
|
1604
|
+
if (!isInternal) logger.warn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
|
1605
|
+
return emptyResult;
|
|
1606
|
+
}
|
|
1607
|
+
const keys = [];
|
|
1608
|
+
const separators = [];
|
|
1609
|
+
let buffer = "";
|
|
1610
|
+
function flushBuffer(separator) {
|
|
1611
|
+
if (buffer) {
|
|
1612
|
+
if (separator) separators.push(separator);
|
|
1613
|
+
keys.push(normalizeKey(buffer));
|
|
1614
|
+
buffer = "";
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
for (let i = 0; i < combination.length; i++) {
|
|
1618
|
+
const char = combination[i];
|
|
1619
|
+
const nextChar = combination[i + 1];
|
|
1620
|
+
if ([
|
|
1621
|
+
"+",
|
|
1622
|
+
"/",
|
|
1623
|
+
"_",
|
|
1624
|
+
"-"
|
|
1625
|
+
].includes(char))
|
|
1626
|
+
/* v8 ignore start -- edge case: doubled special chars */
|
|
1627
|
+
if (char === nextChar) {
|
|
1628
|
+
flushBuffer(char);
|
|
1629
|
+
keys.push(char);
|
|
1630
|
+
i++;
|
|
1631
|
+
} else if ([
|
|
1632
|
+
"+",
|
|
1633
|
+
"/",
|
|
1634
|
+
"_"
|
|
1635
|
+
].includes(char)) flushBuffer(char);
|
|
1636
|
+
else buffer += char;
|
|
1637
|
+
else buffer += char;
|
|
1638
|
+
}
|
|
1639
|
+
flushBuffer();
|
|
1640
|
+
/* v8 ignore start -- edge case: invalid minus in combination */
|
|
1641
|
+
if (keys.some((key) => key.length > 1 && key.includes("-") && key !== "--")) {
|
|
1642
|
+
if (!isInternal) logger.warn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
|
1643
|
+
return emptyResult;
|
|
1644
|
+
}
|
|
1645
|
+
if (keys.length === 0 && combination) return {
|
|
1646
|
+
keys: [normalizeKey(combination)],
|
|
1647
|
+
separators
|
|
1648
|
+
};
|
|
1649
|
+
/* v8 ignore stop */
|
|
1650
|
+
return {
|
|
1651
|
+
keys,
|
|
1652
|
+
separators
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Splits a hotkey string into its constituent combination groups.
|
|
1657
|
+
*
|
|
1658
|
+
* A sequence is a series of combinations that must be pressed in order.
|
|
1659
|
+
* e.g. `a-b`, `ctrl+k-p`
|
|
1660
|
+
*
|
|
1661
|
+
* @param str - The sequence string to parse
|
|
1662
|
+
* @returns Array of combination strings
|
|
1663
|
+
*/
|
|
1664
|
+
function splitKeySequence(str) {
|
|
1665
|
+
if (!str) {
|
|
1666
|
+
logger.warn("Invalid hotkey sequence: empty string provided");
|
|
1667
|
+
return [];
|
|
1668
|
+
}
|
|
1669
|
+
const hasInvalidStart = str.startsWith("-") && !["---", "--+"].includes(str);
|
|
1670
|
+
const hasInvalidEnd = str.endsWith("-") && !str.endsWith("+-") && !str.endsWith("_-") && str !== "-" && str !== "---";
|
|
1671
|
+
if (hasInvalidStart || hasInvalidEnd) {
|
|
1672
|
+
logger.warn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
|
1673
|
+
return [];
|
|
1674
|
+
}
|
|
1675
|
+
const result = [];
|
|
1676
|
+
let buffer = "";
|
|
1677
|
+
let i = 0;
|
|
1678
|
+
while (i < str.length) {
|
|
1679
|
+
const char = str[i];
|
|
1680
|
+
if (char === "-") {
|
|
1681
|
+
const prevChar = str[i - 1] ?? "";
|
|
1682
|
+
const prevPrevChar = i > 1 ? str[i - 2] : void 0;
|
|
1683
|
+
if (["+", "_"].includes(prevChar) && !["+", "/"].includes(prevPrevChar ?? "")) {
|
|
1684
|
+
buffer += char;
|
|
1685
|
+
i++;
|
|
1686
|
+
} else {
|
|
1687
|
+
if (buffer) {
|
|
1688
|
+
result.push(buffer);
|
|
1689
|
+
buffer = "";
|
|
1690
|
+
} else result.push("-");
|
|
1691
|
+
i++;
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
buffer += char;
|
|
1695
|
+
i++;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (buffer) result.push(buffer);
|
|
1699
|
+
const collapsed = [];
|
|
1700
|
+
let minusCount = 0;
|
|
1701
|
+
for (const part of result)
|
|
1702
|
+
/* v8 ignore start -- edge case: minus collapsing */
|
|
1703
|
+
if (part === "-") {
|
|
1704
|
+
if (minusCount % 2 === 0) collapsed.push("-");
|
|
1705
|
+
minusCount++;
|
|
1706
|
+
} else {
|
|
1707
|
+
minusCount = 0;
|
|
1708
|
+
collapsed.push(part);
|
|
1709
|
+
}
|
|
1710
|
+
/* v8 ignore start -- edge case: invalid combination in sequence */
|
|
1711
|
+
if (!collapsed.every((s) => splitKeyCombination(s, true).keys.length > 0)) {
|
|
1712
|
+
logger.warn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
|
1713
|
+
return [];
|
|
1714
|
+
}
|
|
1715
|
+
/* v8 ignore stop */
|
|
1716
|
+
return collapsed;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/composables/useHotkey/index.ts
|
|
1721
|
+
/**
|
|
1722
|
+
* @module useHotkey
|
|
1723
|
+
*
|
|
1724
|
+
* @remarks
|
|
1725
|
+
* Hotkey listener composable with key combination and sequence support.
|
|
1726
|
+
*
|
|
1727
|
+
* Key features:
|
|
1728
|
+
* - Key combination support (`ctrl+k`, `shift+enter`)
|
|
1729
|
+
* - Key sequence support (`g-h` for GitHub-style shortcuts)
|
|
1730
|
+
* - Platform-aware modifiers (Mac: cmd→meta, others: cmd→ctrl)
|
|
1731
|
+
* - Input focus detection (skip when typing in inputs)
|
|
1732
|
+
* - Sequence timeout with automatic reset
|
|
1733
|
+
* - Key alias normalization
|
|
1734
|
+
* - Pause/resume/stop functionality
|
|
1735
|
+
* - Automatic cleanup on scope disposal
|
|
1736
|
+
* - SSR-safe (no-op when not in browser)
|
|
1737
|
+
*
|
|
1738
|
+
* Builds on useEventListener for lifecycle-managed keyboard events.
|
|
1739
|
+
*/
|
|
1740
|
+
const isMac = IN_BROWSER && /mac|iphone|ipad|ipod/i.test(navigator?.userAgent ?? "");
|
|
1741
|
+
/**
|
|
1742
|
+
* A composable that listens for hotkey combinations and sequences.
|
|
1743
|
+
*
|
|
1744
|
+
* @param keys - The hotkey string (e.g., 'ctrl+k', 'g-h')
|
|
1745
|
+
* @param callback - The function to call when the hotkey is triggered
|
|
1746
|
+
* @param options - Configuration options
|
|
1747
|
+
* @returns An object with state refs and control methods
|
|
1748
|
+
*
|
|
1749
|
+
* @see https://0.vuetifyjs.com/composables/system/use-hotkey
|
|
1750
|
+
*
|
|
1751
|
+
* @example
|
|
1752
|
+
* ```ts
|
|
1753
|
+
* import { useHotkey } from '@vuetify/v0'
|
|
1754
|
+
*
|
|
1755
|
+
* // Simple combination
|
|
1756
|
+
* const { isActive, pause, resume } = useHotkey('ctrl+k', () => {
|
|
1757
|
+
* console.log('Command palette opened')
|
|
1758
|
+
* })
|
|
1759
|
+
*
|
|
1760
|
+
* // Key sequence (GitHub-style)
|
|
1761
|
+
* useHotkey('g-h', () => console.log('Go home'))
|
|
1762
|
+
*
|
|
1763
|
+
* // With options
|
|
1764
|
+
* useHotkey('escape', closeModal, { inputs: true })
|
|
1765
|
+
*
|
|
1766
|
+
* // Pause/resume control
|
|
1767
|
+
* pause() // Temporarily disable
|
|
1768
|
+
* resume() // Re-enable
|
|
1769
|
+
* ```
|
|
1770
|
+
*/
|
|
1771
|
+
function useHotkey(keys, callback, options = {}, _platform) {
|
|
1772
|
+
const platformIsMac = _platform?.isMac ?? isMac;
|
|
1773
|
+
const { event = "keydown", inputs = false, preventDefault = true, stopPropagation = false, sequenceTimeout = 1e3 } = options;
|
|
1774
|
+
const isPaused = shallowRef(false);
|
|
1775
|
+
const cleanup = shallowRef(null);
|
|
1776
|
+
const isActive = toRef(() => !/* @__PURE__ */ isNull(cleanup.value));
|
|
1777
|
+
let sequenceTimer;
|
|
1778
|
+
let keyGroups = [];
|
|
1779
|
+
let isSequence = false;
|
|
1780
|
+
let groupIndex = 0;
|
|
1781
|
+
function isInputFocused() {
|
|
1782
|
+
if (toValue(inputs)) return false;
|
|
1783
|
+
const activeElement = document.activeElement;
|
|
1784
|
+
if (!activeElement) return false;
|
|
1785
|
+
const role = activeElement.getAttribute("role");
|
|
1786
|
+
if (role === "textbox" || role === "searchbox" || role === "combobox" || role === "spinbutton") return true;
|
|
1787
|
+
if (activeElement.tagName === "INPUT") {
|
|
1788
|
+
const type = activeElement.type?.toLowerCase();
|
|
1789
|
+
return [
|
|
1790
|
+
"text",
|
|
1791
|
+
"email",
|
|
1792
|
+
"password",
|
|
1793
|
+
"search",
|
|
1794
|
+
"tel",
|
|
1795
|
+
"url",
|
|
1796
|
+
"number",
|
|
1797
|
+
"date",
|
|
1798
|
+
"datetime-local",
|
|
1799
|
+
"time",
|
|
1800
|
+
"month",
|
|
1801
|
+
"week"
|
|
1802
|
+
].includes(type);
|
|
1803
|
+
}
|
|
1804
|
+
return activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable;
|
|
1805
|
+
}
|
|
1806
|
+
function resetSequence() {
|
|
1807
|
+
groupIndex = 0;
|
|
1808
|
+
clearTimeout(sequenceTimer);
|
|
1809
|
+
}
|
|
1810
|
+
function handler(e) {
|
|
1811
|
+
const group = keyGroups[groupIndex];
|
|
1812
|
+
if (!group || isInputFocused()) return;
|
|
1813
|
+
if (!matchesKeyGroup(e, group, platformIsMac)) {
|
|
1814
|
+
if (isSequence) resetSequence();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (toValue(stopPropagation)) e.stopPropagation();
|
|
1818
|
+
if (toValue(preventDefault)) e.preventDefault();
|
|
1819
|
+
if (!isSequence) {
|
|
1820
|
+
callback(e);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
clearTimeout(sequenceTimer);
|
|
1824
|
+
groupIndex++;
|
|
1825
|
+
if (groupIndex === keyGroups.length) {
|
|
1826
|
+
callback(e);
|
|
1827
|
+
resetSequence();
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
sequenceTimer = setTimeout(resetSequence, toValue(sequenceTimeout));
|
|
1831
|
+
}
|
|
1832
|
+
function setup() {
|
|
1833
|
+
if (!IN_BROWSER || isPaused.value) return;
|
|
1834
|
+
const currentKeys = toValue(keys);
|
|
1835
|
+
if (!currentKeys) return;
|
|
1836
|
+
const groups = splitKeySequence(currentKeys.toLowerCase());
|
|
1837
|
+
if (groups.length === 0) return;
|
|
1838
|
+
isSequence = groups.length > 1;
|
|
1839
|
+
keyGroups = groups;
|
|
1840
|
+
resetSequence();
|
|
1841
|
+
cleanup.value = useWindowEventListener(toValue(event), handler);
|
|
1842
|
+
}
|
|
1843
|
+
function dispose() {
|
|
1844
|
+
if (cleanup.value) {
|
|
1845
|
+
cleanup.value();
|
|
1846
|
+
cleanup.value = null;
|
|
1847
|
+
}
|
|
1848
|
+
clearTimeout(sequenceTimer);
|
|
1849
|
+
keyGroups = [];
|
|
1850
|
+
isSequence = false;
|
|
1851
|
+
groupIndex = 0;
|
|
1852
|
+
}
|
|
1853
|
+
function pause() {
|
|
1854
|
+
isPaused.value = true;
|
|
1855
|
+
dispose();
|
|
1856
|
+
}
|
|
1857
|
+
function resume() {
|
|
1858
|
+
isPaused.value = false;
|
|
1859
|
+
setup();
|
|
1860
|
+
}
|
|
1861
|
+
function stop() {
|
|
1862
|
+
dispose();
|
|
1863
|
+
}
|
|
1864
|
+
watch(() => toValue(keys), () => {
|
|
1865
|
+
if (isPaused.value) return;
|
|
1866
|
+
dispose();
|
|
1867
|
+
setup();
|
|
1868
|
+
}, { immediate: true });
|
|
1869
|
+
watch(() => toValue(event), () => {
|
|
1870
|
+
if (isPaused.value || !toValue(keys)) return;
|
|
1871
|
+
dispose();
|
|
1872
|
+
setup();
|
|
1873
|
+
});
|
|
1874
|
+
onScopeDispose(stop, true);
|
|
1875
|
+
return {
|
|
1876
|
+
isActive: shallowReadonly(isActive),
|
|
1877
|
+
isPaused: shallowReadonly(isPaused),
|
|
1878
|
+
pause,
|
|
1879
|
+
resume,
|
|
1880
|
+
stop
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function isModifier(key) {
|
|
1884
|
+
return MODIFIERS.includes(key);
|
|
1885
|
+
}
|
|
1886
|
+
function parseKeyGroup(group) {
|
|
1887
|
+
const { keys: parts } = splitKeyCombination(group.toLowerCase());
|
|
1888
|
+
const modifiers = Object.fromEntries(MODIFIERS.map((m) => [m, false]));
|
|
1889
|
+
if (parts.length === 0) return {
|
|
1890
|
+
modifiers,
|
|
1891
|
+
actualKey: void 0
|
|
1892
|
+
};
|
|
1893
|
+
let actualKey;
|
|
1894
|
+
for (const part of parts) if (isModifier(part)) modifiers[part] = true;
|
|
1895
|
+
else actualKey = part;
|
|
1896
|
+
return {
|
|
1897
|
+
modifiers,
|
|
1898
|
+
actualKey
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function matchesKeyGroup(e, group, platformIsMac) {
|
|
1902
|
+
const { modifiers, actualKey } = parseKeyGroup(group);
|
|
1903
|
+
const expectCtrl = modifiers.ctrl || !platformIsMac && (modifiers.cmd || modifiers.meta);
|
|
1904
|
+
const expectMeta = platformIsMac && (modifiers.cmd || modifiers.meta);
|
|
1905
|
+
const modifiersMatch = e.ctrlKey === expectCtrl && e.metaKey === expectMeta && e.shiftKey === modifiers.shift && e.altKey === modifiers.alt;
|
|
1906
|
+
if (/* @__PURE__ */ isUndefined(actualKey)) return modifiersMatch;
|
|
1907
|
+
return modifiersMatch && e.key.toLowerCase() === actualKey.toLowerCase();
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
//#endregion
|
|
1911
|
+
//#region src/composables/useIntersectionObserver/index.ts
|
|
1912
|
+
/**
|
|
1913
|
+
* @module useIntersectionObserver
|
|
1914
|
+
*
|
|
1915
|
+
* @remarks
|
|
1916
|
+
* IntersectionObserver composable with lifecycle management.
|
|
1917
|
+
*
|
|
1918
|
+
* Key features:
|
|
1919
|
+
* - IntersectionObserver API wrapper
|
|
1920
|
+
* - Pause/resume/stop functionality
|
|
1921
|
+
* - Automatic cleanup on unmount
|
|
1922
|
+
* - SSR-safe (checks SUPPORTS_INTERSECTION_OBSERVER)
|
|
1923
|
+
* - Hydration-aware
|
|
1924
|
+
* - Immediate callback option
|
|
1925
|
+
*
|
|
1926
|
+
* Perfect for lazy loading, infinite scroll, and visibility detection.
|
|
1927
|
+
*/
|
|
1928
|
+
/**
|
|
1929
|
+
* A composable that uses the Intersection Observer API to detect when an element
|
|
1930
|
+
* is visible in the viewport.
|
|
1931
|
+
*
|
|
1932
|
+
* @param target The element to observe.
|
|
1933
|
+
* @param callback The callback to execute when the element's intersection changes.
|
|
1934
|
+
* @param options The options for the Intersection Observer.
|
|
1935
|
+
* @returns An object with methods to control the observer.
|
|
1936
|
+
*
|
|
1937
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
|
|
1938
|
+
* @see https://0.vuetifyjs.com/composables/system/use-intersection-observer
|
|
1939
|
+
*
|
|
1940
|
+
* @example
|
|
1941
|
+
* ```ts
|
|
1942
|
+
* import { ref } from 'vue'
|
|
1943
|
+
* import { useIntersectionObserver } from '@vuetify/v0'
|
|
1944
|
+
*
|
|
1945
|
+
* const target = ref<HTMLElement>()
|
|
1946
|
+
* const isVisible = ref(false)
|
|
1947
|
+
*
|
|
1948
|
+
* const { isIntersecting, pause, resume } = useIntersectionObserver(
|
|
1949
|
+
* target,
|
|
1950
|
+
* (entries) => {
|
|
1951
|
+
* const entry = entries[0]
|
|
1952
|
+
* if (entry) {
|
|
1953
|
+
* isVisible.value = entry.isIntersecting
|
|
1954
|
+
* console.log('Element is visible:', entry.isIntersecting)
|
|
1955
|
+
* }
|
|
1956
|
+
* },
|
|
1957
|
+
* { threshold: 0.5 }
|
|
1958
|
+
* )
|
|
1959
|
+
*
|
|
1960
|
+
* // Pause observation
|
|
1961
|
+
* pause()
|
|
1962
|
+
*
|
|
1963
|
+
* // Resume observation
|
|
1964
|
+
* resume()
|
|
1965
|
+
* ```
|
|
1966
|
+
*/
|
|
1967
|
+
function useIntersectionObserver(target, callback, options = {}) {
|
|
1968
|
+
const isIntersecting = shallowRef(false);
|
|
1969
|
+
return {
|
|
1970
|
+
...createObserver(target, callback, {
|
|
1971
|
+
supports: SUPPORTS_INTERSECTION_OBSERVER,
|
|
1972
|
+
once: options.once,
|
|
1973
|
+
create: (cb) => new IntersectionObserver((entries) => {
|
|
1974
|
+
cb(entries.map((e) => ({
|
|
1975
|
+
boundingClientRect: e.boundingClientRect,
|
|
1976
|
+
intersectionRatio: e.intersectionRatio,
|
|
1977
|
+
intersectionRect: e.intersectionRect,
|
|
1978
|
+
isIntersecting: e.isIntersecting,
|
|
1979
|
+
rootBounds: e.rootBounds,
|
|
1980
|
+
target: e.target,
|
|
1981
|
+
time: e.time
|
|
1982
|
+
})));
|
|
1983
|
+
}, {
|
|
1984
|
+
root: options.root ?? null,
|
|
1985
|
+
rootMargin: options.rootMargin ?? "0px",
|
|
1986
|
+
threshold: options.threshold ?? 0
|
|
1987
|
+
}),
|
|
1988
|
+
observe: (obs, el) => obs.observe(el),
|
|
1989
|
+
onEntry: (entries) => {
|
|
1990
|
+
const latest = entries.at(-1);
|
|
1991
|
+
if (latest) isIntersecting.value = latest.isIntersecting;
|
|
1992
|
+
},
|
|
1993
|
+
onPause: () => {
|
|
1994
|
+
isIntersecting.value = false;
|
|
1995
|
+
},
|
|
1996
|
+
shouldStop: (entries) => entries.at(-1)?.isIntersecting ?? false,
|
|
1997
|
+
immediate: options.immediate ? (el) => {
|
|
1998
|
+
return [{
|
|
1999
|
+
boundingClientRect: el.getBoundingClientRect(),
|
|
2000
|
+
intersectionRatio: 0,
|
|
2001
|
+
intersectionRect: new DOMRect(0, 0, 0, 0),
|
|
2002
|
+
isIntersecting: false,
|
|
2003
|
+
rootBounds: null,
|
|
2004
|
+
target: el,
|
|
2005
|
+
time: performance.now()
|
|
2006
|
+
}];
|
|
2007
|
+
} : void 0
|
|
2008
|
+
}),
|
|
2009
|
+
isIntersecting: shallowReadonly(isIntersecting)
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* A convenience composable that uses the Intersection Observer API to detect
|
|
2014
|
+
* when an element is visible in the viewport.
|
|
2015
|
+
*
|
|
2016
|
+
* @param target The element to observe.
|
|
2017
|
+
* @param options The options for the Intersection Observer.
|
|
2018
|
+
* @returns An object with the intersection state.
|
|
2019
|
+
*
|
|
2020
|
+
* @see https://0.vuetifyjs.com/composables/system/use-intersection-observer
|
|
2021
|
+
*
|
|
2022
|
+
* @example
|
|
2023
|
+
* ```ts
|
|
2024
|
+
* import { ref } from 'vue'
|
|
2025
|
+
* import { useElementIntersection } from '@vuetify/v0'
|
|
2026
|
+
*
|
|
2027
|
+
* const myElement = ref<HTMLElement>()
|
|
2028
|
+
* const { isIntersecting, intersectionRatio } = useElementIntersection(myElement, {
|
|
2029
|
+
* threshold: 0.5
|
|
2030
|
+
* })
|
|
2031
|
+
*
|
|
2032
|
+
* // Use in template to conditionally render or animate
|
|
2033
|
+
* watchEffect(() => {
|
|
2034
|
+
* if (isIntersecting.value) {
|
|
2035
|
+
* console.log('Element is visible!', intersectionRatio.value)
|
|
2036
|
+
* }
|
|
2037
|
+
* })
|
|
2038
|
+
* ```
|
|
2039
|
+
*/
|
|
2040
|
+
function useElementIntersection(target, options = {}) {
|
|
2041
|
+
const isIntersecting = shallowRef(false);
|
|
2042
|
+
const intersectionRatio = shallowRef(0);
|
|
2043
|
+
const { pause: _pause, resume, stop, isActive, isPaused } = useIntersectionObserver(target, (entries) => {
|
|
2044
|
+
const entry = entries.at(-1);
|
|
2045
|
+
if (entry) {
|
|
2046
|
+
isIntersecting.value = entry.isIntersecting;
|
|
2047
|
+
intersectionRatio.value = entry.intersectionRatio;
|
|
2048
|
+
}
|
|
2049
|
+
}, {
|
|
2050
|
+
immediate: true,
|
|
2051
|
+
...options
|
|
2052
|
+
});
|
|
2053
|
+
function pause() {
|
|
2054
|
+
isIntersecting.value = false;
|
|
2055
|
+
intersectionRatio.value = 0;
|
|
2056
|
+
_pause();
|
|
2057
|
+
}
|
|
2058
|
+
return {
|
|
2059
|
+
isIntersecting: shallowReadonly(isIntersecting),
|
|
2060
|
+
intersectionRatio: shallowReadonly(intersectionRatio),
|
|
2061
|
+
isActive,
|
|
2062
|
+
isPaused,
|
|
2063
|
+
pause,
|
|
2064
|
+
resume,
|
|
2065
|
+
stop
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
//#endregion
|
|
2070
|
+
//#region src/composables/useLazy/index.ts
|
|
2071
|
+
/**
|
|
2072
|
+
* @module useLazy
|
|
2073
|
+
*
|
|
2074
|
+
* @see https://0.vuetifyjs.com/composables/system/use-lazy
|
|
2075
|
+
*
|
|
2076
|
+
* @remarks
|
|
2077
|
+
* Deferred content rendering composable for performance optimization.
|
|
2078
|
+
*
|
|
2079
|
+
* Key features:
|
|
2080
|
+
* - Content only renders after first activation (unless eager)
|
|
2081
|
+
* - Optional reset on deactivation for memory savings
|
|
2082
|
+
* - Readonly state refs for template binding
|
|
2083
|
+
* - Manual reset control
|
|
2084
|
+
* - SSR-safe
|
|
2085
|
+
*
|
|
2086
|
+
* Perfect for dialogs, menus, tooltips, and any component with
|
|
2087
|
+
* conditionally rendered content.
|
|
2088
|
+
*/
|
|
2089
|
+
/**
|
|
2090
|
+
* Deferred content rendering for performance optimization.
|
|
2091
|
+
*
|
|
2092
|
+
* @param active Reactive boolean controlling activation state
|
|
2093
|
+
* @param options Configuration options
|
|
2094
|
+
* @returns Lazy context with state refs and control functions
|
|
2095
|
+
*
|
|
2096
|
+
* @see https://0.vuetifyjs.com/composables/system/use-lazy
|
|
2097
|
+
*
|
|
2098
|
+
* @example
|
|
2099
|
+
* ```ts
|
|
2100
|
+
* import { ref } from 'vue'
|
|
2101
|
+
* import { useLazy } from '@vuetify/v0'
|
|
2102
|
+
*
|
|
2103
|
+
* const isOpen = ref(false)
|
|
2104
|
+
* const { hasContent, onAfterLeave } = useLazy(isOpen)
|
|
2105
|
+
*
|
|
2106
|
+
* // In template:
|
|
2107
|
+
* // <Transition @after-leave="onAfterLeave">
|
|
2108
|
+
* // <div v-if="isOpen">
|
|
2109
|
+
* // <template v-if="hasContent">
|
|
2110
|
+
* // <!-- Heavy content here -->
|
|
2111
|
+
* // </template>
|
|
2112
|
+
* // </div>
|
|
2113
|
+
* // </Transition>
|
|
2114
|
+
* ```
|
|
2115
|
+
*
|
|
2116
|
+
* @example
|
|
2117
|
+
* Eager mode (render immediately):
|
|
2118
|
+
* ```ts
|
|
2119
|
+
* const { hasContent } = useLazy(isOpen, { eager: true })
|
|
2120
|
+
* // hasContent.value is always true
|
|
2121
|
+
* ```
|
|
2122
|
+
*
|
|
2123
|
+
* @example
|
|
2124
|
+
* With reactive eager prop:
|
|
2125
|
+
* ```ts
|
|
2126
|
+
* const props = defineProps<{ eager: boolean }>()
|
|
2127
|
+
* const { hasContent } = useLazy(isOpen, {
|
|
2128
|
+
* eager: toRef(() => props.eager),
|
|
2129
|
+
* })
|
|
2130
|
+
* ```
|
|
2131
|
+
*/
|
|
2132
|
+
function useLazy(active, options = {}) {
|
|
2133
|
+
const { eager = false } = options;
|
|
2134
|
+
const isBooted = shallowRef(false);
|
|
2135
|
+
const hasContent = toRef(() => isBooted.value || toValue(eager) || toValue(active));
|
|
2136
|
+
const stop = watch(() => toValue(active), (value) => {
|
|
2137
|
+
if (value) isBooted.value = true;
|
|
2138
|
+
}, { immediate: true });
|
|
2139
|
+
function reset() {
|
|
2140
|
+
isBooted.value = false;
|
|
2141
|
+
}
|
|
2142
|
+
function onAfterLeave() {
|
|
2143
|
+
if (!toValue(eager)) reset();
|
|
2144
|
+
}
|
|
2145
|
+
onScopeDispose(stop, true);
|
|
2146
|
+
return {
|
|
2147
|
+
isBooted: shallowReadonly(isBooted),
|
|
2148
|
+
hasContent: shallowReadonly(hasContent),
|
|
2149
|
+
reset,
|
|
2150
|
+
onAfterLeave
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region src/composables/useMediaQuery/index.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* @module useMediaQuery
|
|
2158
|
+
*
|
|
2159
|
+
* @see https://0.vuetifyjs.com/composables/system/use-media-query
|
|
2160
|
+
*
|
|
2161
|
+
* @remarks
|
|
2162
|
+
* Reactive media query composable with automatic cleanup.
|
|
2163
|
+
*
|
|
2164
|
+
* Key features:
|
|
2165
|
+
* - Window matchMedia integration
|
|
2166
|
+
* - Reactive query string support
|
|
2167
|
+
* - SSR-safe (returns false on server)
|
|
2168
|
+
* - Hydration-aware (defers client updates until hydrated)
|
|
2169
|
+
* - Automatic listener cleanup on scope disposal
|
|
2170
|
+
* - Supports both static and dynamic queries
|
|
2171
|
+
*
|
|
2172
|
+
* Perfect for responsive conditionals beyond breakpoint detection.
|
|
2173
|
+
*/
|
|
2174
|
+
/**
|
|
2175
|
+
* Reactive media query matching.
|
|
2176
|
+
*
|
|
2177
|
+
* @param query CSS media query string (reactive).
|
|
2178
|
+
* @returns The media query context.
|
|
2179
|
+
*
|
|
2180
|
+
* @see https://0.vuetifyjs.com/composables/system/use-media-query
|
|
2181
|
+
*
|
|
2182
|
+
* @example
|
|
2183
|
+
* ```ts
|
|
2184
|
+
* // Static query
|
|
2185
|
+
* const { matches } = useMediaQuery('(prefers-color-scheme: dark)')
|
|
2186
|
+
*
|
|
2187
|
+
* // Dynamic query
|
|
2188
|
+
* const minWidth = ref(768)
|
|
2189
|
+
* const { matches } = useMediaQuery(() => `(min-width: ${minWidth.value}px)`)
|
|
2190
|
+
*
|
|
2191
|
+
* // Manual cleanup
|
|
2192
|
+
* const { matches, stop } = useMediaQuery('(hover: hover)')
|
|
2193
|
+
* stop() // Remove listener early
|
|
2194
|
+
* ```
|
|
2195
|
+
*/
|
|
2196
|
+
function useMediaQuery(query) {
|
|
2197
|
+
const matches = shallowRef(false);
|
|
2198
|
+
const mediaQueryList = shallowRef(null);
|
|
2199
|
+
const resolvedQuery = computed(() => toValue(query));
|
|
2200
|
+
const hydration = useHydration();
|
|
2201
|
+
let cleanup = null;
|
|
2202
|
+
function update() {
|
|
2203
|
+
if (!IN_BROWSER || !SUPPORTS_MATCH_MEDIA) return;
|
|
2204
|
+
cleanup?.();
|
|
2205
|
+
cleanup = null;
|
|
2206
|
+
const mql = window.matchMedia(resolvedQuery.value);
|
|
2207
|
+
mediaQueryList.value = mql;
|
|
2208
|
+
matches.value = mql.matches;
|
|
2209
|
+
function handler(e) {
|
|
2210
|
+
matches.value = e.matches;
|
|
2211
|
+
}
|
|
2212
|
+
mql.addEventListener("change", handler);
|
|
2213
|
+
cleanup = () => mql.removeEventListener("change", handler);
|
|
2214
|
+
}
|
|
2215
|
+
const stopWatch = watch([resolvedQuery, () => hydration.isHydrated.value], ([_, hydrated]) => {
|
|
2216
|
+
if (hydrated) update();
|
|
2217
|
+
}, { immediate: true });
|
|
2218
|
+
function stop() {
|
|
2219
|
+
stopWatch();
|
|
2220
|
+
cleanup?.();
|
|
2221
|
+
cleanup = null;
|
|
2222
|
+
}
|
|
2223
|
+
onScopeDispose(stop, true);
|
|
2224
|
+
return {
|
|
2225
|
+
matches: shallowReadonly(matches),
|
|
2226
|
+
query: resolvedQuery,
|
|
2227
|
+
mediaQueryList: shallowReadonly(mediaQueryList),
|
|
2228
|
+
stop
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Check if the user prefers dark color scheme.
|
|
2233
|
+
*
|
|
2234
|
+
* @returns The media query context.
|
|
2235
|
+
*
|
|
2236
|
+
* @example
|
|
2237
|
+
* ```ts
|
|
2238
|
+
* const { matches: prefersDark } = usePrefersDark()
|
|
2239
|
+
* ```
|
|
2240
|
+
*/
|
|
2241
|
+
function usePrefersDark() {
|
|
2242
|
+
return useMediaQuery("(prefers-color-scheme: dark)");
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Check if the user prefers reduced motion.
|
|
2246
|
+
*
|
|
2247
|
+
* @returns The media query context.
|
|
2248
|
+
*
|
|
2249
|
+
* @example
|
|
2250
|
+
* ```ts
|
|
2251
|
+
* const { matches: prefersReducedMotion } = usePrefersReducedMotion()
|
|
2252
|
+
* ```
|
|
2253
|
+
*/
|
|
2254
|
+
function usePrefersReducedMotion() {
|
|
2255
|
+
return useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Check if the user prefers more contrast.
|
|
2259
|
+
*
|
|
2260
|
+
* @returns The media query context.
|
|
2261
|
+
*
|
|
2262
|
+
* @example
|
|
2263
|
+
* ```ts
|
|
2264
|
+
* const { matches: prefersContrast } = usePrefersContrast()
|
|
2265
|
+
* ```
|
|
2266
|
+
*/
|
|
2267
|
+
function usePrefersContrast() {
|
|
2268
|
+
return useMediaQuery("(prefers-contrast: more)");
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
//#endregion
|
|
2272
|
+
//#region src/composables/useMutationObserver/index.ts
|
|
2273
|
+
/**
|
|
2274
|
+
* @module useMutationObserver
|
|
2275
|
+
*
|
|
2276
|
+
* @remarks
|
|
2277
|
+
* MutationObserver composable with lifecycle management.
|
|
2278
|
+
*
|
|
2279
|
+
* Key features:
|
|
2280
|
+
* - MutationObserver API wrapper
|
|
2281
|
+
* - Pause/resume/stop functionality
|
|
2282
|
+
* - Automatic cleanup on unmount
|
|
2283
|
+
* - SSR-safe (checks SUPPORTS_MUTATION_OBSERVER)
|
|
2284
|
+
* - Hydration-aware
|
|
2285
|
+
* - Configurable observation options (childList, attributes, characterData, etc.)
|
|
2286
|
+
*
|
|
2287
|
+
* Perfect for detecting DOM changes and responding to mutations.
|
|
2288
|
+
*/
|
|
2289
|
+
/**
|
|
2290
|
+
* A composable that uses the Mutation Observer API to detect changes in the DOM.
|
|
2291
|
+
*
|
|
2292
|
+
* @param target The element to observe.
|
|
2293
|
+
* @param callback The callback to execute when a mutation is observed.
|
|
2294
|
+
* @param options The options for the Mutation Observer.
|
|
2295
|
+
* @returns An object with methods to control the observer.
|
|
2296
|
+
*
|
|
2297
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
|
2298
|
+
* @see https://0.vuetifyjs.com/composables/system/use-mutation-observer
|
|
2299
|
+
*
|
|
2300
|
+
* @example
|
|
2301
|
+
* ```ts
|
|
2302
|
+
* import { ref } from 'vue'
|
|
2303
|
+
* import { useMutationObserver } from '@vuetify/v0'
|
|
2304
|
+
*
|
|
2305
|
+
* const container = ref<HTMLElement>()
|
|
2306
|
+
*
|
|
2307
|
+
* const { pause, resume, isPaused } = useMutationObserver(
|
|
2308
|
+
* container,
|
|
2309
|
+
* (mutations) => {
|
|
2310
|
+
* mutations.forEach((mutation) => {
|
|
2311
|
+
* if (mutation.type === 'childList') {
|
|
2312
|
+
* console.log('Children changed:', mutation.addedNodes, mutation.removedNodes)
|
|
2313
|
+
* } else if (mutation.type === 'attributes') {
|
|
2314
|
+
* console.log('Attribute changed:', mutation.attributeName)
|
|
2315
|
+
* }
|
|
2316
|
+
* })
|
|
2317
|
+
* },
|
|
2318
|
+
* {
|
|
2319
|
+
* childList: true,
|
|
2320
|
+
* attributes: true,
|
|
2321
|
+
* subtree: true
|
|
2322
|
+
* }
|
|
2323
|
+
* )
|
|
2324
|
+
*
|
|
2325
|
+
* // Pause observation
|
|
2326
|
+
* pause()
|
|
2327
|
+
*
|
|
2328
|
+
* // Resume observation
|
|
2329
|
+
* resume()
|
|
2330
|
+
* ```
|
|
2331
|
+
*/
|
|
2332
|
+
function useMutationObserver(target, callback, options = {}) {
|
|
2333
|
+
const observerOptions = {
|
|
2334
|
+
childList: options.childList ?? true,
|
|
2335
|
+
attributes: options.attributes ?? false,
|
|
2336
|
+
characterData: options.characterData ?? false,
|
|
2337
|
+
subtree: options.subtree ?? false,
|
|
2338
|
+
attributeOldValue: options.attributeOldValue ?? false,
|
|
2339
|
+
characterDataOldValue: options.characterDataOldValue ?? false,
|
|
2340
|
+
attributeFilter: options.attributeFilter
|
|
2341
|
+
};
|
|
2342
|
+
return createObserver(target, callback, {
|
|
2343
|
+
supports: SUPPORTS_MUTATION_OBSERVER,
|
|
2344
|
+
once: options.once,
|
|
2345
|
+
create: (cb) => new MutationObserver((mutations) => {
|
|
2346
|
+
cb(mutations.map((m) => ({
|
|
2347
|
+
type: m.type,
|
|
2348
|
+
target: m.target,
|
|
2349
|
+
addedNodes: m.addedNodes,
|
|
2350
|
+
removedNodes: m.removedNodes,
|
|
2351
|
+
previousSibling: m.previousSibling,
|
|
2352
|
+
nextSibling: m.nextSibling,
|
|
2353
|
+
attributeName: m.attributeName,
|
|
2354
|
+
attributeNamespace: m.attributeNamespace,
|
|
2355
|
+
oldValue: m.oldValue
|
|
2356
|
+
})));
|
|
2357
|
+
}),
|
|
2358
|
+
observe: (obs, el) => obs.observe(el, observerOptions),
|
|
2359
|
+
immediate: options.immediate ? (el) => {
|
|
2360
|
+
const emptyNodeList = {
|
|
2361
|
+
length: 0,
|
|
2362
|
+
item: () => null,
|
|
2363
|
+
forEach: () => {},
|
|
2364
|
+
*[Symbol.iterator]() {}
|
|
2365
|
+
};
|
|
2366
|
+
return [{
|
|
2367
|
+
type: "childList",
|
|
2368
|
+
target: el,
|
|
2369
|
+
addedNodes: emptyNodeList,
|
|
2370
|
+
removedNodes: emptyNodeList,
|
|
2371
|
+
previousSibling: null,
|
|
2372
|
+
nextSibling: null,
|
|
2373
|
+
attributeName: null,
|
|
2374
|
+
attributeNamespace: null,
|
|
2375
|
+
oldValue: null
|
|
2376
|
+
}];
|
|
2377
|
+
} : void 0,
|
|
2378
|
+
onceIncludesImmediate: true
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
//#endregion
|
|
2383
|
+
//#region src/composables/createNested/strategies.ts
|
|
2384
|
+
/**
|
|
2385
|
+
* Multiple open strategy: allows multiple nodes to be open simultaneously.
|
|
2386
|
+
*
|
|
2387
|
+
* @remarks
|
|
2388
|
+
* Perfect for tree views, file explorers, and nested menus where users expect
|
|
2389
|
+
* to keep multiple sections expanded at once.
|
|
2390
|
+
*
|
|
2391
|
+
* @example
|
|
2392
|
+
* ```ts
|
|
2393
|
+
* const tree = createNested({ openStrategy: multipleOpenStrategy })
|
|
2394
|
+
* tree.open('node-1')
|
|
2395
|
+
* tree.open('node-2')
|
|
2396
|
+
* // Both nodes are now open
|
|
2397
|
+
* ```
|
|
2398
|
+
*/
|
|
2399
|
+
const multipleOpenStrategy = {
|
|
2400
|
+
onOpen: void 0,
|
|
2401
|
+
onClose: void 0
|
|
2402
|
+
};
|
|
2403
|
+
/**
|
|
2404
|
+
* Single open strategy: only one node can be open at a time (accordion behavior).
|
|
2405
|
+
*
|
|
2406
|
+
* @remarks
|
|
2407
|
+
* Perfect for accordions, single-select navigation, and contexts where only
|
|
2408
|
+
* one section should be visible at a time.
|
|
2409
|
+
*
|
|
2410
|
+
* @example
|
|
2411
|
+
* ```ts
|
|
2412
|
+
* const tree = createNested({ openStrategy: singleOpenStrategy })
|
|
2413
|
+
* tree.open('node-1') // node-1 opens
|
|
2414
|
+
* tree.open('node-2') // node-1 closes, node-2 opens
|
|
2415
|
+
* ```
|
|
2416
|
+
*/
|
|
2417
|
+
const singleOpenStrategy = {
|
|
2418
|
+
onOpen: (id, context) => {
|
|
2419
|
+
for (const openedId of context.openedIds) if (openedId !== id) context.openedIds.delete(openedId);
|
|
2420
|
+
},
|
|
2421
|
+
onClose: void 0
|
|
2422
|
+
};
|
|
2423
|
+
|
|
2424
|
+
//#endregion
|
|
2425
|
+
//#region src/composables/createNested/index.ts
|
|
2426
|
+
/**
|
|
2427
|
+
* @module createNested
|
|
2428
|
+
*
|
|
2429
|
+
* @remarks
|
|
2430
|
+
* Hierarchical tree management composable extending createGroup with:
|
|
2431
|
+
* - Parent-child relationship tracking (children/parents Maps)
|
|
2432
|
+
* - Open/close state management
|
|
2433
|
+
* - Tree traversal utilities (getPath, getDescendants, etc.)
|
|
2434
|
+
* - Pluggable open strategies
|
|
2435
|
+
*
|
|
2436
|
+
* Inheritance chain: createSelection → createGroup → createNested
|
|
2437
|
+
*/
|
|
2438
|
+
/**
|
|
2439
|
+
* Resolves open mode to an OpenStrategy.
|
|
2440
|
+
*/
|
|
2441
|
+
function resolveOpenStrategy(open = "multiple") {
|
|
2442
|
+
return open === "single" ? singleOpenStrategy : multipleOpenStrategy;
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Creates a new nested tree instance with hierarchical management.
|
|
2446
|
+
*
|
|
2447
|
+
* Extends `createGroup` to support parent-child relationships, tree traversal,
|
|
2448
|
+
* and open/close state management. Perfect for tree views, nested navigation,
|
|
2449
|
+
* and hierarchical data structures.
|
|
2450
|
+
*
|
|
2451
|
+
* @param options The options for the nested instance.
|
|
2452
|
+
* @template Z The type of the nested ticket.
|
|
2453
|
+
* @template E The type of the nested context.
|
|
2454
|
+
* @returns A new nested instance with tree management capabilities.
|
|
2455
|
+
*
|
|
2456
|
+
* @remarks
|
|
2457
|
+
* **Key Differences from `createGroup`:**
|
|
2458
|
+
* - `register()` accepts `parentId` for establishing parent-child relationships
|
|
2459
|
+
* - `unregister()` accepts optional `cascade` parameter for cascading deletions
|
|
2460
|
+
* - Adds tree traversal methods: `getPath()`, `getAncestors()`, `getDescendants()`
|
|
2461
|
+
* - Adds computed properties: `roots`, `leaves`, `isLeaf()`, `getDepth()`
|
|
2462
|
+
* - Adds open state management: `open()`, `close()`, `flip()`, `opened()`
|
|
2463
|
+
*
|
|
2464
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-nested
|
|
2465
|
+
*
|
|
2466
|
+
* @example
|
|
2467
|
+
* ```ts
|
|
2468
|
+
* import { createNested } from '@vuetify/v0'
|
|
2469
|
+
*
|
|
2470
|
+
* const tree = createNested()
|
|
2471
|
+
*
|
|
2472
|
+
* const root = tree.register({ id: 'root', value: 'Root' })
|
|
2473
|
+
* const child1 = tree.register({ id: 'child-1', value: 'Child 1', parentId: 'root' })
|
|
2474
|
+
* const child2 = tree.register({ id: 'child-2', value: 'Child 2', parentId: 'root' })
|
|
2475
|
+
*
|
|
2476
|
+
* console.log(tree.getPath('child-1')) // ['root', 'child-1']
|
|
2477
|
+
* console.log(tree.getDescendants('root')) // ['child-1', 'child-2']
|
|
2478
|
+
* console.log(tree.isLeaf('child-2')) // true
|
|
2479
|
+
*
|
|
2480
|
+
* tree.open('root')
|
|
2481
|
+
* console.log(tree.opened('root')) // true
|
|
2482
|
+
* ```
|
|
2483
|
+
*/
|
|
2484
|
+
function createNested(_options = {}) {
|
|
2485
|
+
const { open: openMode = "multiple", openAll = false, reveal: revealOnOpen = false, selection: selectionMode = "cascade", active: activeMode = "single", openStrategy, ...options } = _options;
|
|
2486
|
+
const mandatoryOption = _options.mandatory ?? false;
|
|
2487
|
+
const multipleOption = _options.multiple ?? true;
|
|
2488
|
+
const resolvedOpenStrategy = openStrategy ?? resolveOpenStrategy(openMode);
|
|
2489
|
+
const group = createGroup(options);
|
|
2490
|
+
const logger$1 = useLogger();
|
|
2491
|
+
const children = shallowReactive(/* @__PURE__ */ new Map());
|
|
2492
|
+
const parents = shallowReactive(/* @__PURE__ */ new Map());
|
|
2493
|
+
const openedIds = shallowReactive(/* @__PURE__ */ new Set());
|
|
2494
|
+
const activeIds = shallowReactive(/* @__PURE__ */ new Set());
|
|
2495
|
+
const rootIds = shallowReactive(/* @__PURE__ */ new Set());
|
|
2496
|
+
let batching = false;
|
|
2497
|
+
let pendingChildren;
|
|
2498
|
+
let pendingParents;
|
|
2499
|
+
let pendingRoots;
|
|
2500
|
+
const activeItems = computed(() => {
|
|
2501
|
+
return new Set(Array.from(activeIds).map((id) => group.get(id)).filter((item) => !/* @__PURE__ */ isUndefined(item)));
|
|
2502
|
+
});
|
|
2503
|
+
const activeIndexes = computed(() => {
|
|
2504
|
+
return new Set(Array.from(activeItems.value).map((item) => item?.index).filter((index) => !/* @__PURE__ */ isUndefined(index)));
|
|
2505
|
+
});
|
|
2506
|
+
const openedItems = computed(() => {
|
|
2507
|
+
return new Set(Array.from(openedIds).map((id) => group.get(id)).filter((item) => !/* @__PURE__ */ isUndefined(item)));
|
|
2508
|
+
});
|
|
2509
|
+
const roots = computed(() => {
|
|
2510
|
+
return Array.from(rootIds).map((id) => group.get(id)).filter((item) => !/* @__PURE__ */ isUndefined(item));
|
|
2511
|
+
});
|
|
2512
|
+
const leaves = computed(() => {
|
|
2513
|
+
return group.values().filter((item) => {
|
|
2514
|
+
const childList = children.get(item.id);
|
|
2515
|
+
return /* @__PURE__ */ isUndefined(childList) || childList.length === 0;
|
|
2516
|
+
});
|
|
2517
|
+
});
|
|
2518
|
+
function opened(id) {
|
|
2519
|
+
return openedIds.has(id);
|
|
2520
|
+
}
|
|
2521
|
+
function open(ids) {
|
|
2522
|
+
for (const id of toArray(ids)) {
|
|
2523
|
+
if (!group.has(id)) continue;
|
|
2524
|
+
if (revealOnOpen) {
|
|
2525
|
+
let parentId = parents.get(id);
|
|
2526
|
+
while (!/* @__PURE__ */ isUndefined(parentId)) {
|
|
2527
|
+
openedIds.add(parentId);
|
|
2528
|
+
parentId = parents.get(parentId);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
openedIds.add(id);
|
|
2532
|
+
resolvedOpenStrategy.onOpen?.(id, context);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function close(ids) {
|
|
2536
|
+
for (const id of toArray(ids)) {
|
|
2537
|
+
if (!group.has(id)) continue;
|
|
2538
|
+
openedIds.delete(id);
|
|
2539
|
+
resolvedOpenStrategy.onClose?.(id, context);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
function flip(ids) {
|
|
2543
|
+
for (const id of toArray(ids)) {
|
|
2544
|
+
if (!group.has(id)) continue;
|
|
2545
|
+
if (opened(id)) close(id);
|
|
2546
|
+
else open(id);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Open node(s) and their immediate non-leaf children.
|
|
2551
|
+
* Useful for expanding a section with its immediate subcategories.
|
|
2552
|
+
*/
|
|
2553
|
+
function unfold(ids) {
|
|
2554
|
+
for (const id of toArray(ids)) {
|
|
2555
|
+
if (!group.has(id)) continue;
|
|
2556
|
+
open(id);
|
|
2557
|
+
const childIds = children.get(id) ?? [];
|
|
2558
|
+
for (const childId of childIds) if (!isLeaf(childId)) open(childId);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Reveal a node by opening all its ancestors.
|
|
2563
|
+
* Makes the node visible in a collapsed tree without opening the node itself.
|
|
2564
|
+
*/
|
|
2565
|
+
function reveal(ids) {
|
|
2566
|
+
for (const id of toArray(ids)) {
|
|
2567
|
+
if (!group.has(id)) continue;
|
|
2568
|
+
const ancestorIds = getAncestors(id);
|
|
2569
|
+
if (ancestorIds.length > 0) open(ancestorIds);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Fully expand a node and all its non-leaf descendants.
|
|
2574
|
+
* Opens the entire subtree rooted at the given node.
|
|
2575
|
+
*/
|
|
2576
|
+
function expand(ids) {
|
|
2577
|
+
for (const id of toArray(ids)) {
|
|
2578
|
+
if (!group.has(id)) continue;
|
|
2579
|
+
if (!isLeaf(id)) open(id);
|
|
2580
|
+
for (const descendantId of getDescendants(id)) if (!isLeaf(descendantId)) open(descendantId);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
function expandAll() {
|
|
2584
|
+
for (const [id, childList] of children) if (childList.length > 0) openedIds.add(id);
|
|
2585
|
+
}
|
|
2586
|
+
function collapseAll() {
|
|
2587
|
+
openedIds.clear();
|
|
2588
|
+
}
|
|
2589
|
+
function activated(id) {
|
|
2590
|
+
return activeIds.has(id);
|
|
2591
|
+
}
|
|
2592
|
+
function activate(ids) {
|
|
2593
|
+
for (const id of toArray(ids)) {
|
|
2594
|
+
if (!group.has(id)) continue;
|
|
2595
|
+
if (activeMode === "single") {
|
|
2596
|
+
for (const activeId of activeIds) if (activeId !== id) activeIds.delete(activeId);
|
|
2597
|
+
}
|
|
2598
|
+
activeIds.add(id);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function deactivate(ids) {
|
|
2602
|
+
for (const id of toArray(ids)) activeIds.delete(id);
|
|
2603
|
+
}
|
|
2604
|
+
function deactivateAll() {
|
|
2605
|
+
activeIds.clear();
|
|
2606
|
+
}
|
|
2607
|
+
function getPath(id) {
|
|
2608
|
+
const path = [];
|
|
2609
|
+
let currentId = id;
|
|
2610
|
+
while (!/* @__PURE__ */ isUndefined(currentId)) {
|
|
2611
|
+
path.unshift(currentId);
|
|
2612
|
+
currentId = parents.get(currentId);
|
|
2613
|
+
}
|
|
2614
|
+
return path;
|
|
2615
|
+
}
|
|
2616
|
+
function getAncestors(id) {
|
|
2617
|
+
return getPath(id).slice(0, -1);
|
|
2618
|
+
}
|
|
2619
|
+
function getDescendants(id) {
|
|
2620
|
+
const descendants = [];
|
|
2621
|
+
const initial = children.get(id);
|
|
2622
|
+
if (!initial) return descendants;
|
|
2623
|
+
const queue = initial.slice();
|
|
2624
|
+
let index = 0;
|
|
2625
|
+
while (index < queue.length) {
|
|
2626
|
+
const currentId = queue[index++];
|
|
2627
|
+
descendants.push(currentId);
|
|
2628
|
+
const childIds = children.get(currentId);
|
|
2629
|
+
if (childIds) for (const cid of childIds) queue.push(cid);
|
|
2630
|
+
}
|
|
2631
|
+
return descendants;
|
|
2632
|
+
}
|
|
2633
|
+
function isLeaf(id) {
|
|
2634
|
+
const childList = children.get(id);
|
|
2635
|
+
return /* @__PURE__ */ isUndefined(childList) || childList.length === 0;
|
|
2636
|
+
}
|
|
2637
|
+
function getDepth(id) {
|
|
2638
|
+
return getAncestors(id).length;
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Check if `ancestorId` is an ancestor of `descendantId`.
|
|
2642
|
+
* Returns false if either ID doesn't exist in the tree.
|
|
2643
|
+
*/
|
|
2644
|
+
function isAncestorOf(ancestorId, descendantId) {
|
|
2645
|
+
if (!group.has(ancestorId) || !group.has(descendantId)) return false;
|
|
2646
|
+
if (ancestorId === descendantId) return false;
|
|
2647
|
+
let currentId = parents.get(descendantId);
|
|
2648
|
+
while (!/* @__PURE__ */ isUndefined(currentId)) {
|
|
2649
|
+
if (currentId === ancestorId) return true;
|
|
2650
|
+
currentId = parents.get(currentId);
|
|
2651
|
+
}
|
|
2652
|
+
return false;
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Check if `id` has `ancestorId` as an ancestor.
|
|
2656
|
+
* Semantic alias for isAncestorOf(ancestorId, id).
|
|
2657
|
+
*/
|
|
2658
|
+
function hasAncestor(id, ancestorId) {
|
|
2659
|
+
return isAncestorOf(ancestorId, id);
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Get siblings of a node (including self).
|
|
2663
|
+
* For root nodes, returns all root IDs.
|
|
2664
|
+
*/
|
|
2665
|
+
function siblings(id) {
|
|
2666
|
+
if (!group.has(id)) return [];
|
|
2667
|
+
const parentId = parents.get(id);
|
|
2668
|
+
if (/* @__PURE__ */ isUndefined(parentId)) return group.values().filter((item) => /* @__PURE__ */ isUndefined(parents.get(item.id))).map((item) => item.id);
|
|
2669
|
+
return children.get(parentId) ?? [];
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Get 1-indexed position among siblings (for aria-posinset).
|
|
2673
|
+
* Returns 0 if node not found.
|
|
2674
|
+
*/
|
|
2675
|
+
function position(id) {
|
|
2676
|
+
const index = siblings(id).indexOf(id);
|
|
2677
|
+
return index === -1 ? 0 : index + 1;
|
|
2678
|
+
}
|
|
2679
|
+
function updateAncestors(id) {
|
|
2680
|
+
const { selectedIds, mixedIds } = group;
|
|
2681
|
+
let parentId = parents.get(id);
|
|
2682
|
+
while (!/* @__PURE__ */ isUndefined(parentId)) {
|
|
2683
|
+
const childIds = children.get(parentId);
|
|
2684
|
+
if (childIds && childIds.length > 0) {
|
|
2685
|
+
const allSelected = childIds.every((cid) => selectedIds.has(cid));
|
|
2686
|
+
const someSelected = !allSelected && childIds.some((cid) => selectedIds.has(cid) || mixedIds.has(cid));
|
|
2687
|
+
if (allSelected) {
|
|
2688
|
+
mixedIds.delete(parentId);
|
|
2689
|
+
selectedIds.add(parentId);
|
|
2690
|
+
} else if (someSelected) {
|
|
2691
|
+
selectedIds.delete(parentId);
|
|
2692
|
+
mixedIds.add(parentId);
|
|
2693
|
+
} else {
|
|
2694
|
+
mixedIds.delete(parentId);
|
|
2695
|
+
selectedIds.delete(parentId);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
parentId = parents.get(parentId);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
function getLeafDescendants(id) {
|
|
2702
|
+
return getDescendants(id).filter((did) => isLeaf(did));
|
|
2703
|
+
}
|
|
2704
|
+
function select(ids) {
|
|
2705
|
+
for (const id of toArray(ids)) {
|
|
2706
|
+
if (!group.has(id)) continue;
|
|
2707
|
+
if (selectionMode === "independent") group.select(id);
|
|
2708
|
+
else if (selectionMode === "leaf") if (isLeaf(id)) group.select(id);
|
|
2709
|
+
else for (const lid of getLeafDescendants(id)) group.select(lid);
|
|
2710
|
+
else {
|
|
2711
|
+
if (!toValue(multipleOption)) {
|
|
2712
|
+
group.select(id);
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
const { selectedIds, mixedIds } = group;
|
|
2716
|
+
mixedIds.delete(id);
|
|
2717
|
+
selectedIds.add(id);
|
|
2718
|
+
for (const did of getDescendants(id)) {
|
|
2719
|
+
mixedIds.delete(did);
|
|
2720
|
+
selectedIds.add(did);
|
|
2721
|
+
}
|
|
2722
|
+
updateAncestors(id);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function unselect(ids) {
|
|
2727
|
+
for (const id of toArray(ids)) {
|
|
2728
|
+
if (!group.has(id)) continue;
|
|
2729
|
+
if (selectionMode === "independent") group.unselect(id);
|
|
2730
|
+
else if (selectionMode === "leaf") if (isLeaf(id)) group.unselect(id);
|
|
2731
|
+
else for (const lid of getLeafDescendants(id)) group.unselect(lid);
|
|
2732
|
+
else {
|
|
2733
|
+
const { selectedIds, mixedIds } = group;
|
|
2734
|
+
if (toValue(mandatoryOption)) {
|
|
2735
|
+
const toRemove = new Set([id, ...getDescendants(id)]);
|
|
2736
|
+
if ([...selectedIds].every((sid) => toRemove.has(sid))) return;
|
|
2737
|
+
}
|
|
2738
|
+
mixedIds.delete(id);
|
|
2739
|
+
selectedIds.delete(id);
|
|
2740
|
+
for (const did of getDescendants(id)) {
|
|
2741
|
+
mixedIds.delete(did);
|
|
2742
|
+
selectedIds.delete(did);
|
|
2743
|
+
}
|
|
2744
|
+
updateAncestors(id);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
function toggle(ids) {
|
|
2749
|
+
for (const id of toArray(ids)) if (selectionMode === "independent") group.toggle(id);
|
|
2750
|
+
else if (selectionMode === "leaf") if (isLeaf(id)) group.toggle(id);
|
|
2751
|
+
else {
|
|
2752
|
+
const leafIds = getLeafDescendants(id);
|
|
2753
|
+
if (leafIds.every((lid) => group.selected(lid))) for (const lid of leafIds) group.unselect(lid);
|
|
2754
|
+
else for (const lid of leafIds) group.select(lid);
|
|
2755
|
+
}
|
|
2756
|
+
else if (group.selected(id) || group.mixed(id)) unselect(id);
|
|
2757
|
+
else select(id);
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Converts the tree to a flat array with parentId references.
|
|
2761
|
+
* Useful for serialization or sending to APIs/AI systems.
|
|
2762
|
+
*/
|
|
2763
|
+
function toFlat() {
|
|
2764
|
+
return group.values().map((item) => ({
|
|
2765
|
+
id: item.id,
|
|
2766
|
+
parentId: parents.get(item.id),
|
|
2767
|
+
value: item.value
|
|
2768
|
+
}));
|
|
2769
|
+
}
|
|
2770
|
+
function register(registration = {}) {
|
|
2771
|
+
const id = registration.id ?? /* @__PURE__ */ useId$1();
|
|
2772
|
+
const parentId = registration.parentId;
|
|
2773
|
+
const nested = registration.children;
|
|
2774
|
+
if (!/* @__PURE__ */ isUndefined(parentId) && !group.has(parentId)) logger$1.warn(`Parent with id "${parentId}" does not exist. Registering "${id}" without parent.`);
|
|
2775
|
+
if (!/* @__PURE__ */ isUndefined(parentId) && group.has(parentId)) {
|
|
2776
|
+
if (batching) {
|
|
2777
|
+
pendingParents.set(id, parentId);
|
|
2778
|
+
const pending = pendingChildren.get(parentId);
|
|
2779
|
+
if (pending) pending.push(id);
|
|
2780
|
+
else {
|
|
2781
|
+
const existing = children.get(parentId);
|
|
2782
|
+
const list = existing ? [...existing, id] : [id];
|
|
2783
|
+
pendingChildren.set(parentId, list);
|
|
2784
|
+
}
|
|
2785
|
+
} else {
|
|
2786
|
+
parents.set(id, parentId);
|
|
2787
|
+
const list = children.get(parentId);
|
|
2788
|
+
children.set(parentId, list ? [...list, id] : [id]);
|
|
2789
|
+
}
|
|
2790
|
+
if (openAll) openedIds.add(parentId);
|
|
2791
|
+
} else if (batching) {
|
|
2792
|
+
pendingParents.set(id, void 0);
|
|
2793
|
+
pendingRoots.add(id);
|
|
2794
|
+
} else {
|
|
2795
|
+
parents.set(id, void 0);
|
|
2796
|
+
rootIds.add(id);
|
|
2797
|
+
}
|
|
2798
|
+
const { children: _, ...rest } = registration;
|
|
2799
|
+
const item = {
|
|
2800
|
+
...rest,
|
|
2801
|
+
id,
|
|
2802
|
+
parentId: batching ? pendingParents.get(id) : parents.get(id),
|
|
2803
|
+
isOpen: toRef(() => opened(id)),
|
|
2804
|
+
isActive: toRef(() => activated(id)),
|
|
2805
|
+
isLeaf: toRef(() => isLeaf(id)),
|
|
2806
|
+
depth: toRef(() => getDepth(id)),
|
|
2807
|
+
open: () => open(id),
|
|
2808
|
+
close: () => close(id),
|
|
2809
|
+
flip: () => flip(id),
|
|
2810
|
+
reveal: () => reveal(id),
|
|
2811
|
+
activate: () => activate(id),
|
|
2812
|
+
deactivate: () => deactivate(id),
|
|
2813
|
+
getPath: () => getPath(id),
|
|
2814
|
+
getAncestors: () => getAncestors(id),
|
|
2815
|
+
getDescendants: () => getDescendants(id),
|
|
2816
|
+
isAncestorOf: (descendantId) => isAncestorOf(id, descendantId),
|
|
2817
|
+
hasAncestor: (ancestorId) => hasAncestor(id, ancestorId),
|
|
2818
|
+
siblings: () => siblings(id),
|
|
2819
|
+
position: () => position(id)
|
|
2820
|
+
};
|
|
2821
|
+
const ticket = group.register(item);
|
|
2822
|
+
if (registration.active) activate(id);
|
|
2823
|
+
if (nested?.length) for (const child of nested) register({
|
|
2824
|
+
...child,
|
|
2825
|
+
parentId: id
|
|
2826
|
+
});
|
|
2827
|
+
return ticket;
|
|
2828
|
+
}
|
|
2829
|
+
function unregister(id, cascade = false) {
|
|
2830
|
+
if (!group.has(id)) return;
|
|
2831
|
+
const parentId = parents.get(id);
|
|
2832
|
+
if (!/* @__PURE__ */ isUndefined(parentId)) {
|
|
2833
|
+
const siblings$1 = children.get(parentId);
|
|
2834
|
+
if (siblings$1) {
|
|
2835
|
+
const filtered = siblings$1.filter((sid) => sid !== id);
|
|
2836
|
+
if (filtered.length === 0) children.delete(parentId);
|
|
2837
|
+
else children.set(parentId, filtered);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
const list = children.get(id);
|
|
2841
|
+
if (list?.length) if (cascade) {
|
|
2842
|
+
const descendantIds = getDescendants(id);
|
|
2843
|
+
for (const did of descendantIds) {
|
|
2844
|
+
parents.delete(did);
|
|
2845
|
+
children.delete(did);
|
|
2846
|
+
openedIds.delete(did);
|
|
2847
|
+
activeIds.delete(did);
|
|
2848
|
+
rootIds.delete(did);
|
|
2849
|
+
}
|
|
2850
|
+
group.offboard(descendantIds);
|
|
2851
|
+
} else {
|
|
2852
|
+
for (const did of getDescendants(id)) {
|
|
2853
|
+
openedIds.delete(did);
|
|
2854
|
+
activeIds.delete(did);
|
|
2855
|
+
}
|
|
2856
|
+
for (const cid of list) {
|
|
2857
|
+
parents.set(cid, void 0);
|
|
2858
|
+
rootIds.add(cid);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
parents.delete(id);
|
|
2862
|
+
children.delete(id);
|
|
2863
|
+
openedIds.delete(id);
|
|
2864
|
+
activeIds.delete(id);
|
|
2865
|
+
rootIds.delete(id);
|
|
2866
|
+
group.unregister(id);
|
|
2867
|
+
}
|
|
2868
|
+
function offboard(ids, cascade = false) {
|
|
2869
|
+
for (const id of ids) unregister(id, cascade);
|
|
2870
|
+
}
|
|
2871
|
+
function onboard(registrations) {
|
|
2872
|
+
batching = true;
|
|
2873
|
+
pendingChildren = /* @__PURE__ */ new Map();
|
|
2874
|
+
pendingParents = /* @__PURE__ */ new Map();
|
|
2875
|
+
pendingRoots = /* @__PURE__ */ new Set();
|
|
2876
|
+
try {
|
|
2877
|
+
const result = group.batch(() => registrations.map((registration) => register(registration)));
|
|
2878
|
+
for (const [id, list] of pendingChildren) children.set(id, list);
|
|
2879
|
+
for (const [id, pid] of pendingParents) parents.set(id, pid);
|
|
2880
|
+
for (const id of pendingRoots) rootIds.add(id);
|
|
2881
|
+
return result;
|
|
2882
|
+
} finally {
|
|
2883
|
+
batching = false;
|
|
2884
|
+
pendingChildren = void 0;
|
|
2885
|
+
pendingParents = void 0;
|
|
2886
|
+
pendingRoots = void 0;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
function clear() {
|
|
2890
|
+
children.clear();
|
|
2891
|
+
parents.clear();
|
|
2892
|
+
openedIds.clear();
|
|
2893
|
+
activeIds.clear();
|
|
2894
|
+
rootIds.clear();
|
|
2895
|
+
group.reset();
|
|
2896
|
+
}
|
|
2897
|
+
function reset() {
|
|
2898
|
+
clear();
|
|
2899
|
+
}
|
|
2900
|
+
const context = {
|
|
2901
|
+
...group,
|
|
2902
|
+
children,
|
|
2903
|
+
parents,
|
|
2904
|
+
openedIds,
|
|
2905
|
+
openedItems,
|
|
2906
|
+
activeIds,
|
|
2907
|
+
activeItems,
|
|
2908
|
+
activeIndexes,
|
|
2909
|
+
roots,
|
|
2910
|
+
leaves,
|
|
2911
|
+
getPath,
|
|
2912
|
+
getAncestors,
|
|
2913
|
+
getDescendants,
|
|
2914
|
+
isLeaf,
|
|
2915
|
+
getDepth,
|
|
2916
|
+
isAncestorOf,
|
|
2917
|
+
hasAncestor,
|
|
2918
|
+
siblings,
|
|
2919
|
+
position,
|
|
2920
|
+
open,
|
|
2921
|
+
close,
|
|
2922
|
+
flip,
|
|
2923
|
+
opened,
|
|
2924
|
+
unfold,
|
|
2925
|
+
reveal,
|
|
2926
|
+
expand,
|
|
2927
|
+
expandAll,
|
|
2928
|
+
collapseAll,
|
|
2929
|
+
activate,
|
|
2930
|
+
deactivate,
|
|
2931
|
+
activated,
|
|
2932
|
+
deactivateAll,
|
|
2933
|
+
toFlat,
|
|
2934
|
+
openStrategy: resolvedOpenStrategy,
|
|
2935
|
+
select,
|
|
2936
|
+
unselect,
|
|
2937
|
+
toggle,
|
|
2938
|
+
register,
|
|
2939
|
+
unregister,
|
|
2940
|
+
offboard,
|
|
2941
|
+
onboard,
|
|
2942
|
+
clear,
|
|
2943
|
+
reset,
|
|
2944
|
+
get size() {
|
|
2945
|
+
return group.size;
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
return context;
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Creates a new nested context with provide/inject pattern.
|
|
2952
|
+
*
|
|
2953
|
+
* @param options The options for the nested context.
|
|
2954
|
+
* @returns A trinity tuple [useNested, provideNested, defaultNested]
|
|
2955
|
+
*
|
|
2956
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-nested
|
|
2957
|
+
*
|
|
2958
|
+
* @example
|
|
2959
|
+
* ```ts
|
|
2960
|
+
* import { createNestedContext } from '@vuetify/v0'
|
|
2961
|
+
*
|
|
2962
|
+
* export const [useTree, provideTree, tree] = createNestedContext()
|
|
2963
|
+
*
|
|
2964
|
+
* // In parent: provideTree()
|
|
2965
|
+
* // In child: const tree = useTree()
|
|
2966
|
+
* ```
|
|
2967
|
+
*/
|
|
2968
|
+
function createNestedContext(_options = {}) {
|
|
2969
|
+
const { namespace = "v0:nested", ...options } = _options;
|
|
2970
|
+
const [useNestedContext, _provideNestedContext] = createContext(namespace);
|
|
2971
|
+
const context = createNested(options);
|
|
2972
|
+
function provideNestedContext(_context = context, app) {
|
|
2973
|
+
return _provideNestedContext(_context, app);
|
|
2974
|
+
}
|
|
2975
|
+
return createTrinity(useNestedContext, provideNestedContext, context);
|
|
2976
|
+
}
|
|
2977
|
+
/**
|
|
2978
|
+
* Returns the current nested instance from context.
|
|
2979
|
+
*
|
|
2980
|
+
* @param namespace The namespace for the nested context. Defaults to `'v0:nested'`.
|
|
2981
|
+
* @returns The current nested instance.
|
|
2982
|
+
*
|
|
2983
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-nested
|
|
2984
|
+
*/
|
|
2985
|
+
function useNested(namespace = "v0:nested") {
|
|
2986
|
+
return useContext(namespace);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
//#endregion
|
|
2990
|
+
//#region src/composables/createQueue/index.ts
|
|
2991
|
+
/**
|
|
2992
|
+
* @module createQueue
|
|
2993
|
+
*
|
|
2994
|
+
* @remarks
|
|
2995
|
+
* A queue composable for managing time-based collections with:
|
|
2996
|
+
* - Automatic timeout-based removal
|
|
2997
|
+
* - Pause/resume functionality
|
|
2998
|
+
* - FIFO (First In, First Out) ordering
|
|
2999
|
+
* - Manual dismissal support
|
|
3000
|
+
* - Queue progression management
|
|
3001
|
+
*
|
|
3002
|
+
* Built on top of createRegistry, the queue automatically manages timeouts for tickets,
|
|
3003
|
+
* ensuring only the first ticket in the queue is active at any time. When a ticket
|
|
3004
|
+
* expires or is removed, the next ticket in the queue automatically becomes active.
|
|
3005
|
+
*/
|
|
3006
|
+
/**
|
|
3007
|
+
* Creates a new queue instance
|
|
3008
|
+
*
|
|
3009
|
+
* @param options The options for the queue instance
|
|
3010
|
+
* @template Z The type of queue ticket that extends QueueTicket. Use this to add custom properties to tickets.
|
|
3011
|
+
* @template E The type of queue context that extends QueueContext<Z>. Use this when extending the queue with additional methods.
|
|
3012
|
+
* @returns A new queue instance
|
|
3013
|
+
*
|
|
3014
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-queue
|
|
3015
|
+
*
|
|
3016
|
+
* @example
|
|
3017
|
+
* ```ts
|
|
3018
|
+
* import { useQueue } from '@vuetify/v0'
|
|
3019
|
+
*
|
|
3020
|
+
* const queue = useQueue()
|
|
3021
|
+
*
|
|
3022
|
+
* // Register an ticket with default timeout (3000ms)
|
|
3023
|
+
* const ticket1 = queue.register({ value: 'Ticket 1' })
|
|
3024
|
+
*
|
|
3025
|
+
* // Register an ticket with custom timeout
|
|
3026
|
+
* const ticket2 = queue.register({ value: 'Ticket 2', timeout: 5000 })
|
|
3027
|
+
*
|
|
3028
|
+
* // Register a persistent ticket that must be manually dismissed
|
|
3029
|
+
* const ticket3 = queue.register({ value: 'Ticket 3', timeout: -1 })
|
|
3030
|
+
*
|
|
3031
|
+
* // Dismiss an ticket using the convenience method
|
|
3032
|
+
* ticket3.dismiss()
|
|
3033
|
+
*
|
|
3034
|
+
* console.log(queue.size) // 2
|
|
3035
|
+
* ```
|
|
3036
|
+
*/
|
|
3037
|
+
function createQueue(_options = {}) {
|
|
3038
|
+
const { timeout: _timeout = 3e3, ...options } = _options;
|
|
3039
|
+
const registry = createRegistry({
|
|
3040
|
+
...options,
|
|
3041
|
+
events: true
|
|
3042
|
+
});
|
|
3043
|
+
const timeouts = /* @__PURE__ */ new Map();
|
|
3044
|
+
function startTimeout(ticket) {
|
|
3045
|
+
if (/* @__PURE__ */ isUndefined(ticket.timeout) || ticket.timeout < 0 || ticket.isPaused) return;
|
|
3046
|
+
const timeout = setTimeout(() => {
|
|
3047
|
+
timeouts.delete(ticket.id);
|
|
3048
|
+
registry.unregister(ticket.id);
|
|
3049
|
+
resume();
|
|
3050
|
+
}, ticket.timeout);
|
|
3051
|
+
timeouts.set(ticket.id, timeout);
|
|
3052
|
+
}
|
|
3053
|
+
function clearTimeout$1(id) {
|
|
3054
|
+
const timeout = timeouts.get(id);
|
|
3055
|
+
if (timeout) {
|
|
3056
|
+
globalThis.clearTimeout(timeout);
|
|
3057
|
+
timeouts.delete(id);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function register(registration = {}) {
|
|
3061
|
+
const id = registration.id ?? /* @__PURE__ */ useId$1();
|
|
3062
|
+
const timeout = Object.prototype.hasOwnProperty.call(registration, "timeout") ? registration.timeout : _timeout;
|
|
3063
|
+
const ticket = {
|
|
3064
|
+
...registration,
|
|
3065
|
+
id,
|
|
3066
|
+
timeout,
|
|
3067
|
+
isPaused: registry.size > 0,
|
|
3068
|
+
dismiss: () => unregister(id)
|
|
3069
|
+
};
|
|
3070
|
+
const registered = registry.register(ticket);
|
|
3071
|
+
startTimeout(registered);
|
|
3072
|
+
return registered;
|
|
3073
|
+
}
|
|
3074
|
+
function unregister(id) {
|
|
3075
|
+
const ticket = /* @__PURE__ */ isUndefined(id) ? registry.seek("first") : registry.get(id);
|
|
3076
|
+
if (!ticket) return void 0;
|
|
3077
|
+
const wasFirst = ticket.index === 0;
|
|
3078
|
+
clearTimeout$1(ticket.id);
|
|
3079
|
+
registry.unregister(ticket.id);
|
|
3080
|
+
if (wasFirst) resume();
|
|
3081
|
+
return ticket;
|
|
3082
|
+
}
|
|
3083
|
+
function offboard(ids) {
|
|
3084
|
+
let hadFirst = false;
|
|
3085
|
+
for (const id of ids) {
|
|
3086
|
+
const ticket = registry.get(id);
|
|
3087
|
+
if (!ticket) continue;
|
|
3088
|
+
if (ticket.index === 0) hadFirst = true;
|
|
3089
|
+
clearTimeout$1(ticket.id);
|
|
3090
|
+
}
|
|
3091
|
+
registry.offboard(ids);
|
|
3092
|
+
if (hadFirst) resume();
|
|
3093
|
+
}
|
|
3094
|
+
function pause() {
|
|
3095
|
+
const ticket = registry.seek("first");
|
|
3096
|
+
if (!ticket || ticket.isPaused) return void 0;
|
|
3097
|
+
clearTimeout$1(ticket.id);
|
|
3098
|
+
return registry.upsert(ticket.id, { isPaused: true });
|
|
3099
|
+
}
|
|
3100
|
+
function resume() {
|
|
3101
|
+
const ticket = registry.seek("first");
|
|
3102
|
+
if (!ticket || ticket.index !== 0 || !ticket.isPaused) return void 0;
|
|
3103
|
+
const updated = registry.upsert(ticket.id, { isPaused: false });
|
|
3104
|
+
startTimeout(updated);
|
|
3105
|
+
return updated;
|
|
3106
|
+
}
|
|
3107
|
+
function clear() {
|
|
3108
|
+
for (const id of timeouts.keys()) clearTimeout$1(id);
|
|
3109
|
+
registry.clear();
|
|
3110
|
+
}
|
|
3111
|
+
function dispose() {
|
|
3112
|
+
clear();
|
|
3113
|
+
registry.dispose();
|
|
3114
|
+
}
|
|
3115
|
+
onScopeDispose(dispose, true);
|
|
3116
|
+
function onboard(registrations) {
|
|
3117
|
+
return registry.batch(() => registrations.map((registration) => register(registration)));
|
|
3118
|
+
}
|
|
3119
|
+
return {
|
|
3120
|
+
...registry,
|
|
3121
|
+
register,
|
|
3122
|
+
unregister,
|
|
3123
|
+
onboard,
|
|
3124
|
+
offboard,
|
|
3125
|
+
pause,
|
|
3126
|
+
resume,
|
|
3127
|
+
clear,
|
|
3128
|
+
dispose,
|
|
3129
|
+
get size() {
|
|
3130
|
+
return registry.size;
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
/**
|
|
3135
|
+
* Creates a new queue context.
|
|
3136
|
+
*
|
|
3137
|
+
* @param options The options for the queue context.
|
|
3138
|
+
* @template Z The type of the queue ticket.
|
|
3139
|
+
* @template E The type of the queue context.
|
|
3140
|
+
* @returns A new queue context.
|
|
3141
|
+
*
|
|
3142
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-queue
|
|
3143
|
+
*
|
|
3144
|
+
* @example
|
|
3145
|
+
* ```ts
|
|
3146
|
+
* import { createQueueContext } from '@vuetify/v0'
|
|
3147
|
+
*
|
|
3148
|
+
* export const [useQueue, provideQueue, context] = createQueueContext({
|
|
3149
|
+
* timeout: 5000,
|
|
3150
|
+
* })
|
|
3151
|
+
* ```
|
|
3152
|
+
*/
|
|
3153
|
+
function createQueueContext(_options = {}) {
|
|
3154
|
+
const { namespace = "v0:queue", ...options } = _options;
|
|
3155
|
+
const [useQueueContext, _provideQueueContext] = createContext(namespace);
|
|
3156
|
+
const context = createQueue(options);
|
|
3157
|
+
function provideQueueContext(_context = context, app) {
|
|
3158
|
+
return _provideQueueContext(_context, app);
|
|
3159
|
+
}
|
|
3160
|
+
return createTrinity(useQueueContext, provideQueueContext, context);
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Returns the current queue instance.
|
|
3164
|
+
*
|
|
3165
|
+
* @param namespace The namespace for the queue context. Defaults to `'v0:queue'`.
|
|
3166
|
+
* @returns The current queue instance.
|
|
3167
|
+
*
|
|
3168
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-queue
|
|
3169
|
+
*
|
|
3170
|
+
* @example
|
|
3171
|
+
* ```vue
|
|
3172
|
+
* <script setup lang="ts">
|
|
3173
|
+
* import { useQueue } from '@vuetify/v0'
|
|
3174
|
+
*
|
|
3175
|
+
* const queue = useQueue()
|
|
3176
|
+
* <\/script>
|
|
3177
|
+
* ```
|
|
3178
|
+
*/
|
|
3179
|
+
function useQueue(namespace = "v0:queue") {
|
|
3180
|
+
return useContext(namespace);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
//#endregion
|
|
3184
|
+
//#region src/composables/useStack/index.ts
|
|
3185
|
+
/**
|
|
3186
|
+
* @module useStack
|
|
3187
|
+
*
|
|
3188
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-stack
|
|
3189
|
+
*
|
|
3190
|
+
* @remarks
|
|
3191
|
+
* Overlay z-index stacking composable with:
|
|
3192
|
+
* - Global reactive registry of active overlays
|
|
3193
|
+
* - Automatic z-index calculation based on selection order
|
|
3194
|
+
* - `globalTop` tracking - is this ticket topmost globally?
|
|
3195
|
+
* - Manual select/unselect for explicit control
|
|
3196
|
+
* - Auto-cleanup when registered within component setup
|
|
3197
|
+
*
|
|
3198
|
+
* Built on createSelection for consistent ticket-based management.
|
|
3199
|
+
*/
|
|
3200
|
+
/**
|
|
3201
|
+
* Creates a new stack instance for managing overlay z-indexes.
|
|
3202
|
+
*
|
|
3203
|
+
* @param options Configuration options
|
|
3204
|
+
* @returns Stack context with registry methods and scrim helpers
|
|
3205
|
+
*
|
|
3206
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-stack
|
|
3207
|
+
*
|
|
3208
|
+
* @example
|
|
3209
|
+
* ```ts
|
|
3210
|
+
* import { createStack } from '@vuetify/v0'
|
|
3211
|
+
*
|
|
3212
|
+
* const stack = createStack({ baseZIndex: 1000 })
|
|
3213
|
+
*
|
|
3214
|
+
* const ticket = stack.register({
|
|
3215
|
+
* onDismiss: () => console.log('dismissed'),
|
|
3216
|
+
* blocking: false,
|
|
3217
|
+
* })
|
|
3218
|
+
*
|
|
3219
|
+
* ticket.select() // Activate overlay
|
|
3220
|
+
* console.log(ticket.zIndex.value) // 1000
|
|
3221
|
+
*
|
|
3222
|
+
* ticket.unselect() // Deactivate overlay
|
|
3223
|
+
* ```
|
|
3224
|
+
*/
|
|
3225
|
+
function createStack(_options = {}) {
|
|
3226
|
+
const { baseZIndex = 2e3, increment = 10, ...options } = _options;
|
|
3227
|
+
const selection = createSelection({
|
|
3228
|
+
...options,
|
|
3229
|
+
multiple: true
|
|
3230
|
+
});
|
|
3231
|
+
const isActive = toRef(() => selection.selectedIds.size > 0);
|
|
3232
|
+
const top = toRef(() => {
|
|
3233
|
+
const ids$1 = Array.from(selection.selectedIds);
|
|
3234
|
+
if (ids$1.length === 0) return void 0;
|
|
3235
|
+
return selection.get(ids$1.at(-1));
|
|
3236
|
+
});
|
|
3237
|
+
const scrimZIndex = toRef(() => {
|
|
3238
|
+
const ticket = top.value;
|
|
3239
|
+
return ticket ? ticket.zIndex.value - 1 : 0;
|
|
3240
|
+
});
|
|
3241
|
+
const isBlocking = toRef(() => top.value?.blocking ?? false);
|
|
3242
|
+
function ids() {
|
|
3243
|
+
return Array.from(selection.selectedIds);
|
|
3244
|
+
}
|
|
3245
|
+
function register(input = {}) {
|
|
3246
|
+
const id = input.id ?? /* @__PURE__ */ useId$1();
|
|
3247
|
+
const blocking = input.blocking ?? false;
|
|
3248
|
+
const onDismiss = input.onDismiss;
|
|
3249
|
+
const zIndex = computed(() => {
|
|
3250
|
+
const position = ids().indexOf(id);
|
|
3251
|
+
if (position === -1) return baseZIndex;
|
|
3252
|
+
return baseZIndex + position * increment;
|
|
3253
|
+
});
|
|
3254
|
+
const globalTop = computed(() => {
|
|
3255
|
+
const list = ids();
|
|
3256
|
+
if (list.length === 0) return false;
|
|
3257
|
+
return list.at(-1) === id;
|
|
3258
|
+
});
|
|
3259
|
+
function dismiss() {
|
|
3260
|
+
if (blocking) return;
|
|
3261
|
+
onDismiss?.();
|
|
3262
|
+
selection.unselect(id);
|
|
3263
|
+
}
|
|
3264
|
+
const ticket = selection.register({
|
|
3265
|
+
blocking,
|
|
3266
|
+
onDismiss,
|
|
3267
|
+
zIndex,
|
|
3268
|
+
globalTop,
|
|
3269
|
+
dismiss,
|
|
3270
|
+
...input,
|
|
3271
|
+
id
|
|
3272
|
+
});
|
|
3273
|
+
if (instanceExists()) onScopeDispose(() => {
|
|
3274
|
+
selection.unregister(id);
|
|
3275
|
+
}, true);
|
|
3276
|
+
return ticket;
|
|
3277
|
+
}
|
|
3278
|
+
return {
|
|
3279
|
+
...selection,
|
|
3280
|
+
register,
|
|
3281
|
+
isActive,
|
|
3282
|
+
top,
|
|
3283
|
+
scrimZIndex,
|
|
3284
|
+
isBlocking,
|
|
3285
|
+
get size() {
|
|
3286
|
+
return selection.size;
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Creates a stack context trinity for provide/inject usage.
|
|
3292
|
+
*
|
|
3293
|
+
* @param options Configuration options including namespace
|
|
3294
|
+
* @returns Trinity of [useStackContext, provideStackContext, context]
|
|
3295
|
+
*
|
|
3296
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-stack
|
|
3297
|
+
*
|
|
3298
|
+
* @example
|
|
3299
|
+
* ```ts
|
|
3300
|
+
* import { createStackContext } from '@vuetify/v0'
|
|
3301
|
+
*
|
|
3302
|
+
* const [useMyStack, provideMyStack, myStack] = createStackContext({
|
|
3303
|
+
* namespace: 'my:stack',
|
|
3304
|
+
* baseZIndex: 3000,
|
|
3305
|
+
* })
|
|
3306
|
+
*
|
|
3307
|
+
* // In parent component:
|
|
3308
|
+
* provideMyStack()
|
|
3309
|
+
*
|
|
3310
|
+
* // In child component:
|
|
3311
|
+
* const stack = useMyStack()
|
|
3312
|
+
* ```
|
|
3313
|
+
*/
|
|
3314
|
+
function createStackContext(_options = {}) {
|
|
3315
|
+
const { namespace = "v0:stack", ...options } = _options;
|
|
3316
|
+
const [useStackContext, _provideStackContext] = createContext(namespace);
|
|
3317
|
+
const context = createStack(options);
|
|
3318
|
+
function provideStackContext(_context = context, app) {
|
|
3319
|
+
return _provideStackContext(_context, app);
|
|
3320
|
+
}
|
|
3321
|
+
return createTrinity(useStackContext, provideStackContext, context);
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Creates a Vue plugin to provide stack context at app level.
|
|
3325
|
+
*
|
|
3326
|
+
* @param options Configuration options
|
|
3327
|
+
* @returns Vue plugin instance
|
|
3328
|
+
*
|
|
3329
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-stack
|
|
3330
|
+
*
|
|
3331
|
+
* @example
|
|
3332
|
+
* ```ts
|
|
3333
|
+
* import { createApp } from 'vue'
|
|
3334
|
+
* import { createStackPlugin } from '@vuetify/v0'
|
|
3335
|
+
*
|
|
3336
|
+
* const app = createApp(App)
|
|
3337
|
+
* app.use(createStackPlugin({ baseZIndex: 2000 }))
|
|
3338
|
+
* app.mount('#app')
|
|
3339
|
+
* ```
|
|
3340
|
+
*/
|
|
3341
|
+
function createStackPlugin(_options = {}) {
|
|
3342
|
+
const { namespace = "v0:stack", ...options } = _options;
|
|
3343
|
+
const [, provideStackContext, context] = createStackContext({
|
|
3344
|
+
...options,
|
|
3345
|
+
namespace
|
|
3346
|
+
});
|
|
3347
|
+
return createPlugin({
|
|
3348
|
+
namespace,
|
|
3349
|
+
provide: (app) => {
|
|
3350
|
+
provideStackContext(context, app);
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
let fallbackStack;
|
|
3355
|
+
function getStackFallback() {
|
|
3356
|
+
if (!fallbackStack) fallbackStack = createStack();
|
|
3357
|
+
return fallbackStack;
|
|
3358
|
+
}
|
|
3359
|
+
/**
|
|
3360
|
+
* Returns the current stack context.
|
|
3361
|
+
*
|
|
3362
|
+
* @param namespace The namespace for the stack context. Defaults to 'v0:stack'.
|
|
3363
|
+
* @returns The stack context instance
|
|
3364
|
+
*
|
|
3365
|
+
* @remarks Falls back to an internal shared instance if no provider exists.
|
|
3366
|
+
* For SSR safety, use createStackPlugin to provide isolated context per app.
|
|
3367
|
+
*
|
|
3368
|
+
* Tickets registered within component setup are automatically unregistered
|
|
3369
|
+
* when the component is unmounted.
|
|
3370
|
+
*
|
|
3371
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-stack
|
|
3372
|
+
*
|
|
3373
|
+
* @example
|
|
3374
|
+
* ```vue
|
|
3375
|
+
* <script setup lang="ts">
|
|
3376
|
+
* import { shallowRef, watch } from 'vue'
|
|
3377
|
+
* import { useStack } from '@vuetify/v0'
|
|
3378
|
+
*
|
|
3379
|
+
* const stack = useStack()
|
|
3380
|
+
* const isOpen = shallowRef(false)
|
|
3381
|
+
*
|
|
3382
|
+
* // Ticket is auto-cleaned up when component unmounts
|
|
3383
|
+
* const ticket = stack.register({
|
|
3384
|
+
* onDismiss: () => { isOpen.value = false },
|
|
3385
|
+
* })
|
|
3386
|
+
*
|
|
3387
|
+
* // Sync selection state with isOpen
|
|
3388
|
+
* watch(isOpen, v => v ? ticket.select() : ticket.unselect())
|
|
3389
|
+
* <\/script>
|
|
3390
|
+
*
|
|
3391
|
+
* <template>
|
|
3392
|
+
* <button @click="isOpen = true">Open</button>
|
|
3393
|
+
* <div v-if="isOpen" :style="{ zIndex: ticket.zIndex.value }">
|
|
3394
|
+
* <p>Dialog content</p>
|
|
3395
|
+
* <button @click="ticket.dismiss()">Close</button>
|
|
3396
|
+
* </div>
|
|
3397
|
+
* </template>
|
|
3398
|
+
* ```
|
|
3399
|
+
*/
|
|
3400
|
+
function useStack(namespace = "v0:stack") {
|
|
3401
|
+
const fallback = getStackFallback();
|
|
3402
|
+
if (!instanceExists()) return fallback;
|
|
3403
|
+
try {
|
|
3404
|
+
return useContext(namespace);
|
|
3405
|
+
} catch {
|
|
3406
|
+
return fallback;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
//#endregion
|
|
3411
|
+
//#region src/composables/createStep/index.ts
|
|
3412
|
+
/**
|
|
3413
|
+
* @module useStep
|
|
3414
|
+
*
|
|
3415
|
+
* @remarks
|
|
3416
|
+
* Navigation composable that extends useSingle with first/last/next/prev/step methods.
|
|
3417
|
+
*
|
|
3418
|
+
* Key features:
|
|
3419
|
+
* - Configurable circular or bounded navigation
|
|
3420
|
+
* - Automatic disabled item skipping
|
|
3421
|
+
* - Arbitrary step counts (positive/negative)
|
|
3422
|
+
* - Perfect for wizards, carousels, pagination, onboarding flows
|
|
3423
|
+
*
|
|
3424
|
+
* Inheritance chain: useRegistry → useSelection → useSingle → useStep
|
|
3425
|
+
*/
|
|
3426
|
+
/**
|
|
3427
|
+
* Creates a new step instance with navigation through items.
|
|
3428
|
+
*
|
|
3429
|
+
* Extends `createSingle` with `first()`, `last()`, `next()`, `prev()`, and `step(count)` methods
|
|
3430
|
+
* for sequential navigation. Supports both circular (wrapping) and bounded (stopping at edges) modes.
|
|
3431
|
+
*
|
|
3432
|
+
* @param options The options for the step instance.
|
|
3433
|
+
* @template Z The type of the step ticket.
|
|
3434
|
+
* @template E The type of the step context.
|
|
3435
|
+
* @returns A new step instance with navigation methods.
|
|
3436
|
+
*
|
|
3437
|
+
* @remarks
|
|
3438
|
+
* **Key Features:**
|
|
3439
|
+
* - **Configurable Navigation**: `circular: true` for wrapping, `false` for bounded (default: false)
|
|
3440
|
+
* - **Disabled Item Skipping**: Automatically skips disabled items during navigation
|
|
3441
|
+
* - **Bidirectional**: Forward (`next`, positive `step`) and backward (`prev`, negative `step`)
|
|
3442
|
+
* - **Safe Edge Cases**: Handles empty registries and all-disabled scenarios gracefully
|
|
3443
|
+
*
|
|
3444
|
+
* **Navigation Methods:**
|
|
3445
|
+
* - `first()`: Select first non-disabled item
|
|
3446
|
+
* - `last()`: Select last non-disabled item
|
|
3447
|
+
* - `next()`: Move to next item (wraps if circular, stops at end if bounded)
|
|
3448
|
+
* - `prev()`: Move to previous item (wraps if circular, stops at start if bounded)
|
|
3449
|
+
* - `step(count)`: Move by `count` positions (negative for backward)
|
|
3450
|
+
*
|
|
3451
|
+
* **Circular Mode (`circular: true`):**
|
|
3452
|
+
* - Uses modulo arithmetic for wrapping: `((index % length) + length) % length`
|
|
3453
|
+
* - Works correctly with negative indexes and large step counts
|
|
3454
|
+
* - Perfect for carousels, theme switchers, infinite scrolling
|
|
3455
|
+
*
|
|
3456
|
+
* **Bounded Mode (`circular: false`, default):**
|
|
3457
|
+
* - Navigation stops at boundaries (no wrapping)
|
|
3458
|
+
* - `next()` on last item does nothing
|
|
3459
|
+
* - `prev()` on first item does nothing
|
|
3460
|
+
* - Perfect for pagination, wizards with explicit completion, forms
|
|
3461
|
+
*
|
|
3462
|
+
* **Inheritance Chain:**
|
|
3463
|
+
* `useRegistry` → `createSelection` → `createSingle` → `createStep`
|
|
3464
|
+
*
|
|
3465
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-step
|
|
3466
|
+
*
|
|
3467
|
+
* @example
|
|
3468
|
+
* ```ts
|
|
3469
|
+
* import { createStep } from '@vuetify/v0'
|
|
3470
|
+
*
|
|
3471
|
+
* // Bounded navigation (default) - for pagination
|
|
3472
|
+
* const pagination = createStep({ circular: false })
|
|
3473
|
+
* pagination.onboard([
|
|
3474
|
+
* { id: 'page-1', value: 1 },
|
|
3475
|
+
* { id: 'page-2', value: 2 },
|
|
3476
|
+
* { id: 'page-3', value: 3 },
|
|
3477
|
+
* ])
|
|
3478
|
+
* pagination.first() // Select page 1
|
|
3479
|
+
* pagination.prev() // Does nothing (already at first)
|
|
3480
|
+
* pagination.next() // Select page 2
|
|
3481
|
+
*
|
|
3482
|
+
* // Circular navigation - for carousels
|
|
3483
|
+
* const carousel = createStep({ circular: true })
|
|
3484
|
+
* carousel.onboard([
|
|
3485
|
+
* { id: 'slide-1', value: 'First' },
|
|
3486
|
+
* { id: 'slide-2', value: 'Second' },
|
|
3487
|
+
* { id: 'slide-3', value: 'Third' },
|
|
3488
|
+
* ])
|
|
3489
|
+
* carousel.first()
|
|
3490
|
+
* carousel.prev() // Wraps to 'slide-3'
|
|
3491
|
+
* carousel.next() // Wraps to 'slide-1'
|
|
3492
|
+
* ```
|
|
3493
|
+
*/
|
|
3494
|
+
function createStep(_options = {}) {
|
|
3495
|
+
const { circular = false, ...options } = _options;
|
|
3496
|
+
const registry = createSingle(options);
|
|
3497
|
+
function first() {
|
|
3498
|
+
const ticket = registry.seek("first");
|
|
3499
|
+
if (ticket) registry.select(ticket.id);
|
|
3500
|
+
}
|
|
3501
|
+
function last() {
|
|
3502
|
+
const ticket = registry.seek("last");
|
|
3503
|
+
if (ticket) registry.select(ticket.id);
|
|
3504
|
+
}
|
|
3505
|
+
function next() {
|
|
3506
|
+
step(1);
|
|
3507
|
+
}
|
|
3508
|
+
function prev() {
|
|
3509
|
+
step(-1);
|
|
3510
|
+
}
|
|
3511
|
+
function wrapped(length, index) {
|
|
3512
|
+
return (index % length + length) % length;
|
|
3513
|
+
}
|
|
3514
|
+
function step(count = 1) {
|
|
3515
|
+
const length = registry.size;
|
|
3516
|
+
if (!length) return;
|
|
3517
|
+
const currentIndex = registry.selectedIndex.value;
|
|
3518
|
+
const direction = Math.sign(count || 1);
|
|
3519
|
+
let hops = 0;
|
|
3520
|
+
let index = circular ? wrapped(length, currentIndex + count) : currentIndex + count;
|
|
3521
|
+
if (!circular && (index < 0 || index >= length)) return;
|
|
3522
|
+
let id = registry.lookup(index);
|
|
3523
|
+
while (!/* @__PURE__ */ isUndefined(id) && toValue(registry.get(id)?.disabled) && hops < length) {
|
|
3524
|
+
index = circular ? wrapped(length, index + direction) : index + direction;
|
|
3525
|
+
if (!circular && (index < 0 || index >= length)) return;
|
|
3526
|
+
id = registry.lookup(index);
|
|
3527
|
+
hops++;
|
|
3528
|
+
}
|
|
3529
|
+
if (/* @__PURE__ */ isUndefined(id) || hops === length) return;
|
|
3530
|
+
registry.selectedIds.clear();
|
|
3531
|
+
registry.select(id);
|
|
3532
|
+
}
|
|
3533
|
+
return {
|
|
3534
|
+
...registry,
|
|
3535
|
+
first,
|
|
3536
|
+
last,
|
|
3537
|
+
next,
|
|
3538
|
+
prev,
|
|
3539
|
+
step,
|
|
3540
|
+
get size() {
|
|
3541
|
+
return registry.size;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
/**
|
|
3546
|
+
* Creates a new step context.
|
|
3547
|
+
*
|
|
3548
|
+
* @param options The options for the step context.
|
|
3549
|
+
* @template Z The input ticket type.
|
|
3550
|
+
* @template E The output ticket type.
|
|
3551
|
+
* @template R The context type.
|
|
3552
|
+
* @returns A new step context.
|
|
3553
|
+
*
|
|
3554
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-step
|
|
3555
|
+
*
|
|
3556
|
+
* @example
|
|
3557
|
+
* ```ts
|
|
3558
|
+
* import { createStepContext } from '@vuetify/v0'
|
|
3559
|
+
*
|
|
3560
|
+
* // With default namespace 'v0:step'
|
|
3561
|
+
* export const [useStep, provideStep, context] = createStepContext()
|
|
3562
|
+
*
|
|
3563
|
+
* // In a parent component:
|
|
3564
|
+
* provideStep()
|
|
3565
|
+
*
|
|
3566
|
+
* // In a child component:
|
|
3567
|
+
* const context = useStep()
|
|
3568
|
+
* context.next() // Progress to next step
|
|
3569
|
+
* ```
|
|
3570
|
+
*/
|
|
3571
|
+
function createStepContext(_options = {}) {
|
|
3572
|
+
const { namespace = "v0:step", ...options } = _options;
|
|
3573
|
+
const [useStepContext, _provideStepContext] = createContext(namespace);
|
|
3574
|
+
const context = createStep(options);
|
|
3575
|
+
function provideStepContext(_context = context, app) {
|
|
3576
|
+
return _provideStepContext(_context, app);
|
|
3577
|
+
}
|
|
3578
|
+
return createTrinity(useStepContext, provideStepContext, context);
|
|
3579
|
+
}
|
|
3580
|
+
/**
|
|
3581
|
+
* Returns the current step instance.
|
|
3582
|
+
*
|
|
3583
|
+
* @param namespace The namespace for the step context. Defaults to `'v0:step'`.
|
|
3584
|
+
* @template Z The input ticket type.
|
|
3585
|
+
* @template E The output ticket type.
|
|
3586
|
+
* @template R The context type.
|
|
3587
|
+
* @returns The current step instance.
|
|
3588
|
+
*
|
|
3589
|
+
* @see https://0.vuetifyjs.com/composables/selection/use-step
|
|
3590
|
+
*
|
|
3591
|
+
* @example
|
|
3592
|
+
* ```vue
|
|
3593
|
+
* <script setup lang="ts">
|
|
3594
|
+
* import { useStep } from '@vuetify/v0'
|
|
3595
|
+
*
|
|
3596
|
+
* const wizard = useStep()
|
|
3597
|
+
* <\/script>
|
|
3598
|
+
*
|
|
3599
|
+
* <template>
|
|
3600
|
+
* <div>
|
|
3601
|
+
* <p>Current step: {{ wizard.selectedIndex }}</p>
|
|
3602
|
+
* <button @click="wizard.next()">Next</button>
|
|
3603
|
+
* </div>
|
|
3604
|
+
* </template>
|
|
3605
|
+
* ```
|
|
3606
|
+
*/
|
|
3607
|
+
function useStep(namespace = "v0:step") {
|
|
3608
|
+
return useContext(namespace);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
//#endregion
|
|
3612
|
+
//#region src/composables/useStorage/index.ts
|
|
3613
|
+
/**
|
|
3614
|
+
* @module useStorage
|
|
3615
|
+
*
|
|
3616
|
+
* @remarks
|
|
3617
|
+
* Reactive storage composable with adapter pattern for localStorage, sessionStorage, or memory.
|
|
3618
|
+
*
|
|
3619
|
+
* Key features:
|
|
3620
|
+
* - Reactive refs that sync with storage
|
|
3621
|
+
* - localStorage, sessionStorage, and memory adapters
|
|
3622
|
+
* - Custom serialization support
|
|
3623
|
+
* - SSR fallback to memory adapter
|
|
3624
|
+
* - Automatic cleanup on remove/clear
|
|
3625
|
+
*
|
|
3626
|
+
* Uses adapter pattern to abstract storage implementation details.
|
|
3627
|
+
*/
|
|
3628
|
+
/**
|
|
3629
|
+
* Creates a new storage instance.
|
|
3630
|
+
*
|
|
3631
|
+
* @param options The options for the storage instance.
|
|
3632
|
+
* @template E The type of the storage context.
|
|
3633
|
+
* @returns A new storage instance.
|
|
3634
|
+
*
|
|
3635
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-storage
|
|
3636
|
+
*
|
|
3637
|
+
* @example
|
|
3638
|
+
* ```ts
|
|
3639
|
+
* import { createStorage } from '@vuetify/v0'
|
|
3640
|
+
*
|
|
3641
|
+
* const storage = createStorage()
|
|
3642
|
+
*
|
|
3643
|
+
* storage.set('username', 'MyUsername')
|
|
3644
|
+
*
|
|
3645
|
+
* const username = storage.get('username')
|
|
3646
|
+
*
|
|
3647
|
+
* console.log(username.value) // MyUsername
|
|
3648
|
+
*
|
|
3649
|
+
* storage.clear()
|
|
3650
|
+
* ```
|
|
3651
|
+
*/
|
|
3652
|
+
function createStorage(options = {}) {
|
|
3653
|
+
const { adapter = IN_BROWSER ? window.localStorage : new MemoryAdapter(), prefix = "v0:", serializer = {
|
|
3654
|
+
read: JSON.parse,
|
|
3655
|
+
write: JSON.stringify
|
|
3656
|
+
} } = options;
|
|
3657
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3658
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
3659
|
+
function has(key) {
|
|
3660
|
+
const prefixedKey = `${prefix}${key}`;
|
|
3661
|
+
return cache.has(prefixedKey);
|
|
3662
|
+
}
|
|
3663
|
+
function get(key, defaultValue) {
|
|
3664
|
+
const prefixedKey = `${prefix}${key}`;
|
|
3665
|
+
if (cache.has(prefixedKey)) return cache.get(prefixedKey);
|
|
3666
|
+
const storedValue = adapter?.getItem(prefixedKey);
|
|
3667
|
+
let initialValue = defaultValue;
|
|
3668
|
+
if (storedValue) try {
|
|
3669
|
+
initialValue = serializer.read(storedValue);
|
|
3670
|
+
} catch (error) {
|
|
3671
|
+
console.error(`[v0:storage] Failed to parse stored value for key "${prefixedKey}":`, error);
|
|
3672
|
+
}
|
|
3673
|
+
const valueRef = ref(initialValue);
|
|
3674
|
+
const stop = watch(valueRef, (newValue) => {
|
|
3675
|
+
if (/* @__PURE__ */ isNullOrUndefined(newValue)) adapter?.removeItem(prefixedKey);
|
|
3676
|
+
else try {
|
|
3677
|
+
adapter?.setItem(prefixedKey, serializer.write(newValue));
|
|
3678
|
+
} catch (error) {
|
|
3679
|
+
console.error(`[v0:storage] Failed to write key "${prefixedKey}":`, error);
|
|
3680
|
+
}
|
|
3681
|
+
}, { deep: true });
|
|
3682
|
+
watchers.set(prefixedKey, stop);
|
|
3683
|
+
cache.set(prefixedKey, valueRef);
|
|
3684
|
+
return valueRef;
|
|
3685
|
+
}
|
|
3686
|
+
function set(key, value) {
|
|
3687
|
+
const valueRef = get(key);
|
|
3688
|
+
valueRef.value = value;
|
|
3689
|
+
}
|
|
3690
|
+
function remove(key) {
|
|
3691
|
+
const prefixedKey = `${prefix}${key}`;
|
|
3692
|
+
const stop = watchers.get(prefixedKey);
|
|
3693
|
+
if (!stop) return;
|
|
3694
|
+
stop();
|
|
3695
|
+
watchers.delete(prefixedKey);
|
|
3696
|
+
adapter?.removeItem(prefixedKey);
|
|
3697
|
+
cache.delete(prefixedKey);
|
|
3698
|
+
}
|
|
3699
|
+
function clear() {
|
|
3700
|
+
if (watchers.size > 0) {
|
|
3701
|
+
for (const stop of watchers.values()) stop();
|
|
3702
|
+
watchers.clear();
|
|
3703
|
+
}
|
|
3704
|
+
if (cache.size > 0) {
|
|
3705
|
+
for (const key of cache.keys()) adapter?.removeItem(key);
|
|
3706
|
+
cache.clear();
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
if (IN_BROWSER && adapter === window.localStorage) useWindowEventListener("storage", (e) => {
|
|
3710
|
+
if (!e.key?.startsWith(prefix)) return;
|
|
3711
|
+
const valueRef = cache.get(e.key);
|
|
3712
|
+
if (!valueRef) return;
|
|
3713
|
+
watchers.get(e.key)?.();
|
|
3714
|
+
valueRef.value = e.newValue == null ? void 0 : serializer.read(e.newValue);
|
|
3715
|
+
const stop = watch(valueRef, (newValue) => {
|
|
3716
|
+
if (/* @__PURE__ */ isNullOrUndefined(newValue)) adapter?.removeItem(e.key);
|
|
3717
|
+
else try {
|
|
3718
|
+
adapter?.setItem(e.key, serializer.write(newValue));
|
|
3719
|
+
} catch (error) {
|
|
3720
|
+
console.error(`[v0:storage] Failed to write key "${e.key}":`, error);
|
|
3721
|
+
}
|
|
3722
|
+
}, { deep: true });
|
|
3723
|
+
watchers.set(e.key, stop);
|
|
3724
|
+
});
|
|
3725
|
+
return {
|
|
3726
|
+
has,
|
|
3727
|
+
get,
|
|
3728
|
+
set,
|
|
3729
|
+
remove,
|
|
3730
|
+
clear
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
function createStorageFallback() {
|
|
3734
|
+
return createStorage({ adapter: new MemoryAdapter() });
|
|
3735
|
+
}
|
|
3736
|
+
const [createStorageContext, createStoragePlugin, useStorage] = createPluginContext("v0:storage", (options) => createStorage(options), { fallback: () => createStorageFallback() });
|
|
3737
|
+
|
|
3738
|
+
//#endregion
|
|
3739
|
+
//#region src/composables/createTimeline/index.ts
|
|
3740
|
+
/**
|
|
3741
|
+
* @module createTimeline
|
|
3742
|
+
*
|
|
3743
|
+
* @remarks
|
|
3744
|
+
* Bounded undo/redo system with overflow management.
|
|
3745
|
+
*
|
|
3746
|
+
* Key features:
|
|
3747
|
+
* - Fixed-size history (default: 10 items)
|
|
3748
|
+
* - Undo/redo stack management
|
|
3749
|
+
* - Overflow queue (preserves oldest items)
|
|
3750
|
+
* - Automatic reindexing after operations
|
|
3751
|
+
* - Perfect for command pattern, history tracking
|
|
3752
|
+
*
|
|
3753
|
+
* Extends createRegistry with temporal navigation capabilities.
|
|
3754
|
+
*/
|
|
3755
|
+
/**
|
|
3756
|
+
* Creates a new timeline instance.
|
|
3757
|
+
*
|
|
3758
|
+
* @param _options The options for the timeline instance.
|
|
3759
|
+
* @template Z The type of the timeline ticket.
|
|
3760
|
+
* @template E The type of the timeline context.
|
|
3761
|
+
* @returns A new timeline instance.
|
|
3762
|
+
*
|
|
3763
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
3764
|
+
*
|
|
3765
|
+
* @example
|
|
3766
|
+
* ```ts
|
|
3767
|
+
* import { useTimeline } from '@vuetify/v0'
|
|
3768
|
+
*
|
|
3769
|
+
* const timeline = useTimeline({ size: 3 })
|
|
3770
|
+
*
|
|
3771
|
+
* timeline.onboard([{ id: 'one' }, { id: 'two' }, { id: 'three' }])
|
|
3772
|
+
*
|
|
3773
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }, { id: 'three' }]
|
|
3774
|
+
*
|
|
3775
|
+
* timeline.undo()
|
|
3776
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }]
|
|
3777
|
+
*
|
|
3778
|
+
* timeline.redo()
|
|
3779
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }, { id: 'three' }]
|
|
3780
|
+
* ```
|
|
3781
|
+
*/
|
|
3782
|
+
function createTimeline(_options = {}) {
|
|
3783
|
+
const { size = 10, ...options } = _options;
|
|
3784
|
+
const registry = createRegistry(options);
|
|
3785
|
+
const stack = [];
|
|
3786
|
+
const overflow = [];
|
|
3787
|
+
function register(item) {
|
|
3788
|
+
stack.length = 0;
|
|
3789
|
+
if (registry.size < size) return registry.register({ ...item });
|
|
3790
|
+
const removing = registry.seek("first");
|
|
3791
|
+
if (overflow.length === size) overflow.shift();
|
|
3792
|
+
overflow.push(removing);
|
|
3793
|
+
registry.unregister(removing.id);
|
|
3794
|
+
const ticket = registry.register({ ...item });
|
|
3795
|
+
registry.reindex();
|
|
3796
|
+
return ticket;
|
|
3797
|
+
}
|
|
3798
|
+
function undo() {
|
|
3799
|
+
const item = registry.seek("last");
|
|
3800
|
+
if (!item) return void 0;
|
|
3801
|
+
stack.push(item);
|
|
3802
|
+
registry.unregister(item.id);
|
|
3803
|
+
const restored = overflow.pop();
|
|
3804
|
+
if (restored) {
|
|
3805
|
+
const remaining = registry.values();
|
|
3806
|
+
registry.clear();
|
|
3807
|
+
registry.onboard([restored, ...remaining]);
|
|
3808
|
+
registry.reindex();
|
|
3809
|
+
}
|
|
3810
|
+
return item;
|
|
3811
|
+
}
|
|
3812
|
+
function redo() {
|
|
3813
|
+
if (stack.length === 0) return void 0;
|
|
3814
|
+
const item = stack.pop();
|
|
3815
|
+
const ticket = registry.register(item);
|
|
3816
|
+
registry.reindex();
|
|
3817
|
+
return ticket;
|
|
3818
|
+
}
|
|
3819
|
+
return {
|
|
3820
|
+
...registry,
|
|
3821
|
+
register,
|
|
3822
|
+
undo,
|
|
3823
|
+
redo,
|
|
3824
|
+
get size() {
|
|
3825
|
+
return registry.size;
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Creates a new timeline context.
|
|
3831
|
+
*
|
|
3832
|
+
* @param options The options for the timeline context.
|
|
3833
|
+
* @template Z The type of the timeline ticket.
|
|
3834
|
+
* @template E The type of the timeline context.
|
|
3835
|
+
* @returns A new timeline context.
|
|
3836
|
+
*
|
|
3837
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
3838
|
+
*
|
|
3839
|
+
* @example
|
|
3840
|
+
* ```ts
|
|
3841
|
+
* import { createTimelineContext } from '@vuetify/v0'
|
|
3842
|
+
*
|
|
3843
|
+
* // With default namespace 'v0:timeline'
|
|
3844
|
+
* export const [useTimeline, provideTimeline, context] = createTimelineContext({ size: 5 })
|
|
3845
|
+
*
|
|
3846
|
+
* context.register({ id: 'example' })
|
|
3847
|
+
*
|
|
3848
|
+
* // In a parent component
|
|
3849
|
+
* provideTimeline()
|
|
3850
|
+
*
|
|
3851
|
+
* // In a child component
|
|
3852
|
+
* const timeline = useTimeline()
|
|
3853
|
+
*
|
|
3854
|
+
* console.log(timeline.values()) // [{ id: 'example' }]
|
|
3855
|
+
* ```
|
|
3856
|
+
*/
|
|
3857
|
+
function createTimelineContext(_options = {}) {
|
|
3858
|
+
const { namespace = "v0:timeline", ...options } = _options;
|
|
3859
|
+
const [useTimelineContext, _provideTimelineContext] = createContext(namespace);
|
|
3860
|
+
const context = createTimeline(options);
|
|
3861
|
+
function provideTimelineContext(_context = context, app) {
|
|
3862
|
+
return _provideTimelineContext(_context, app);
|
|
3863
|
+
}
|
|
3864
|
+
return createTrinity(useTimelineContext, provideTimelineContext, context);
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Returns the current timeline instance.
|
|
3868
|
+
*
|
|
3869
|
+
* @param namespace The namespace for the timeline context. Defaults to `'v0:timeline'`.
|
|
3870
|
+
* @returns The current timeline instance.
|
|
3871
|
+
*
|
|
3872
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
3873
|
+
*
|
|
3874
|
+
* @example
|
|
3875
|
+
* ```vue
|
|
3876
|
+
* <script setup lang="ts">
|
|
3877
|
+
* import { useTimeline } from '@vuetify/v0'
|
|
3878
|
+
*
|
|
3879
|
+
* const timeline = useTimeline()
|
|
3880
|
+
* <\/script>
|
|
3881
|
+
* ```
|
|
3882
|
+
*/
|
|
3883
|
+
function useTimeline(namespace = "v0:timeline") {
|
|
3884
|
+
return useContext(namespace);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
//#endregion
|
|
3888
|
+
//#region src/composables/useToggleScope/index.ts
|
|
3889
|
+
/**
|
|
3890
|
+
* @module useToggleScope
|
|
3891
|
+
*
|
|
3892
|
+
* @remarks
|
|
3893
|
+
* Conditionally manages an effect scope based on a reactive boolean condition.
|
|
3894
|
+
* When the source becomes true, creates and runs an effect scope. When false, stops the scope.
|
|
3895
|
+
* All reactive effects created within the scoped function are automatically cleaned up on deactivation.
|
|
3896
|
+
*
|
|
3897
|
+
* Key features:
|
|
3898
|
+
* - Uses Vue's effectScope for efficient reactive effect lifecycle management
|
|
3899
|
+
* - Automatic cleanup when condition becomes false
|
|
3900
|
+
* - Supports optional reset callback for scope restart capability
|
|
3901
|
+
* - Handles rapid toggling and parent scope disposal safely
|
|
3902
|
+
* - SSR-safe (effectScope is part of Vue core)
|
|
3903
|
+
*
|
|
3904
|
+
* Perfect for conditional side effects, feature flags, and performance optimization
|
|
3905
|
+
* by only running reactive effects when needed.
|
|
3906
|
+
*/
|
|
3907
|
+
/**
|
|
3908
|
+
* Conditionally manages an effect scope based on a reactive boolean source.
|
|
3909
|
+
*
|
|
3910
|
+
* @param source A reactive boolean value or getter that controls the scope lifecycle
|
|
3911
|
+
* @param fn The function to run within the effect scope. Can optionally receive controls for manual scope management.
|
|
3912
|
+
* @returns Controls object with isActive state and start/stop/reset methods
|
|
3913
|
+
*
|
|
3914
|
+
* @see https://vuejs.org/api/reactivity-advanced.html#effectscope
|
|
3915
|
+
* @see https://0.vuetifyjs.com/composables/system/use-toggle-scope
|
|
3916
|
+
*
|
|
3917
|
+
* @example
|
|
3918
|
+
* ```ts
|
|
3919
|
+
* import { ref } from 'vue'
|
|
3920
|
+
* import { useToggleScope } from '@vuetify/v0'
|
|
3921
|
+
*
|
|
3922
|
+
* const isEnabled = ref(false)
|
|
3923
|
+
*
|
|
3924
|
+
* const { isActive } = useToggleScope(isEnabled, () => {
|
|
3925
|
+
* // This code runs when isEnabled becomes true
|
|
3926
|
+
* const unwatch = watch(someRef, () => {
|
|
3927
|
+
* console.log('Watching...')
|
|
3928
|
+
* })
|
|
3929
|
+
*
|
|
3930
|
+
* // Cleanup happens automatically when isEnabled becomes false
|
|
3931
|
+
* })
|
|
3932
|
+
*
|
|
3933
|
+
* // Toggle the scope on/off
|
|
3934
|
+
* isEnabled.value = true // Starts the scope
|
|
3935
|
+
* console.log(isActive.value) // true
|
|
3936
|
+
* isEnabled.value = false // Stops and cleans up
|
|
3937
|
+
* console.log(isActive.value) // false
|
|
3938
|
+
* ```
|
|
3939
|
+
*
|
|
3940
|
+
* @example
|
|
3941
|
+
* With manual controls:
|
|
3942
|
+
* ```ts
|
|
3943
|
+
* const isEnabled = ref(true)
|
|
3944
|
+
*
|
|
3945
|
+
* useToggleScope(isEnabled, (controls) => {
|
|
3946
|
+
* // Access controls inside the scope
|
|
3947
|
+
* console.log('Active:', controls.isActive.value)
|
|
3948
|
+
*
|
|
3949
|
+
* // Manually reset the scope if needed
|
|
3950
|
+
* someEvent.on('reset', () => controls.reset())
|
|
3951
|
+
* })
|
|
3952
|
+
* ```
|
|
3953
|
+
*/
|
|
3954
|
+
function useToggleScope(source, fn) {
|
|
3955
|
+
const scope = shallowRef();
|
|
3956
|
+
const isActive = toRef(() => !!scope.value);
|
|
3957
|
+
function start() {
|
|
3958
|
+
if (scope.value) return;
|
|
3959
|
+
scope.value = effectScope();
|
|
3960
|
+
scope.value.run(() => fn.length > 0 ? fn(controls) : fn());
|
|
3961
|
+
}
|
|
3962
|
+
function stop() {
|
|
3963
|
+
scope.value?.stop();
|
|
3964
|
+
scope.value = void 0;
|
|
3965
|
+
}
|
|
3966
|
+
function reset() {
|
|
3967
|
+
stop();
|
|
3968
|
+
start();
|
|
3969
|
+
}
|
|
3970
|
+
const controls = {
|
|
3971
|
+
isActive: shallowReadonly(isActive),
|
|
3972
|
+
start,
|
|
3973
|
+
stop,
|
|
3974
|
+
reset
|
|
3975
|
+
};
|
|
3976
|
+
watch(source, (active) => {
|
|
3977
|
+
if (active && !scope.value) start();
|
|
3978
|
+
else if (!active) stop();
|
|
3979
|
+
}, { immediate: true });
|
|
3980
|
+
onScopeDispose(stop, true);
|
|
3981
|
+
return controls;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
//#endregion
|
|
3985
|
+
//#region src/composables/createVirtual/index.ts
|
|
3986
|
+
/**
|
|
3987
|
+
* @module createVirtual
|
|
3988
|
+
*
|
|
3989
|
+
* @remarks
|
|
3990
|
+
* Virtual scrolling composable for efficiently rendering large lists.
|
|
3991
|
+
*
|
|
3992
|
+
* Key features:
|
|
3993
|
+
* - Renders only visible items (viewport + overscan)
|
|
3994
|
+
* - Dynamic or fixed item heights
|
|
3995
|
+
* - SSR-safe (checks IN_BROWSER)
|
|
3996
|
+
* - Bidirectional scrolling (forward/reverse for chat apps)
|
|
3997
|
+
* - Scroll anchoring (maintains position across data changes)
|
|
3998
|
+
* - Edge detection for infinite scroll
|
|
3999
|
+
* - iOS momentum and elastic scrolling
|
|
4000
|
+
* - Configurable overscan (extra items rendered for smooth scrolling)
|
|
4001
|
+
*
|
|
4002
|
+
* Perfect for large data sets, chat apps, and infinite scroll implementations.
|
|
4003
|
+
*/
|
|
4004
|
+
/**
|
|
4005
|
+
* Virtual scrolling composable for efficiently rendering large lists
|
|
4006
|
+
*
|
|
4007
|
+
* @param items Reactive array of items to virtualize
|
|
4008
|
+
* @param options Configuration options
|
|
4009
|
+
* @returns Virtual scrolling context
|
|
4010
|
+
*
|
|
4011
|
+
* @see https://0.vuetifyjs.com/composables/utilities/create-virtual
|
|
4012
|
+
*
|
|
4013
|
+
* @example
|
|
4014
|
+
* ```vue
|
|
4015
|
+
* <script setup lang="ts">
|
|
4016
|
+
* import { createVirtual } from '@vuetify/v0'
|
|
4017
|
+
*
|
|
4018
|
+
* const items = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })))
|
|
4019
|
+
* const virtual = createVirtual(items)
|
|
4020
|
+
* <\/script>
|
|
4021
|
+
*
|
|
4022
|
+
* <template>
|
|
4023
|
+
* <div
|
|
4024
|
+
* ref="virtual.element"
|
|
4025
|
+
* style="height: 600px; overflow-y: auto;"
|
|
4026
|
+
* @scroll="virtual.scroll"
|
|
4027
|
+
* >
|
|
4028
|
+
* <div :style="{ height: `${virtual.offset.value}px` }" />
|
|
4029
|
+
*
|
|
4030
|
+
* <div v-for="item in virtual.items.value" :key="item.index">
|
|
4031
|
+
* {{ item.raw.name }}
|
|
4032
|
+
* </div>
|
|
4033
|
+
*
|
|
4034
|
+
* <div :style="{ height: `${virtual.size.value}px` }" />
|
|
4035
|
+
* </div>
|
|
4036
|
+
* </template>
|
|
4037
|
+
* ```
|
|
4038
|
+
*/
|
|
4039
|
+
function createVirtual(items, _options = {}) {
|
|
4040
|
+
const { itemHeight: _itemHeight, height, overscan = 5, direction = "forward", anchor = "auto", anchorSmooth = true, onStartReached, onEndReached, startThreshold = 0, endThreshold = 0, momentum: momentumOption, elastic: elasticOption } = _options;
|
|
4041
|
+
const element = ref();
|
|
4042
|
+
const itemHeight = shallowRef(Number.parseFloat(String(_itemHeight || 0)));
|
|
4043
|
+
const heights = shallowRef([]);
|
|
4044
|
+
const offsets = shallowRef([]);
|
|
4045
|
+
const first = shallowRef(0);
|
|
4046
|
+
const last = shallowRef(0);
|
|
4047
|
+
const offset = shallowRef(0);
|
|
4048
|
+
const size = shallowRef(0);
|
|
4049
|
+
const viewportHeight = shallowRef(0);
|
|
4050
|
+
const state = shallowRef("ok");
|
|
4051
|
+
const isIOS = IN_BROWSER && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
4052
|
+
const momentum = momentumOption ?? isIOS;
|
|
4053
|
+
const elastic = elasticOption ?? isIOS;
|
|
4054
|
+
let raf = -1;
|
|
4055
|
+
let rebuildRaf = -1;
|
|
4056
|
+
let edgeRaf = -1;
|
|
4057
|
+
const cachedViewport = Number.parseInt(String(height)) || 0;
|
|
4058
|
+
let anchorIndex = -1;
|
|
4059
|
+
let anchorOffset = 0;
|
|
4060
|
+
const computedItems = computed(() => items.value.slice(first.value, last.value).map((item, i) => ({
|
|
4061
|
+
raw: item,
|
|
4062
|
+
index: i + first.value
|
|
4063
|
+
})));
|
|
4064
|
+
watch(element, (el) => {
|
|
4065
|
+
if (!IN_BROWSER || !el?.style) return;
|
|
4066
|
+
/* v8 ignore start -- browser-only style assignments */
|
|
4067
|
+
if (momentum) el.style.webkitOverflowScrolling = "touch";
|
|
4068
|
+
if (!elastic) el.style.overscrollBehavior = "none";
|
|
4069
|
+
/* v8 ignore stop */
|
|
4070
|
+
});
|
|
4071
|
+
useResizeObserver(element, (entries) => {
|
|
4072
|
+
if (!entries[0]) return;
|
|
4073
|
+
viewportHeight.value = entries[0].contentRect.height;
|
|
4074
|
+
update();
|
|
4075
|
+
});
|
|
4076
|
+
watch(items, (newItems) => {
|
|
4077
|
+
captureAnchor();
|
|
4078
|
+
const length = newItems.length;
|
|
4079
|
+
const newHeights = heights.value.length === length ? heights.value : Array.from({ length }, () => null);
|
|
4080
|
+
if (heights.value !== newHeights) heights.value = newHeights;
|
|
4081
|
+
rebuild();
|
|
4082
|
+
}, { immediate: true });
|
|
4083
|
+
watch(element, () => {
|
|
4084
|
+
if (!element.value) return;
|
|
4085
|
+
if (direction === "reverse" && items.value.length > 0) {
|
|
4086
|
+
const lastIndex = items.value.length - 1;
|
|
4087
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || itemHeight.value);
|
|
4088
|
+
element.value.scrollTop = totalHeight;
|
|
4089
|
+
}
|
|
4090
|
+
update();
|
|
4091
|
+
});
|
|
4092
|
+
function captureAnchor() {
|
|
4093
|
+
if (!element.value || anchor === "auto") return;
|
|
4094
|
+
if (anchor === "start") {
|
|
4095
|
+
anchorIndex = 0;
|
|
4096
|
+
anchorOffset = 0;
|
|
4097
|
+
} else if (anchor === "end") {
|
|
4098
|
+
anchorIndex = items.value.length - 1;
|
|
4099
|
+
anchorOffset = 0;
|
|
4100
|
+
} else if (/* @__PURE__ */ isFunction(anchor)) {
|
|
4101
|
+
const result = anchor(items.value);
|
|
4102
|
+
if (/* @__PURE__ */ isNumber(result)) {
|
|
4103
|
+
anchorIndex = result;
|
|
4104
|
+
anchorOffset = element.value.scrollTop - (offsets.value[result] || 0);
|
|
4105
|
+
}
|
|
4106
|
+
} else {
|
|
4107
|
+
/* v8 ignore start -- default anchor capture */
|
|
4108
|
+
anchorIndex = first.value;
|
|
4109
|
+
anchorOffset = element.value.scrollTop - (offsets.value[first.value] || 0);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
function restoreAnchor() {
|
|
4113
|
+
if (!element.value || anchorIndex < 0) return;
|
|
4114
|
+
const newScrollTop = (offsets.value[anchorIndex] || 0) + anchorOffset;
|
|
4115
|
+
if (anchorSmooth && direction === "forward") element.value.scrollTo({
|
|
4116
|
+
top: newScrollTop,
|
|
4117
|
+
behavior: "smooth"
|
|
4118
|
+
});
|
|
4119
|
+
else element.value.scrollTop = newScrollTop;
|
|
4120
|
+
/* v8 ignore stop */
|
|
4121
|
+
anchorIndex = -1;
|
|
4122
|
+
anchorOffset = 0;
|
|
4123
|
+
}
|
|
4124
|
+
function rebuild() {
|
|
4125
|
+
const length = items.value.length;
|
|
4126
|
+
const newOffsets = offsets.value.length === length ? offsets.value : Array.from({ length });
|
|
4127
|
+
let offset$1 = 0;
|
|
4128
|
+
for (let i = 0; i < length; i++) {
|
|
4129
|
+
newOffsets[i] = offset$1;
|
|
4130
|
+
offset$1 += heights.value[i] || itemHeight.value;
|
|
4131
|
+
}
|
|
4132
|
+
if (offsets.value !== newOffsets) offsets.value = newOffsets;
|
|
4133
|
+
update();
|
|
4134
|
+
restoreAnchor();
|
|
4135
|
+
}
|
|
4136
|
+
function findIndex(scrollTop) {
|
|
4137
|
+
const arr = offsets.value;
|
|
4138
|
+
if (arr.length === 0) return 0;
|
|
4139
|
+
let low = 0;
|
|
4140
|
+
let high = arr.length - 1;
|
|
4141
|
+
while (low <= high) {
|
|
4142
|
+
const mid = low + high >> 1;
|
|
4143
|
+
if (arr[mid] <= scrollTop) low = mid + 1;
|
|
4144
|
+
else high = mid - 1;
|
|
4145
|
+
}
|
|
4146
|
+
return Math.max(0, high);
|
|
4147
|
+
}
|
|
4148
|
+
function update() {
|
|
4149
|
+
if (!element.value) return;
|
|
4150
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
4151
|
+
if (!viewport || !itemHeight.value) return;
|
|
4152
|
+
const scrollTop = element.value.scrollTop || 0;
|
|
4153
|
+
const length = items.value.length;
|
|
4154
|
+
const visibleStart = findIndex(scrollTop);
|
|
4155
|
+
const visibleEnd = findIndex(scrollTop + viewport) + 1;
|
|
4156
|
+
const start = /* @__PURE__ */ clamp(visibleStart - overscan, 0, length);
|
|
4157
|
+
const end = /* @__PURE__ */ clamp(visibleEnd + overscan, start, length);
|
|
4158
|
+
first.value = start;
|
|
4159
|
+
last.value = end;
|
|
4160
|
+
offset.value = offsets.value[start] || 0;
|
|
4161
|
+
const lastIndex = length - 1;
|
|
4162
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || itemHeight.value);
|
|
4163
|
+
size.value = totalHeight - (offsets.value[end] || totalHeight);
|
|
4164
|
+
}
|
|
4165
|
+
function checkEdges() {
|
|
4166
|
+
if (!element.value) return;
|
|
4167
|
+
const scrollTop = element.value.scrollTop;
|
|
4168
|
+
const scrollHeight = element.value.scrollHeight;
|
|
4169
|
+
const clientHeight = element.value.clientHeight;
|
|
4170
|
+
const distanceFromStart = scrollTop;
|
|
4171
|
+
const distanceFromEnd = scrollHeight - (scrollTop + clientHeight);
|
|
4172
|
+
if (IN_BROWSER) {
|
|
4173
|
+
cancelAnimationFrame(edgeRaf);
|
|
4174
|
+
edgeRaf = requestAnimationFrame(() => {
|
|
4175
|
+
if (onStartReached && distanceFromStart <= startThreshold) onStartReached(distanceFromStart);
|
|
4176
|
+
if (onEndReached && distanceFromEnd <= endThreshold) onEndReached(distanceFromEnd);
|
|
4177
|
+
});
|
|
4178
|
+
} else {
|
|
4179
|
+
if (onStartReached && distanceFromStart <= startThreshold) onStartReached(distanceFromStart);
|
|
4180
|
+
if (onEndReached && distanceFromEnd <= endThreshold) onEndReached(distanceFromEnd);
|
|
4181
|
+
}
|
|
4182
|
+
/* v8 ignore stop */
|
|
4183
|
+
}
|
|
4184
|
+
function resize(index, height$1) {
|
|
4185
|
+
if (heights.value[index] === height$1) return;
|
|
4186
|
+
heights.value[index] = height$1;
|
|
4187
|
+
if (!itemHeight.value) itemHeight.value = height$1;
|
|
4188
|
+
if (IN_BROWSER) {
|
|
4189
|
+
cancelAnimationFrame(rebuildRaf);
|
|
4190
|
+
rebuildRaf = requestAnimationFrame(rebuild);
|
|
4191
|
+
} else rebuild();
|
|
4192
|
+
/* v8 ignore stop */
|
|
4193
|
+
}
|
|
4194
|
+
function scroll() {
|
|
4195
|
+
if (IN_BROWSER) {
|
|
4196
|
+
cancelAnimationFrame(raf);
|
|
4197
|
+
raf = requestAnimationFrame(update);
|
|
4198
|
+
checkEdges();
|
|
4199
|
+
} else update();
|
|
4200
|
+
/* v8 ignore stop */
|
|
4201
|
+
}
|
|
4202
|
+
function scrollTo(index, scrollOptions) {
|
|
4203
|
+
if (!element.value) return;
|
|
4204
|
+
const targetOffset = offsets.value[index] || 0;
|
|
4205
|
+
const behavior = scrollOptions?.behavior ?? "auto";
|
|
4206
|
+
const block = scrollOptions?.block ?? "start";
|
|
4207
|
+
const extraOffset = scrollOptions?.offset ?? 0;
|
|
4208
|
+
let scrollTop = targetOffset + extraOffset;
|
|
4209
|
+
switch (block) {
|
|
4210
|
+
case "center": {
|
|
4211
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
4212
|
+
const itemH = heights.value[index] || itemHeight.value;
|
|
4213
|
+
scrollTop = targetOffset - viewport / 2 + itemH / 2 + extraOffset;
|
|
4214
|
+
break;
|
|
4215
|
+
}
|
|
4216
|
+
case "end": {
|
|
4217
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
4218
|
+
const itemH = heights.value[index] || itemHeight.value;
|
|
4219
|
+
scrollTop = targetOffset - viewport + itemH + extraOffset;
|
|
4220
|
+
break;
|
|
4221
|
+
}
|
|
4222
|
+
case "nearest": {
|
|
4223
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
4224
|
+
const currentScroll = element.value.scrollTop;
|
|
4225
|
+
const itemH = heights.value[index] || itemHeight.value;
|
|
4226
|
+
if (targetOffset < currentScroll) scrollTop = targetOffset + extraOffset;
|
|
4227
|
+
else if (targetOffset + itemH > currentScroll + viewport) scrollTop = targetOffset - viewport + itemH + extraOffset;
|
|
4228
|
+
else return;
|
|
4229
|
+
break;
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
if (behavior === "smooth") element.value.scrollTo({
|
|
4233
|
+
top: scrollTop,
|
|
4234
|
+
behavior: "smooth"
|
|
4235
|
+
});
|
|
4236
|
+
else element.value.scrollTop = scrollTop;
|
|
4237
|
+
update();
|
|
4238
|
+
}
|
|
4239
|
+
function reset() {
|
|
4240
|
+
state.value = "ok";
|
|
4241
|
+
anchorIndex = -1;
|
|
4242
|
+
anchorOffset = 0;
|
|
4243
|
+
if (element.value && direction === "reverse" && items.value.length > 0) {
|
|
4244
|
+
const lastIndex = items.value.length - 1;
|
|
4245
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || itemHeight.value);
|
|
4246
|
+
element.value.scrollTop = totalHeight;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
onScopeDispose(() => {
|
|
4250
|
+
cancelAnimationFrame(raf);
|
|
4251
|
+
cancelAnimationFrame(rebuildRaf);
|
|
4252
|
+
cancelAnimationFrame(edgeRaf);
|
|
4253
|
+
}, true);
|
|
4254
|
+
return {
|
|
4255
|
+
element,
|
|
4256
|
+
items: computedItems,
|
|
4257
|
+
offset: readonly(offset),
|
|
4258
|
+
size: readonly(size),
|
|
4259
|
+
state,
|
|
4260
|
+
scrollTo,
|
|
4261
|
+
scroll,
|
|
4262
|
+
scrollend: scroll,
|
|
4263
|
+
resize,
|
|
4264
|
+
reset
|
|
4265
|
+
};
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Creates a virtual scrolling context with dependency injection support.
|
|
4269
|
+
*
|
|
4270
|
+
* @param items Reactive array of items to virtualize
|
|
4271
|
+
* @param options Configuration options including namespace
|
|
4272
|
+
* @template T The type of the items
|
|
4273
|
+
* @returns Trinity tuple: [useVirtual, provideVirtual, defaultVirtual]
|
|
4274
|
+
*
|
|
4275
|
+
* @see https://0.vuetifyjs.com/composables/utilities/create-virtual
|
|
4276
|
+
*
|
|
4277
|
+
* @example
|
|
4278
|
+
* ```ts
|
|
4279
|
+
* // Create injectable context
|
|
4280
|
+
* const [useVirtual, provideVirtual, virtual] = createVirtualContext(items, {
|
|
4281
|
+
* namespace: 'my-virtual',
|
|
4282
|
+
* overscan: 10,
|
|
4283
|
+
* })
|
|
4284
|
+
*
|
|
4285
|
+
* // In parent component
|
|
4286
|
+
* provideVirtual()
|
|
4287
|
+
*
|
|
4288
|
+
* // In child component
|
|
4289
|
+
* const virtual = useVirtual()
|
|
4290
|
+
* ```
|
|
4291
|
+
*/
|
|
4292
|
+
function createVirtualContext(items, _options = {}) {
|
|
4293
|
+
const { namespace = "v0:virtual", ...options } = _options;
|
|
4294
|
+
const [useVirtualContext, _provideVirtualContext] = createContext(namespace);
|
|
4295
|
+
const context = createVirtual(items, options);
|
|
4296
|
+
function provideVirtualContext(_context = context, app) {
|
|
4297
|
+
return _provideVirtualContext(_context, app);
|
|
4298
|
+
}
|
|
4299
|
+
return createTrinity(useVirtualContext, provideVirtualContext, context);
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Returns the current virtual context from dependency injection.
|
|
4303
|
+
*
|
|
4304
|
+
* @param namespace The namespace for the virtual context. Defaults to `v0:virtual`.
|
|
4305
|
+
* @template T The type of the items
|
|
4306
|
+
* @returns The current virtual context.
|
|
4307
|
+
*
|
|
4308
|
+
* @throws An error if the virtual context is not found and no default is provided.
|
|
4309
|
+
*
|
|
4310
|
+
* @see https://0.vuetifyjs.com/composables/utilities/create-virtual
|
|
4311
|
+
*
|
|
4312
|
+
* @example
|
|
4313
|
+
* ```vue
|
|
4314
|
+
* <script lang="ts" setup>
|
|
4315
|
+
* import { createVirtual } from '@vuetify/v0'
|
|
4316
|
+
*
|
|
4317
|
+
* // Inject virtual context provided by parent
|
|
4318
|
+
* const virtual = useVirtual()
|
|
4319
|
+
* <\/script>
|
|
4320
|
+
*
|
|
4321
|
+
* <template>
|
|
4322
|
+
* <div>
|
|
4323
|
+
* <p>Visible items: {{ virtual.items.value.length }}</p>
|
|
4324
|
+
* </div>
|
|
4325
|
+
* </template>
|
|
4326
|
+
* ```
|
|
4327
|
+
*/
|
|
4328
|
+
function useVirtual(namespace = "v0:virtual") {
|
|
4329
|
+
return useContext(namespace);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
//#endregion
|
|
4333
|
+
//#region src/composables/useEventListener/index.ts
|
|
4334
|
+
/**
|
|
4335
|
+
* @module useEventListener
|
|
4336
|
+
*
|
|
4337
|
+
* @remarks
|
|
4338
|
+
* Event listener composable with automatic cleanup on scope disposal.
|
|
4339
|
+
*
|
|
4340
|
+
* Key features:
|
|
4341
|
+
* - Supports Window, Document, and HTMLElement targets
|
|
4342
|
+
* - Reactive targets, events, and listeners
|
|
4343
|
+
* - Event options support (capture, passive, once)
|
|
4344
|
+
* - Automatic removeEventListener on unmount
|
|
4345
|
+
* - Multiple overloads for type safety
|
|
4346
|
+
*
|
|
4347
|
+
* Perfect for safely managing event listeners in Vue components.
|
|
4348
|
+
*/
|
|
4349
|
+
/**
|
|
4350
|
+
* Attaches an event listener to a target.
|
|
4351
|
+
*
|
|
4352
|
+
* @param target The target to attach the event listener to.
|
|
4353
|
+
* @param event The event to listen for.
|
|
4354
|
+
* @param listener The event listener.
|
|
4355
|
+
* @param options The event listener options.
|
|
4356
|
+
* @returns A function to remove the event listener.
|
|
4357
|
+
*
|
|
4358
|
+
* @see https://0.vuetifyjs.com/composables/system/use-event-listener
|
|
4359
|
+
*/
|
|
4360
|
+
function useEventListener(target, event, listener, options) {
|
|
4361
|
+
const cleanups = [];
|
|
4362
|
+
function cleanup() {
|
|
4363
|
+
for (const fn of cleanups) fn();
|
|
4364
|
+
cleanups.length = 0;
|
|
4365
|
+
}
|
|
4366
|
+
function register(el, event$1, listener$1, options$1) {
|
|
4367
|
+
el.addEventListener(event$1, listener$1, options$1);
|
|
4368
|
+
return () => el.removeEventListener(event$1, listener$1, options$1);
|
|
4369
|
+
}
|
|
4370
|
+
const stopWatcher = watch(() => [
|
|
4371
|
+
toValue(target),
|
|
4372
|
+
toValue(event),
|
|
4373
|
+
unref(listener),
|
|
4374
|
+
toValue(options)
|
|
4375
|
+
], ([el, events, listeners, opts]) => {
|
|
4376
|
+
cleanup();
|
|
4377
|
+
if (!el) return;
|
|
4378
|
+
const eventList = toArray(events);
|
|
4379
|
+
const listenerList = toArray(listeners);
|
|
4380
|
+
for (const event$1 of eventList) for (const listenerFn of listenerList) cleanups.push(register(el, event$1, listenerFn, opts));
|
|
4381
|
+
}, {
|
|
4382
|
+
immediate: true,
|
|
4383
|
+
flush: "post"
|
|
4384
|
+
});
|
|
4385
|
+
function stop() {
|
|
4386
|
+
stopWatcher();
|
|
4387
|
+
cleanup();
|
|
4388
|
+
}
|
|
4389
|
+
onScopeDispose(stop, true);
|
|
4390
|
+
return stop;
|
|
4391
|
+
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Attaches an event listener to the window.
|
|
4394
|
+
*
|
|
4395
|
+
* @param event The event to listen for.
|
|
4396
|
+
* @param listener The event listener.
|
|
4397
|
+
* @param options The event listener options.
|
|
4398
|
+
* @template E The event type.
|
|
4399
|
+
* @returns A function to remove the event listener.
|
|
4400
|
+
*
|
|
4401
|
+
* @see https://0.vuetifyjs.com/composables/system/use-event-listener
|
|
4402
|
+
*/
|
|
4403
|
+
function useWindowEventListener(event, listener, options) {
|
|
4404
|
+
return IN_BROWSER ? useEventListener(window, event, listener, options) : () => {};
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Attaches an event listener to the document.
|
|
4408
|
+
*
|
|
4409
|
+
* @param event The event to listen for.
|
|
4410
|
+
* @param listener The event listener.
|
|
4411
|
+
* @param options The event listener options.
|
|
4412
|
+
* @template E The event type.
|
|
4413
|
+
* @returns A function to remove the event listener.
|
|
4414
|
+
*
|
|
4415
|
+
* @see https://0.vuetifyjs.com/composables/system/use-event-listener
|
|
4416
|
+
*/
|
|
4417
|
+
function useDocumentEventListener(event, listener, options) {
|
|
4418
|
+
return IN_BROWSER ? useEventListener(document, event, listener, options) : () => {};
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
//#endregion
|
|
4422
|
+
//#region src/composables/useClickOutside/index.ts
|
|
4423
|
+
/**
|
|
4424
|
+
* @module useClickOutside
|
|
4425
|
+
*
|
|
4426
|
+
* @remarks
|
|
4427
|
+
* Detects clicks outside of specified element(s) with automatic cleanup.
|
|
4428
|
+
*
|
|
4429
|
+
* Key features:
|
|
4430
|
+
* - Two-phase detection (pointerdown → pointerup) prevents drag-out false positives
|
|
4431
|
+
* - Touch scroll threshold ignores swipes/scrolls on mobile
|
|
4432
|
+
* - Capture phase listeners work with stopPropagation
|
|
4433
|
+
* - Pause/resume/stop functionality
|
|
4434
|
+
* - Optional iframe focus detection
|
|
4435
|
+
* - SSR-safe (no-op when not in browser)
|
|
4436
|
+
*
|
|
4437
|
+
* Common use cases: closing popovers, dropdowns, modals, and menus.
|
|
4438
|
+
*
|
|
4439
|
+
* Accessibility: This composable handles pointer interactions only. For accessible
|
|
4440
|
+
* components (dialogs, popovers, menus), pair with `useHotkey` for Escape key
|
|
4441
|
+
* dismissal per WCAG/APG requirements.
|
|
4442
|
+
*/
|
|
4443
|
+
/**
|
|
4444
|
+
* Detects clicks outside of the specified element(s).
|
|
4445
|
+
*
|
|
4446
|
+
* Uses two-phase detection (pointerdown → pointerup) to prevent false positives
|
|
4447
|
+
* when users drag from inside to outside an element.
|
|
4448
|
+
*
|
|
4449
|
+
* @param target Element ref(s) to detect clicks outside of. Accepts a single ref/getter or array of refs/getters.
|
|
4450
|
+
* @param handler Callback invoked when a click outside is detected.
|
|
4451
|
+
* @param options Configuration options.
|
|
4452
|
+
* @returns An object with methods to control the listener.
|
|
4453
|
+
*
|
|
4454
|
+
* @see https://0.vuetifyjs.com/composables/system/use-click-outside
|
|
4455
|
+
*
|
|
4456
|
+
* @example Native element ref
|
|
4457
|
+
* ```ts
|
|
4458
|
+
* const menuRef = useTemplateRef<HTMLElement>('menu')
|
|
4459
|
+
*
|
|
4460
|
+
* useClickOutside(menuRef, () => { isOpen.value = false })
|
|
4461
|
+
* ```
|
|
4462
|
+
*
|
|
4463
|
+
* @example Component ref (e.g., Atom)
|
|
4464
|
+
* ```ts
|
|
4465
|
+
* const atomRef = useTemplateRef<AtomExpose>('atom')
|
|
4466
|
+
*
|
|
4467
|
+
* // Pass the exposed element TemplateRef via getter
|
|
4468
|
+
* useClickOutside(
|
|
4469
|
+
* () => atomRef.value?.element,
|
|
4470
|
+
* () => { isOpen.value = false }
|
|
4471
|
+
* )
|
|
4472
|
+
* ```
|
|
4473
|
+
*
|
|
4474
|
+
* @example Multiple targets
|
|
4475
|
+
* ```ts
|
|
4476
|
+
* const popoverRef = useTemplateRef<AtomExpose>('popover')
|
|
4477
|
+
* const anchorRef = useTemplateRef<HTMLElement>('anchor')
|
|
4478
|
+
*
|
|
4479
|
+
* useClickOutside(
|
|
4480
|
+
* [() => popoverRef.value?.element, anchorRef],
|
|
4481
|
+
* () => { isOpen.value = false }
|
|
4482
|
+
* )
|
|
4483
|
+
* ```
|
|
4484
|
+
*
|
|
4485
|
+
* @example Ignoring elements (CSS selectors or refs)
|
|
4486
|
+
* ```ts
|
|
4487
|
+
* useClickOutside(
|
|
4488
|
+
* () => navRef.value?.element,
|
|
4489
|
+
* () => { isOpen.value = false },
|
|
4490
|
+
* { ignore: ['[data-app-bar]'] }
|
|
4491
|
+
* )
|
|
4492
|
+
* ```
|
|
4493
|
+
*
|
|
4494
|
+
* @example Native dialog with bounds detection
|
|
4495
|
+
* ```ts
|
|
4496
|
+
* // For native <dialog> elements, use bounds mode to detect backdrop clicks
|
|
4497
|
+
* const dialogRef = useTemplateRef<HTMLDialogElement>('dialog')
|
|
4498
|
+
*
|
|
4499
|
+
* useClickOutside(
|
|
4500
|
+
* dialogRef,
|
|
4501
|
+
* () => { dialogRef.value?.close() },
|
|
4502
|
+
* { bounds: true }
|
|
4503
|
+
* )
|
|
4504
|
+
* ```
|
|
4505
|
+
*/
|
|
4506
|
+
function useClickOutside(target, handler, options = {}) {
|
|
4507
|
+
const { capture = true, touchScrollThreshold = 30, detectIframe = false, ignore = [], bounds = false } = options;
|
|
4508
|
+
const isPaused = shallowRef(false);
|
|
4509
|
+
const isActive = toRef(() => !isPaused.value);
|
|
4510
|
+
let initialTarget = null;
|
|
4511
|
+
let startPosition = {
|
|
4512
|
+
x: 0,
|
|
4513
|
+
y: 0
|
|
4514
|
+
};
|
|
4515
|
+
let cleanupPointerDown;
|
|
4516
|
+
let cleanupPointerUp;
|
|
4517
|
+
let cleanupBlur;
|
|
4518
|
+
/**
|
|
4519
|
+
* Resolve target(s) to an array of HTMLElements.
|
|
4520
|
+
*/
|
|
4521
|
+
function getTargets() {
|
|
4522
|
+
return toArray(target).map((source) => toValue(source)).filter((el) => !!el && /* @__PURE__ */ isFunction(el.contains));
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Resolve ignore targets to a tuple of [selectors, elements].
|
|
4526
|
+
* Called once per event to avoid repeated toValue() calls in hot path.
|
|
4527
|
+
*/
|
|
4528
|
+
function resolveIgnoreTargets() {
|
|
4529
|
+
const ignoreTargets = toValue(ignore);
|
|
4530
|
+
if (ignoreTargets.length === 0) return [[], []];
|
|
4531
|
+
const selectors = [];
|
|
4532
|
+
const elements = [];
|
|
4533
|
+
for (const ignoreTarget of ignoreTargets) if (/* @__PURE__ */ isString(ignoreTarget)) selectors.push(ignoreTarget);
|
|
4534
|
+
else {
|
|
4535
|
+
const ignoreEl = toValue(ignoreTarget);
|
|
4536
|
+
if (ignoreEl) elements.push(ignoreEl);
|
|
4537
|
+
}
|
|
4538
|
+
return [selectors, elements];
|
|
4539
|
+
}
|
|
4540
|
+
/**
|
|
4541
|
+
* Check if an element matches resolved ignore targets.
|
|
4542
|
+
*/
|
|
4543
|
+
function isIgnored(el, selectors, elements) {
|
|
4544
|
+
if (!el) return false;
|
|
4545
|
+
for (const selector of selectors) try {
|
|
4546
|
+
if (el.matches(selector) || !/* @__PURE__ */ isNull(el.closest(selector))) return true;
|
|
4547
|
+
} catch {}
|
|
4548
|
+
for (const ignoreEl of elements) if (ignoreEl === el || ignoreEl.contains(el)) return true;
|
|
4549
|
+
return false;
|
|
4550
|
+
}
|
|
4551
|
+
/**
|
|
4552
|
+
* Check if any element in the event path should be ignored.
|
|
4553
|
+
*/
|
|
4554
|
+
function shouldIgnore(path) {
|
|
4555
|
+
const [selectors, elements] = resolveIgnoreTargets();
|
|
4556
|
+
if (selectors.length === 0 && elements.length === 0) return false;
|
|
4557
|
+
return path.some((node) => node instanceof Element && isIgnored(node, selectors, elements));
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* Check if the event target is outside all target elements (DOM containment).
|
|
4561
|
+
*/
|
|
4562
|
+
function isOutside(eventTarget) {
|
|
4563
|
+
/* v8 ignore start -- defensive guards */
|
|
4564
|
+
if (!eventTarget) return false;
|
|
4565
|
+
if (!(eventTarget instanceof Node)) return false;
|
|
4566
|
+
/* v8 ignore stop */
|
|
4567
|
+
const targets = getTargets();
|
|
4568
|
+
if (targets.length === 0) return false;
|
|
4569
|
+
return targets.every((el) => {
|
|
4570
|
+
if (/* @__PURE__ */ isNullOrUndefined(el) || !/* @__PURE__ */ isFunction(el.contains)) return false;
|
|
4571
|
+
return el !== eventTarget && !el.contains(eventTarget);
|
|
4572
|
+
});
|
|
4573
|
+
}
|
|
4574
|
+
/**
|
|
4575
|
+
* Check if coordinates are outside all target elements' bounding rects.
|
|
4576
|
+
*/
|
|
4577
|
+
function isOutsideBounds(x, y) {
|
|
4578
|
+
const targets = getTargets();
|
|
4579
|
+
if (targets.length === 0) return false;
|
|
4580
|
+
return targets.every((el) => {
|
|
4581
|
+
const { left, right, top, bottom } = el.getBoundingClientRect();
|
|
4582
|
+
return x < left || x > right || y < top || y > bottom;
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
/**
|
|
4586
|
+
* Validate that the target is still in the DOM.
|
|
4587
|
+
*/
|
|
4588
|
+
function isValidTarget(eventTarget) {
|
|
4589
|
+
if (!(eventTarget instanceof Element)) return false;
|
|
4590
|
+
if (!eventTarget.isConnected) return false;
|
|
4591
|
+
return true;
|
|
4592
|
+
}
|
|
4593
|
+
/* v8 ignore start -- event handlers tested via integration, not unit */
|
|
4594
|
+
/**
|
|
4595
|
+
* Handle pointerdown - store initial target and position.
|
|
4596
|
+
*/
|
|
4597
|
+
function onPointerDown(event) {
|
|
4598
|
+
if (isPaused.value) return;
|
|
4599
|
+
if (event.defaultPrevented) return;
|
|
4600
|
+
initialTarget = event.composedPath()[0] ?? event.target;
|
|
4601
|
+
startPosition = {
|
|
4602
|
+
x: event.clientX,
|
|
4603
|
+
y: event.clientY
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4606
|
+
/**
|
|
4607
|
+
* Handle pointerup - check if it's an outside click.
|
|
4608
|
+
*/
|
|
4609
|
+
function onPointerUp(event) {
|
|
4610
|
+
if (isPaused.value) return;
|
|
4611
|
+
if (event.defaultPrevented) return;
|
|
4612
|
+
if (!initialTarget) return;
|
|
4613
|
+
const pointerdownTarget = initialTarget;
|
|
4614
|
+
initialTarget = null;
|
|
4615
|
+
if (!isValidTarget(pointerdownTarget)) return;
|
|
4616
|
+
const path = event.composedPath();
|
|
4617
|
+
const pointerupTarget = path[0] ?? event.target;
|
|
4618
|
+
if (event.pointerType === "touch") {
|
|
4619
|
+
const dx = Math.abs(event.clientX - startPosition.x);
|
|
4620
|
+
const dy = Math.abs(event.clientY - startPosition.y);
|
|
4621
|
+
if (dx >= touchScrollThreshold || dy >= touchScrollThreshold) return;
|
|
4622
|
+
}
|
|
4623
|
+
if ((bounds ? isOutsideBounds(startPosition.x, startPosition.y) && isOutsideBounds(event.clientX, event.clientY) : isOutside(pointerdownTarget) && isOutside(pointerupTarget)) && !shouldIgnore(path)) handler(event);
|
|
4624
|
+
}
|
|
4625
|
+
/**
|
|
4626
|
+
* Handle window blur - detect focus moving to iframe.
|
|
4627
|
+
*/
|
|
4628
|
+
function onBlur(event) {
|
|
4629
|
+
if (isPaused.value) return;
|
|
4630
|
+
if (event.defaultPrevented) return;
|
|
4631
|
+
if (document.activeElement instanceof HTMLIFrameElement) {
|
|
4632
|
+
const iframeIsOutside = getTargets().every((el) => !el.contains(document.activeElement));
|
|
4633
|
+
const [selectors, elements] = resolveIgnoreTargets();
|
|
4634
|
+
if (iframeIsOutside && !isIgnored(document.activeElement, selectors, elements)) handler(event);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
/* v8 ignore stop */
|
|
4638
|
+
function setup() {
|
|
4639
|
+
cleanupPointerDown = useDocumentEventListener("pointerdown", onPointerDown, capture);
|
|
4640
|
+
cleanupPointerUp = useDocumentEventListener("pointerup", onPointerUp, capture);
|
|
4641
|
+
if (!detectIframe) return;
|
|
4642
|
+
cleanupBlur = useWindowEventListener("blur", onBlur, capture);
|
|
4643
|
+
}
|
|
4644
|
+
function cleanup() {
|
|
4645
|
+
cleanupPointerDown?.();
|
|
4646
|
+
cleanupPointerUp?.();
|
|
4647
|
+
cleanupBlur?.();
|
|
4648
|
+
cleanupPointerDown = void 0;
|
|
4649
|
+
cleanupPointerUp = void 0;
|
|
4650
|
+
cleanupBlur = void 0;
|
|
4651
|
+
}
|
|
4652
|
+
function pause() {
|
|
4653
|
+
if (isPaused.value) return;
|
|
4654
|
+
isPaused.value = true;
|
|
4655
|
+
initialTarget = null;
|
|
4656
|
+
cleanup();
|
|
4657
|
+
}
|
|
4658
|
+
function resume() {
|
|
4659
|
+
if (!isPaused.value) return;
|
|
4660
|
+
isPaused.value = false;
|
|
4661
|
+
setup();
|
|
4662
|
+
}
|
|
4663
|
+
function stop() {
|
|
4664
|
+
isPaused.value = true;
|
|
4665
|
+
initialTarget = null;
|
|
4666
|
+
cleanup();
|
|
4667
|
+
}
|
|
4668
|
+
setup();
|
|
4669
|
+
onScopeDispose(stop, true);
|
|
4670
|
+
return {
|
|
4671
|
+
isActive: shallowReadonly(isActive),
|
|
4672
|
+
isPaused: shallowReadonly(isPaused),
|
|
4673
|
+
pause,
|
|
4674
|
+
resume,
|
|
4675
|
+
stop
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
//#endregion
|
|
4680
|
+
export { createOverflow as $, singleOpenStrategy as A, createForm as B, createQueue as C, createNestedContext as D, createNested as E, usePrefersReducedMotion as F, createDatePlugin as G, useForm as H, useLazy as I, createBreakpointsContext as J, useDate as K, useElementIntersection as L, useMediaQuery as M, usePrefersContrast as N, useNested as O, usePrefersDark as P, useProxyModel as Q, useIntersectionObserver as R, useStack as S, useQueue as T, createDate as U, createFormContext as V, createDateContext as W, useBreakpoints as X, createBreakpointsPlugin as Y, toReactive as Z, createStepContext as _, createVirtual as a, createHydration as at, createStackContext as b, useToggleScope as c, useHydration as ct, useTimeline as d, useBreadcrumbs as dt, createOverflowContext as et, createStorage as f, createStep as g, useStorage as h, useWindowEventListener as i, createFallbackHydration as it, useMutationObserver as j, multipleOpenStrategy as k, createTimeline as l, createBreadcrumbs as lt, createStoragePlugin as m, useDocumentEventListener as n, useElementSize as nt, createVirtualContext as o, createHydrationContext as ot, createStorageContext as p, createBreakpoints as q, useEventListener as r, useResizeObserver as rt, useVirtual as s, createHydrationPlugin as st, useClickOutside as t, useOverflow as tt, createTimelineContext as u, createBreadcrumbsContext as ut, useStep as v, createQueueContext as w, createStackPlugin as x, createStack as y, useHotkey as z };
|