@vuetify/v0 1.0.0-beta.0 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{adapter-DZeI0ZbW.d.mts → adapter-CQoi3rdC.d.mts} +1 -1
- package/dist/adapter-CVoWg0sw.mjs +99 -0
- package/dist/{adapter-oM18AdUc.mjs → adapter-Cj7vxEdB.mjs} +6 -1
- package/dist/adapter-Cy7wXVFe.mjs +4 -0
- package/dist/{adapter-we-0GbFH.d.mts → adapter-D2ABPTpo.d.mts} +2 -2
- package/dist/{adapter-BO75n0Cn.d.mts → adapter-_srNCBEi.d.mts} +3 -3
- package/dist/browser/index.js +23639 -23267
- package/dist/components/index.d.mts +1 -1
- package/dist/components/index.mjs +1 -1
- package/dist/{components-_uxZ_Zo3.mjs → components-DcOwIGXt.mjs} +304 -214
- package/dist/composables/index.d.mts +20 -20
- package/dist/composables/index.mjs +34 -28
- package/dist/composables-DxKmYiTj.mjs +4077 -0
- package/dist/{consola-CUNHyu8J.d.mts → consola-BPGkJp_q.d.mts} +1 -1
- package/dist/constants/index.d.mts +1 -1
- package/dist/constants/index.mjs +1 -1
- package/dist/{createDataTable-DlFQyBTN.mjs → createDataTable-CBfnCUKU.mjs} +14 -11
- package/dist/{createGroup-DsKJMuRd.mjs → createGroup-BTG6xcyp.mjs} +26 -14
- package/dist/{adapter-BCVxBtDp.mjs → createPagination-B5dfqY-a.mjs} +3 -98
- package/dist/{createRegistry-tJ1PPNIr.mjs → createRegistry-Cb6ftNMU.mjs} +57 -41
- package/dist/{createSelection-DEqhvdoj.mjs → createSelection-BMvXj84C.mjs} +7 -4
- package/dist/{createSingle-ClzWqRCe.mjs → createSingle-BCUJuV4D.mjs} +3 -3
- package/dist/{createTokens-mNxs8DQ3.mjs → createTokens-BSSQAfNX.mjs} +12 -6
- package/dist/data-table/adapters/client/index.d.mts +1 -1
- package/dist/data-table/adapters/client/index.mjs +1 -1
- package/dist/data-table/adapters/index.d.mts +4 -4
- package/dist/data-table/adapters/index.mjs +4 -5
- package/dist/data-table/adapters/server/index.d.mts +1 -1
- package/dist/data-table/adapters/server/index.mjs +1 -1
- package/dist/data-table/adapters/virtual/index.d.mts +1 -1
- package/dist/data-table/adapters/virtual/index.mjs +1 -1
- package/dist/data-table/index.d.mts +5 -5
- package/dist/data-table/index.mjs +5 -5
- package/dist/date/index.d.mts +1 -1
- package/dist/date/index.mjs +36 -33
- package/dist/features/adapters/flagsmith/index.d.mts +1 -1
- package/dist/features/adapters/flagsmith/index.mjs +47 -1
- package/dist/features/adapters/launchdarkly/index.d.mts +1 -1
- package/dist/features/adapters/launchdarkly/index.mjs +37 -1
- package/dist/features/adapters/posthog/index.d.mts +1 -1
- package/dist/features/adapters/posthog/index.mjs +46 -1
- package/dist/features/index.d.mts +2 -2
- package/dist/features/index.mjs +2 -2
- package/dist/{flagsmith-Cn_KVy6v.d.mts → flagsmith-CXzNBqm3.d.mts} +1 -1
- package/dist/{globals-DB-krEA-.mjs → globals-C1X-9O7K.mjs} +1 -1
- package/dist/{index-DWAgQOBa.d.mts → index-Bmz8PZMY.d.mts} +3 -4
- package/dist/{index-BN06veYe.d.mts → index-CKyDkh8J.d.mts} +169 -88
- package/dist/{index-B3PDLk_Y.d.mts → index-DLBPM47A.d.mts} +509 -439
- package/dist/{index-ZD3hw2Zo.d.mts → index-DRrHHgE7.d.mts} +4 -4
- package/dist/{index-ATZqco03.d.mts → index-LMiCHwZl.d.mts} +7 -2
- package/dist/{index-BIyQ3OxC.d.mts → index-rJ_lhMYJ.d.mts} +5 -5
- package/dist/index.d.mts +24 -24
- package/dist/index.mjs +37 -31
- package/dist/json/attributes.json +2 -2
- package/dist/json/web-types.json +3 -3
- package/dist/{launchdarkly-C3Knz7-v.d.mts → launchdarkly-DvhloBts.d.mts} +1 -1
- package/dist/locale/adapters/index.d.mts +2 -2
- package/dist/locale/adapters/index.mjs +2 -3
- package/dist/locale/adapters/v0/index.d.mts +1 -1
- package/dist/locale/adapters/v0/index.mjs +1 -1
- package/dist/locale/adapters/vue-i18n/index.d.mts +1 -1
- package/dist/locale/adapters/vue-i18n/index.mjs +1 -1
- package/dist/locale/index.d.mts +3 -3
- package/dist/locale/index.mjs +3 -3
- package/dist/logger/adapters/consola/index.d.mts +1 -1
- package/dist/logger/adapters/index.d.mts +4 -4
- package/dist/logger/adapters/index.mjs +1 -2
- package/dist/logger/adapters/pino/index.d.mts +1 -1
- package/dist/logger/adapters/v0/index.d.mts +1 -1
- package/dist/logger/adapters/v0/index.mjs +1 -1
- package/dist/logger/index.d.mts +5 -5
- package/dist/logger/index.mjs +2 -2
- package/dist/notifications/index.d.mts +6 -6
- package/dist/notifications/index.mjs +29 -29
- package/dist/palettes/ant/index.d.mts +1 -1
- package/dist/palettes/index.d.mts +3 -3
- package/dist/palettes/material/index.d.mts +1 -1
- package/dist/palettes/md1/index.d.mts +1 -1
- package/dist/palettes/md2/index.d.mts +1 -1
- package/dist/palettes/radix/index.d.mts +1 -1
- package/dist/palettes/tailwind/index.d.mts +1 -1
- package/dist/permissions/adapters/index.d.mts +1 -1
- package/dist/permissions/adapters/index.mjs +1 -2
- package/dist/permissions/adapters/v0/index.d.mts +1 -1
- package/dist/permissions/adapters/v0/index.mjs +1 -1
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +2 -2
- package/dist/{pino-BaYY_KIN.d.mts → pino-B6BI1AOK.d.mts} +1 -1
- package/dist/{posthog-CR8R2ywM.d.mts → posthog-BOT5W7Ds.d.mts} +1 -1
- package/dist/rules/index.d.mts +1 -1
- package/dist/rules/index.mjs +1 -1
- package/dist/{server-CZ83nTn_.mjs → server-D5BJBjB0.mjs} +2 -1
- package/dist/{server-7vjHxjNc.d.mts → server-DbtTGb67.d.mts} +1 -1
- package/dist/storage/adapters/index.d.mts +2 -2
- package/dist/storage/adapters/index.mjs +1 -1
- package/dist/storage/adapters/memory/index.d.mts +1 -1
- package/dist/storage/adapters/memory/index.mjs +1 -1
- package/dist/storage/index.d.mts +3 -3
- package/dist/storage/index.mjs +2 -2
- package/dist/theme/adapters/index.d.mts +1 -1
- package/dist/theme/adapters/index.mjs +3 -4
- package/dist/theme/adapters/unhead/index.d.mts +1 -1
- package/dist/theme/adapters/unhead/index.mjs +1 -1
- package/dist/theme/adapters/v0/index.d.mts +1 -1
- package/dist/theme/adapters/v0/index.mjs +1 -1
- package/dist/theme/index.d.mts +1 -1
- package/dist/theme/index.mjs +5 -4
- package/dist/types/index.d.mts +1 -1
- package/dist/{unhead-CG672gKh.mjs → unhead-i8ji788n.mjs} +12 -8
- package/dist/useEventListener-BK_uaXZv.mjs +103 -0
- package/dist/{useFeatures-B-A-eOo-.mjs → useFeatures-BKAK56af.mjs} +4 -8
- package/dist/{useLocale-CbpoxbVU.mjs → useLocale-CTwn8uZk.mjs} +4 -5
- package/dist/{useLogger-CTgAedNH.mjs → useLogger-D7_hm4e8.mjs} +2 -3
- package/dist/{useNotifications-BkKGUl_1.mjs → useNotifications-B2YeotHN.mjs} +15 -8
- package/dist/{usePermissions-BYOrKcnk.mjs → usePermissions-CWYXR7ji.mjs} +18 -11
- package/dist/{useClickOutside-DoOgNtrj.mjs → useRovingFocus-BdnSUVrD.mjs} +4014 -8138
- package/dist/{useRules-mD_DY9dy.mjs → useRules-CB2_tvd0.mjs} +2 -2
- package/dist/useStorage-BORz1d1g.mjs +162 -0
- package/dist/{useTheme-B1lUya5B.mjs → useTheme-ClUmdmvT.mjs} +12 -9
- package/dist/utilities/index.d.mts +2 -2
- package/dist/utilities/index.mjs +7 -3
- package/dist/{v0-COaQncfZ.mjs → v0-A4GLojAr.mjs} +2 -1
- package/dist/{v0-Bx5Mi7qE.mjs → v0-BU39ZO4s.mjs} +21 -10
- package/dist/{v0-WzS4GAIp.d.mts → v0-D7NA38Vu.d.mts} +1 -1
- package/dist/{v0-CG9o3on7.mjs → v0-DL9JtgCO.mjs} +1 -1
- package/dist/{v0-QIjffi4E.d.mts → v0-DotXXGXU.d.mts} +1 -1
- package/dist/{v0-CHK79mZX.mjs → v0-SwedwC_b.mjs} +3 -4
- package/dist/{v0-DWZ_N2GG.d.mts → v0-rSxMW8FJ.d.mts} +1 -1
- package/dist/{virtual-CrgGA-o6.d.mts → virtual-C-NsLue0.d.mts} +1 -1
- package/dist/{virtual-UumJ2BQ9.mjs → virtual-DFvOoeG6.mjs} +2 -1
- package/package.json +10 -7
- package/dist/adapters-BHD1lc-K.mjs +0 -4
- package/dist/adapters-BYLWDmYl.mjs +0 -5
- package/dist/adapters-BxrTL8E3.mjs +0 -4
- package/dist/adapters-CBgBd7VE.mjs +0 -2
- package/dist/adapters-D64TORDu.mjs +0 -2
- package/dist/flagsmith-Cu93hUIF.mjs +0 -48
- package/dist/launchdarkly-CLGtJCsI.mjs +0 -38
- package/dist/posthog-B5s_KGhy.mjs +0 -47
- /package/dist/{adapter-BEJXp-ie.d.mts → adapter-BYzU4Ey5.d.mts} +0 -0
- /package/dist/{adapter-B6IrPwib.d.mts → adapter-C5F2huij.d.mts} +0 -0
- /package/dist/{adapter-BSKx9WQ7.mjs → adapter-CTsckKAR.mjs} +0 -0
- /package/dist/{adapter-D8zizQVP.mjs → adapter-bwZo7b5p.mjs} +0 -0
- /package/dist/{index-DHmuwBdv.d.mts → index-B9YmdGZ3.d.mts} +0 -0
- /package/dist/{index-PvkxPev3.d.mts → index-BgCkepP_.d.mts} +0 -0
- /package/dist/{index-BOn0AAue.d.mts → index-CROz1VSV.d.mts} +0 -0
- /package/dist/{index-H_hl6-4b.d.mts → index-DXMlYoiQ.d.mts} +0 -0
- /package/dist/{index-C6NnIuDv.d.mts → index-DidZKN6S.d.mts} +0 -0
- /package/dist/{index-CKKoxA9V.d.mts → index-FtE8MXJ_.d.mts} +0 -0
- /package/dist/{memory-BMpJcQjQ.mjs → memory-BJjkQzxs.mjs} +0 -0
- /package/dist/{memory-D-JbdKXg.d.mts → memory-DbyZeNjg.d.mts} +0 -0
- /package/dist/{toArray-CD2-OxAz.mjs → toArray-C89ZaEuF.mjs} +0 -0
- /package/dist/{v0-Dvu8fMl3.mjs → v0-UNyykD27.mjs} +0 -0
|
@@ -0,0 +1,4077 @@
|
|
|
1
|
+
import { i as SUPPORTS_MUTATION_OBSERVER, r as SUPPORTS_MATCH_MEDIA, t as IN_BROWSER } from "./globals-C1X-9O7K.mjs";
|
|
2
|
+
import { V0Error, clamp, instanceExists, isArray, isElement, isFunction, isNaN, isNull, isNullOrUndefined, isNumber, isString, isThenable, isUndefined, mergeDeep, useId as useId$1 } from "./utilities/index.mjs";
|
|
3
|
+
import { i as useContext, n as createContext, t as createTrinity } from "./createTrinity-z-3q9QWR.mjs";
|
|
4
|
+
import { a as createPlugin, i as useLogger, o as createPluginContext } from "./useLogger-D7_hm4e8.mjs";
|
|
5
|
+
import { t as createRegistry } from "./createRegistry-Cb6ftNMU.mjs";
|
|
6
|
+
import { i as createModel } from "./createSelection-BMvXj84C.mjs";
|
|
7
|
+
import { a as useLocale } from "./useLocale-CTwn8uZk.mjs";
|
|
8
|
+
import { t as toArray } from "./toArray-C89ZaEuF.mjs";
|
|
9
|
+
import { n as useEventListener, r as useWindowEventListener } from "./useEventListener-BK_uaXZv.mjs";
|
|
10
|
+
import { J as useResizeObserver, M as ComboboxAdapter, Y as createObserver, b as createInput, nt as useToggleScope, tt as useHydration } from "./useRovingFocus-BdnSUVrD.mjs";
|
|
11
|
+
import { t as createGroup } from "./createGroup-BTG6xcyp.mjs";
|
|
12
|
+
import { a as extractLeaves, t as createDataTable } from "./createDataTable-CBfnCUKU.mjs";
|
|
13
|
+
import { computed, effectScope, isRef, onScopeDispose, onWatcherCleanup, reactive, readonly, ref, shallowReactive, shallowReadonly, shallowRef, toRef, toValue, unref, watch, watchEffect } from "vue";
|
|
14
|
+
//#region src/composables/createCombobox/adapters/server.ts
|
|
15
|
+
/**
|
|
16
|
+
* @module createCombobox/adapters/server
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Server-side combobox adapter. All filtering is performed externally.
|
|
20
|
+
* The adapter is a pass-through — it shows all registered items and
|
|
21
|
+
* exposes loading state for the consumer to control.
|
|
22
|
+
*
|
|
23
|
+
* The consumer watches `query` via useComboboxContext() or slot props
|
|
24
|
+
* and provides filtered items reactively.
|
|
25
|
+
*/
|
|
26
|
+
var ServerComboboxAdapter = class extends ComboboxAdapter {
|
|
27
|
+
setup(context) {
|
|
28
|
+
const isLoading = shallowRef(false);
|
|
29
|
+
return {
|
|
30
|
+
filtered: toRef(() => {
|
|
31
|
+
return new Set(context.items.value.map((t) => t.id));
|
|
32
|
+
}),
|
|
33
|
+
isLoading,
|
|
34
|
+
isEmpty: toRef(() => context.items.value.length === 0)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/composables/createSortable/index.ts
|
|
40
|
+
/**
|
|
41
|
+
* @module createSortable
|
|
42
|
+
*
|
|
43
|
+
* @see https://0.vuetifyjs.com/composables/data/create-sortable
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Headless ordered-list primitive. Extends `createModel` with mutation primitives
|
|
47
|
+
* (`swap`, `reorder`) and an enriched `move:ticket` event on the registry event
|
|
48
|
+
* bus. Honors `disabled` as a real mutation gate at both root and per-ticket scope.
|
|
49
|
+
*
|
|
50
|
+
* Pure logical composable — no DnD, no DOM, no keyboard. Drag-and-drop integration
|
|
51
|
+
* composes with `useDragDrop`; keyboard reorder composes with `useVirtualFocus`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { createSortable } from '@vuetify/v0'
|
|
56
|
+
* import type { SortableTicketInput } from '@vuetify/v0'
|
|
57
|
+
*
|
|
58
|
+
* interface TaskTicket extends SortableTicketInput {
|
|
59
|
+
* value: { id: number; label: string }
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* const sortable = createSortable<TaskTicket>()
|
|
63
|
+
* const a = sortable.register({ value: { id: 1, label: 'A' } })
|
|
64
|
+
* const b = sortable.register({ value: { id: 2, label: 'B' } })
|
|
65
|
+
*
|
|
66
|
+
* sortable.move(a.id, 1)
|
|
67
|
+
* sortable.on('move:ticket', ({ ticket, from, to }) => {
|
|
68
|
+
* console.log(`${ticket.value.label} moved ${from} → ${to}`)
|
|
69
|
+
* })
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Create an ordered-list state primitive.
|
|
74
|
+
*
|
|
75
|
+
* @param options Sortable options.
|
|
76
|
+
* @returns A {@link SortableContext} that extends {@link ModelContext}.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const sortable = createSortable<Todo>()
|
|
81
|
+
* sortable.onboard(todos.value.map(value => ({ value })))
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
function createSortable(_options = {}) {
|
|
85
|
+
const { disabled, ...options } = _options;
|
|
86
|
+
const model = createModel({
|
|
87
|
+
...options,
|
|
88
|
+
events: true,
|
|
89
|
+
disabled
|
|
90
|
+
});
|
|
91
|
+
const logger = useLogger();
|
|
92
|
+
function move(id, toIndex) {
|
|
93
|
+
if (toValue(disabled)) return void 0;
|
|
94
|
+
const ticket = model.get(id);
|
|
95
|
+
if (!ticket) return void 0;
|
|
96
|
+
if (toValue(ticket.disabled)) return void 0;
|
|
97
|
+
const from = ticket.index;
|
|
98
|
+
const result = model.move(id, toIndex);
|
|
99
|
+
if (result && result.index !== from) model.emit("move:ticket", {
|
|
100
|
+
ticket: result,
|
|
101
|
+
from,
|
|
102
|
+
to: result.index
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function swap(a, b) {
|
|
107
|
+
if (toValue(disabled)) return;
|
|
108
|
+
if (a === b) return;
|
|
109
|
+
const ta = model.get(a);
|
|
110
|
+
const tb = model.get(b);
|
|
111
|
+
if (!ta || !tb) return;
|
|
112
|
+
if (toValue(ta.disabled) || toValue(tb.disabled)) return;
|
|
113
|
+
const indexA = ta.index;
|
|
114
|
+
const indexB = tb.index;
|
|
115
|
+
model.batch(() => {
|
|
116
|
+
const ra = model.move(a, indexB);
|
|
117
|
+
if (ra && ra.index !== indexA) model.emit("move:ticket", {
|
|
118
|
+
ticket: ra,
|
|
119
|
+
from: indexA,
|
|
120
|
+
to: ra.index
|
|
121
|
+
});
|
|
122
|
+
const rb = model.move(b, indexA);
|
|
123
|
+
if (rb && rb.index !== indexB) model.emit("move:ticket", {
|
|
124
|
+
ticket: rb,
|
|
125
|
+
from: indexB,
|
|
126
|
+
to: rb.index
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function reorder(ids) {
|
|
131
|
+
if (toValue(disabled)) return;
|
|
132
|
+
const currentSize = model.size;
|
|
133
|
+
if (ids.length !== currentSize) {
|
|
134
|
+
logger.warn(`Reorder expected ${currentSize} ids, got ${ids.length}.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const known = new Set(model.keys());
|
|
138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const id of ids) {
|
|
140
|
+
if (!known.has(id)) {
|
|
141
|
+
logger.warn(`Reorder contains unknown id "${String(id)}".`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (seen.has(id)) {
|
|
145
|
+
logger.warn(`Reorder contains duplicate id "${String(id)}".`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
seen.add(id);
|
|
149
|
+
}
|
|
150
|
+
const froms = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
const ticket = model.get(id);
|
|
153
|
+
if (ticket) froms.set(id, ticket.index);
|
|
154
|
+
}
|
|
155
|
+
model.batch(() => {
|
|
156
|
+
model.reorder(ids);
|
|
157
|
+
for (const [index, id] of ids.entries()) {
|
|
158
|
+
const from = froms.get(id);
|
|
159
|
+
if (from === index) continue;
|
|
160
|
+
const ticket = model.get(id);
|
|
161
|
+
if (ticket) model.emit("move:ticket", {
|
|
162
|
+
ticket,
|
|
163
|
+
from,
|
|
164
|
+
to: index
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
...model,
|
|
171
|
+
move,
|
|
172
|
+
swap,
|
|
173
|
+
reorder,
|
|
174
|
+
get size() {
|
|
175
|
+
return model.size;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/composables/createDataGrid/editing.ts
|
|
181
|
+
/**
|
|
182
|
+
* @module createDataGrid/editing
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* Cell editing state management. Tracks active cell, validation errors,
|
|
186
|
+
* and dirty (staged but uncommitted) edits. Does not mutate source data —
|
|
187
|
+
* commit fires a callback for the consumer to handle.
|
|
188
|
+
*
|
|
189
|
+
* When a `registry` is provided, editing state for rows that get
|
|
190
|
+
* unregistered (or wiped via `clear`) is pruned automatically so consumers
|
|
191
|
+
* can never observe `active` pointing at a phantom row or stale entries
|
|
192
|
+
* lingering in `dirty`.
|
|
193
|
+
*
|
|
194
|
+
* @internal Consumed only by `createDataGrid`. This is a fully decoupled 2D
|
|
195
|
+
* cell-edit primitive — it imports nothing from `createDataGrid` /
|
|
196
|
+
* `createDataTable` (only `#v0/utilities`, `#v0/types`, and Vue), so it is a
|
|
197
|
+
* latent composable. It is parameterized over a row type `T` (defaulting to
|
|
198
|
+
* `unknown`), but a type parameter is not an import — the primitive stays
|
|
199
|
+
* fully decoupled from the grid's concrete ticket types and deliberately
|
|
200
|
+
* row-agnostic: `T` is intentionally unconstrained, so do not narrow it to
|
|
201
|
+
* the grid's `Record<string, unknown>` when promoting. Promote it to a
|
|
202
|
+
* standalone `createCellEditing/`
|
|
203
|
+
* composable the moment a second consumer appears (e.g. an editable
|
|
204
|
+
* `createDataTable`). See `.claude/rules/composables.md` §"Sub-modules:
|
|
205
|
+
* inline, private sibling, or promote".
|
|
206
|
+
*/
|
|
207
|
+
/**
|
|
208
|
+
* Creates cell editing state for a data grid.
|
|
209
|
+
*
|
|
210
|
+
* @param options Cell editing configuration including columns and commit callback
|
|
211
|
+
* @returns Cell editing state and controls
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const editing = createCellEditing({
|
|
216
|
+
* columns: [{ id: 'name', editable: true }],
|
|
217
|
+
* registry: table,
|
|
218
|
+
* onEdit (row, column, value) { ... },
|
|
219
|
+
* })
|
|
220
|
+
*
|
|
221
|
+
* editing.edit(1, 'name')
|
|
222
|
+
* editing.commit('Alice')
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
function createCellEditing(options) {
|
|
226
|
+
const { columns, onEdit, lookup, registry } = options;
|
|
227
|
+
function resolveColumn(column) {
|
|
228
|
+
for (const col of toValue(columns)) if (col.id === column) return col;
|
|
229
|
+
}
|
|
230
|
+
const active = shallowRef(null);
|
|
231
|
+
const error = shallowRef(null);
|
|
232
|
+
const dirty = shallowReactive(/* @__PURE__ */ new Map());
|
|
233
|
+
function edit(row, column) {
|
|
234
|
+
const col = resolveColumn(column);
|
|
235
|
+
if (!col) return;
|
|
236
|
+
if (/* @__PURE__ */ isFunction(col.editable)) {
|
|
237
|
+
const item = lookup?.(row);
|
|
238
|
+
if (/* @__PURE__ */ isUndefined(item) || !col.editable(item)) return;
|
|
239
|
+
} else if (col.editable !== true) return;
|
|
240
|
+
error.value = null;
|
|
241
|
+
active.value = {
|
|
242
|
+
row,
|
|
243
|
+
column
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function commit(value) {
|
|
247
|
+
const cell = active.value;
|
|
248
|
+
if (!cell) return;
|
|
249
|
+
const col = resolveColumn(cell.column);
|
|
250
|
+
if (col?.validate) {
|
|
251
|
+
const result = col.validate(value, lookup?.(cell.row));
|
|
252
|
+
if (result !== true && /* @__PURE__ */ isString(result) && result.length > 0) {
|
|
253
|
+
error.value = result;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const entry = dirty.get(cell.row);
|
|
258
|
+
if (entry) {
|
|
259
|
+
entry.delete(cell.column);
|
|
260
|
+
if (entry.size === 0) dirty.delete(cell.row);
|
|
261
|
+
}
|
|
262
|
+
error.value = null;
|
|
263
|
+
active.value = null;
|
|
264
|
+
onEdit?.(cell.row, cell.column, value);
|
|
265
|
+
}
|
|
266
|
+
function cancel() {
|
|
267
|
+
const cell = active.value;
|
|
268
|
+
if (cell) {
|
|
269
|
+
const entry = dirty.get(cell.row);
|
|
270
|
+
if (entry) {
|
|
271
|
+
entry.delete(cell.column);
|
|
272
|
+
if (entry.size === 0) dirty.delete(cell.row);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
error.value = null;
|
|
276
|
+
active.value = null;
|
|
277
|
+
}
|
|
278
|
+
function onUnregister(data) {
|
|
279
|
+
const ticket = data;
|
|
280
|
+
if (active.value?.row === ticket.id) {
|
|
281
|
+
active.value = null;
|
|
282
|
+
error.value = null;
|
|
283
|
+
}
|
|
284
|
+
dirty.delete(ticket.id);
|
|
285
|
+
}
|
|
286
|
+
function onClear() {
|
|
287
|
+
active.value = null;
|
|
288
|
+
error.value = null;
|
|
289
|
+
dirty.clear();
|
|
290
|
+
}
|
|
291
|
+
if (registry) {
|
|
292
|
+
registry.on("unregister:ticket", onUnregister);
|
|
293
|
+
registry.on("clear:registry", onClear);
|
|
294
|
+
onScopeDispose(() => {
|
|
295
|
+
registry.off("unregister:ticket", onUnregister);
|
|
296
|
+
registry.off("clear:registry", onClear);
|
|
297
|
+
}, true);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
active,
|
|
301
|
+
edit,
|
|
302
|
+
commit,
|
|
303
|
+
cancel,
|
|
304
|
+
error,
|
|
305
|
+
dirty
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/composables/createDataGrid/layout.ts
|
|
310
|
+
/**
|
|
311
|
+
* @module createDataGrid/layout
|
|
312
|
+
*
|
|
313
|
+
* @remarks
|
|
314
|
+
* Manages column layout state for data grids: sizing (percentages 0-100),
|
|
315
|
+
* pinning (left/right/scrollable regions), delta-based resizing compatible
|
|
316
|
+
* with the Splitter two-panel model, column reordering, and visibility.
|
|
317
|
+
*
|
|
318
|
+
* Column existence, display order, AND grid layout config (size, pin,
|
|
319
|
+
* min/max, resizable, reorderable) all live on the table's columns registry
|
|
320
|
+
* (passed in as the only argument). `createRegistry` preserves the extra
|
|
321
|
+
* grid fields on each ticket, so layout reads its config straight off the
|
|
322
|
+
* registered ticket — there is no separate defs array. Columns onboarded
|
|
323
|
+
* before OR after the layout is built are picked up identically: an initial
|
|
324
|
+
* loop seeds the already-registered set and a `register:ticket` handler seeds
|
|
325
|
+
* the rest. Removed columns have their pin/size/visibility state pruned via
|
|
326
|
+
* unregister events.
|
|
327
|
+
*
|
|
328
|
+
* Sizing uses percentages so it is compatible with the Splitter component.
|
|
329
|
+
* Offsets are computed per-region (left, scrollable, right) independently
|
|
330
|
+
* and over visible columns only — hidden columns are excluded from the
|
|
331
|
+
* render set without auto-redistribution (headless: the consumer rebalances
|
|
332
|
+
* via `distribute()` or CSS).
|
|
333
|
+
*
|
|
334
|
+
* @internal Private sibling of `createDataGrid`, and intentionally NOT a
|
|
335
|
+
* promotion candidate: it is bound to the table's `DataTableColumnTicket`
|
|
336
|
+
* types, so it is parent-specific glue rather than a reusable primitive. See
|
|
337
|
+
* `.claude/rules/composables.md` §"Sub-modules: inline, private sibling, or
|
|
338
|
+
* promote".
|
|
339
|
+
*/
|
|
340
|
+
const DEFAULT_EXTRAS = {
|
|
341
|
+
minSize: 2,
|
|
342
|
+
maxSize: 100,
|
|
343
|
+
resizable: true,
|
|
344
|
+
reorderable: true
|
|
345
|
+
};
|
|
346
|
+
function offsets(cols, reverse = false) {
|
|
347
|
+
let offset = 0;
|
|
348
|
+
const start = reverse ? cols.length - 1 : 0;
|
|
349
|
+
const step = reverse ? -1 : 1;
|
|
350
|
+
const end = reverse ? -1 : cols.length;
|
|
351
|
+
for (let i = start; i !== end; i += step) {
|
|
352
|
+
cols[i].offset = offset;
|
|
353
|
+
offset += cols[i].size;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Creates a column layout manager for a data grid.
|
|
358
|
+
*
|
|
359
|
+
* Reads column existence, display order, AND grid layout config (size, pin,
|
|
360
|
+
* min/max, resizable, reorderable) from the table's columns registry. The
|
|
361
|
+
* grid fields ride on each ticket — `createRegistry` preserves them — so
|
|
362
|
+
* there is no separate defs array. Visibility is layout-local view state
|
|
363
|
+
* (not a ticket field) kept here and reconciled via register / unregister /
|
|
364
|
+
* clear events.
|
|
365
|
+
*
|
|
366
|
+
* Generic over the registry's input/output ticket types so the caller's
|
|
367
|
+
* concrete grid column ticket (`DataGridColumnTicket`) flows through. Layout
|
|
368
|
+
* only ever reads from the registry (`values`, `keys`, `on`, `reorder`,
|
|
369
|
+
* `move`, `lookup`) — it never registers — so the output ticket need only be
|
|
370
|
+
* readable as a {@link GridColumnDef}.
|
|
371
|
+
*
|
|
372
|
+
* @param cols The table's columns registry — source of truth for which
|
|
373
|
+
* columns exist, in what order, and their layout config
|
|
374
|
+
* @returns Column layout state and mutation methods
|
|
375
|
+
*/
|
|
376
|
+
function createColumnLayout(cols) {
|
|
377
|
+
const logger = useLogger();
|
|
378
|
+
const configs = /* @__PURE__ */ new Map();
|
|
379
|
+
const group = createGroup({ multiple: true });
|
|
380
|
+
const sizes = shallowReactive(/* @__PURE__ */ new Map());
|
|
381
|
+
const hidden = shallowReactive(/* @__PURE__ */ new Set());
|
|
382
|
+
const order = [];
|
|
383
|
+
function record(id) {
|
|
384
|
+
if (!order.includes(id)) order.push(id);
|
|
385
|
+
}
|
|
386
|
+
/** Even share over the currently-registered leaves: explicit sizes are
|
|
387
|
+
* honored, unsized columns split the remaining percentage. */
|
|
388
|
+
function share(id) {
|
|
389
|
+
const leaves = /* @__PURE__ */ extractLeaves(cols.values());
|
|
390
|
+
let used = 0;
|
|
391
|
+
let unsized = 0;
|
|
392
|
+
for (const leaf of leaves) {
|
|
393
|
+
const size = configs.get(String(leaf.id))?.size;
|
|
394
|
+
if (/* @__PURE__ */ isUndefined(size)) unsized++;
|
|
395
|
+
else used += size;
|
|
396
|
+
}
|
|
397
|
+
const own = configs.get(id)?.size;
|
|
398
|
+
if (!/* @__PURE__ */ isUndefined(own)) return own;
|
|
399
|
+
const remainder = Math.max(0, 100 - used);
|
|
400
|
+
return unsized > 0 ? remainder / unsized : 0;
|
|
401
|
+
}
|
|
402
|
+
function seedLeaf(leaf) {
|
|
403
|
+
const id = String(leaf.id);
|
|
404
|
+
if (!configs.has(id)) configs.set(id, {
|
|
405
|
+
size: leaf.size,
|
|
406
|
+
minSize: leaf.minSize ?? DEFAULT_EXTRAS.minSize,
|
|
407
|
+
maxSize: leaf.maxSize ?? DEFAULT_EXTRAS.maxSize,
|
|
408
|
+
resizable: leaf.resizable ?? DEFAULT_EXTRAS.resizable,
|
|
409
|
+
reorderable: leaf.reorderable ?? DEFAULT_EXTRAS.reorderable,
|
|
410
|
+
pinned: leaf.pinned ?? false
|
|
411
|
+
});
|
|
412
|
+
if (!group.has(id)) group.register({
|
|
413
|
+
id,
|
|
414
|
+
value: id
|
|
415
|
+
});
|
|
416
|
+
if (!sizes.has(id)) sizes.set(id, share(id));
|
|
417
|
+
for (const other of /* @__PURE__ */ extractLeaves(cols.values())) {
|
|
418
|
+
const otherId = String(other.id);
|
|
419
|
+
if (configs.has(otherId) && /* @__PURE__ */ isUndefined(configs.get(otherId)?.size)) sizes.set(otherId, share(otherId));
|
|
420
|
+
}
|
|
421
|
+
const config = configs.get(id);
|
|
422
|
+
if (config.pinned === "left") group.select(id);
|
|
423
|
+
else if (config.pinned === "right") group.mix(id);
|
|
424
|
+
}
|
|
425
|
+
function dropLeaf(id) {
|
|
426
|
+
group.unregister(id);
|
|
427
|
+
configs.delete(id);
|
|
428
|
+
sizes.delete(id);
|
|
429
|
+
hidden.delete(id);
|
|
430
|
+
}
|
|
431
|
+
function leavesOf(ticket) {
|
|
432
|
+
return /* @__PURE__ */ extractLeaves([ticket]);
|
|
433
|
+
}
|
|
434
|
+
for (const ticket of cols.values()) {
|
|
435
|
+
record(String(ticket.id));
|
|
436
|
+
for (const leaf of leavesOf(ticket)) seedLeaf(leaf);
|
|
437
|
+
}
|
|
438
|
+
cols.on("register:ticket", (ticket) => {
|
|
439
|
+
record(String(ticket.id));
|
|
440
|
+
for (const leaf of leavesOf(ticket)) seedLeaf(leaf);
|
|
441
|
+
});
|
|
442
|
+
cols.on("unregister:ticket", (ticket) => {
|
|
443
|
+
const top = order.indexOf(String(ticket.id));
|
|
444
|
+
if (top !== -1) order.splice(top, 1);
|
|
445
|
+
for (const leaf of leavesOf(ticket)) dropLeaf(String(leaf.id));
|
|
446
|
+
});
|
|
447
|
+
cols.on("clear:registry", () => {
|
|
448
|
+
group.clear();
|
|
449
|
+
configs.clear();
|
|
450
|
+
sizes.clear();
|
|
451
|
+
hidden.clear();
|
|
452
|
+
order.length = 0;
|
|
453
|
+
});
|
|
454
|
+
function position(id) {
|
|
455
|
+
if (group.selectedIds.has(id)) return "left";
|
|
456
|
+
if (group.mixed(id)) return "right";
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
function resolve(includeHidden) {
|
|
460
|
+
const resolved = [];
|
|
461
|
+
let index = 0;
|
|
462
|
+
for (const ticket of /* @__PURE__ */ extractLeaves(cols.values())) {
|
|
463
|
+
const id = String(ticket.id);
|
|
464
|
+
const visible = !hidden.has(id);
|
|
465
|
+
if (!includeHidden && !visible) continue;
|
|
466
|
+
const meta = configs.get(id) ?? DEFAULT_EXTRAS;
|
|
467
|
+
resolved.push({
|
|
468
|
+
id,
|
|
469
|
+
index: index++,
|
|
470
|
+
size: sizes.get(id) ?? 0,
|
|
471
|
+
offset: 0,
|
|
472
|
+
pinned: position(id),
|
|
473
|
+
resizable: meta.resizable,
|
|
474
|
+
reorderable: meta.reorderable,
|
|
475
|
+
minSize: meta.minSize,
|
|
476
|
+
maxSize: meta.maxSize,
|
|
477
|
+
visible
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return resolved;
|
|
481
|
+
}
|
|
482
|
+
function split(resolved) {
|
|
483
|
+
const left = [];
|
|
484
|
+
const scrollable = [];
|
|
485
|
+
const right = [];
|
|
486
|
+
for (const col of resolved) if (col.pinned === "left") left.push(col);
|
|
487
|
+
else if (col.pinned === "right") right.push(col);
|
|
488
|
+
else scrollable.push(col);
|
|
489
|
+
offsets(left);
|
|
490
|
+
offsets(scrollable);
|
|
491
|
+
offsets(right, true);
|
|
492
|
+
return {
|
|
493
|
+
left,
|
|
494
|
+
scrollable,
|
|
495
|
+
right
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const pinned = computed(() => split(resolve(false)));
|
|
499
|
+
const columns = computed(() => {
|
|
500
|
+
const { left, scrollable, right } = pinned.value;
|
|
501
|
+
return [
|
|
502
|
+
...left,
|
|
503
|
+
...scrollable,
|
|
504
|
+
...right
|
|
505
|
+
];
|
|
506
|
+
});
|
|
507
|
+
const allColumns = computed(() => resolve(true));
|
|
508
|
+
function pin(id, pos) {
|
|
509
|
+
if (!group.has(id)) return;
|
|
510
|
+
group.unselect(id);
|
|
511
|
+
group.unmix(id);
|
|
512
|
+
if (pos === "left") group.select(id);
|
|
513
|
+
else if (pos === "right") group.mix(id);
|
|
514
|
+
}
|
|
515
|
+
function resize(id, delta) {
|
|
516
|
+
const { left, scrollable, right } = pinned.value;
|
|
517
|
+
const all = [
|
|
518
|
+
...left,
|
|
519
|
+
...scrollable,
|
|
520
|
+
...right
|
|
521
|
+
];
|
|
522
|
+
const target = all.find((c) => c.id === id);
|
|
523
|
+
if (!target || !target.resizable) return;
|
|
524
|
+
const allIndex = all.findIndex((c) => c.id === id);
|
|
525
|
+
if (allIndex === -1 || allIndex === all.length - 1) return;
|
|
526
|
+
const neighbor = all[allIndex + 1];
|
|
527
|
+
const total = target.size + neighbor.size;
|
|
528
|
+
const lower = Math.max(target.minSize, total - neighbor.maxSize);
|
|
529
|
+
const upper = Math.min(target.maxSize, total - neighbor.minSize);
|
|
530
|
+
const clamped = /* @__PURE__ */ clamp(target.size + delta, lower, upper);
|
|
531
|
+
sizes.set(id, clamped);
|
|
532
|
+
sizes.set(neighbor.id, total - clamped);
|
|
533
|
+
}
|
|
534
|
+
function reorder(from, to) {
|
|
535
|
+
if (from === to) return;
|
|
536
|
+
const tops = [...cols.values()];
|
|
537
|
+
const ids = [];
|
|
538
|
+
const leafToTop = [];
|
|
539
|
+
for (const [top, ticket] of tops.entries()) for (const leaf of /* @__PURE__ */ extractLeaves([ticket])) {
|
|
540
|
+
ids.push(String(leaf.id));
|
|
541
|
+
leafToTop.push(top);
|
|
542
|
+
}
|
|
543
|
+
const visible = ids.filter((id) => !hidden.has(id));
|
|
544
|
+
const fromId = visible[from];
|
|
545
|
+
const toId = visible[to];
|
|
546
|
+
if (/* @__PURE__ */ isUndefined(fromId) || /* @__PURE__ */ isUndefined(toId)) return;
|
|
547
|
+
const fromTop = leafToTop[ids.indexOf(fromId)];
|
|
548
|
+
const toTop = leafToTop[ids.indexOf(toId)];
|
|
549
|
+
if (/* @__PURE__ */ isUndefined(fromTop) || /* @__PURE__ */ isUndefined(toTop) || fromTop === toTop) return;
|
|
550
|
+
if (tops[fromTop]?.reorderable === false) return;
|
|
551
|
+
const id = cols.lookup(fromTop);
|
|
552
|
+
if (/* @__PURE__ */ isUndefined(id)) return;
|
|
553
|
+
cols.move(id, toTop);
|
|
554
|
+
}
|
|
555
|
+
function distribute(incoming) {
|
|
556
|
+
const tickets = /* @__PURE__ */ extractLeaves(cols.values());
|
|
557
|
+
if (incoming.length !== tickets.length) {
|
|
558
|
+
logger.warn(`createDataGrid: distribute() expected ${tickets.length} sizes but received ${incoming.length}; ignoring.`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const sum = incoming.reduce((acc, value) => acc + value, 0);
|
|
562
|
+
if (sum <= 0) {
|
|
563
|
+
logger.warn(`createDataGrid: distribute() received sizes summing to ${sum}; expected a positive total.`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const metas = tickets.map((ticket) => configs.get(String(ticket.id)) ?? DEFAULT_EXTRAS);
|
|
567
|
+
let current = incoming.map((value, i) => /* @__PURE__ */ clamp(value / sum * 100, metas[i].minSize, metas[i].maxSize));
|
|
568
|
+
let residual = 100 - current.reduce((acc, value) => acc + value, 0);
|
|
569
|
+
const max = tickets.length * 2;
|
|
570
|
+
for (let pass = 0; pass < max && Math.abs(residual) >= 1e-6; pass++) {
|
|
571
|
+
const room = current.map((value, i) => residual > 0 ? metas[i].maxSize - value : value - metas[i].minSize);
|
|
572
|
+
const available = room.reduce((acc, value) => acc + Math.max(0, value), 0);
|
|
573
|
+
if (available <= 0) break;
|
|
574
|
+
current = current.map((value, i) => room[i] <= 0 ? value : value + residual * (room[i] / available));
|
|
575
|
+
residual = 100 - current.reduce((acc, value) => acc + value, 0);
|
|
576
|
+
}
|
|
577
|
+
if (Math.abs(residual) >= 1e-6) logger.warn(`createDataGrid: distribute() could not reach 100 within min/max constraints (off by ${residual.toFixed(2)})`);
|
|
578
|
+
for (const [i, ticket] of tickets.entries()) sizes.set(String(ticket.id), current[i]);
|
|
579
|
+
}
|
|
580
|
+
function show(id) {
|
|
581
|
+
hidden.delete(id);
|
|
582
|
+
}
|
|
583
|
+
function hide(id) {
|
|
584
|
+
hidden.add(id);
|
|
585
|
+
}
|
|
586
|
+
function toggle(id) {
|
|
587
|
+
if (hidden.has(id)) hidden.delete(id);
|
|
588
|
+
else hidden.add(id);
|
|
589
|
+
}
|
|
590
|
+
function reset() {
|
|
591
|
+
for (const ticket of /* @__PURE__ */ extractLeaves(cols.values())) {
|
|
592
|
+
const id = String(ticket.id);
|
|
593
|
+
sizes.set(id, share(id));
|
|
594
|
+
}
|
|
595
|
+
cols.reorder([...order]);
|
|
596
|
+
hidden.clear();
|
|
597
|
+
for (const ticket of /* @__PURE__ */ extractLeaves(cols.values())) {
|
|
598
|
+
const id = String(ticket.id);
|
|
599
|
+
group.unselect(id);
|
|
600
|
+
group.unmix(id);
|
|
601
|
+
}
|
|
602
|
+
for (const [id, config] of configs) if (config.pinned === "left") group.select(id);
|
|
603
|
+
else if (config.pinned === "right") group.mix(id);
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
pinned,
|
|
607
|
+
columns,
|
|
608
|
+
all: allColumns,
|
|
609
|
+
pin,
|
|
610
|
+
resize,
|
|
611
|
+
reorder,
|
|
612
|
+
distribute,
|
|
613
|
+
show,
|
|
614
|
+
hide,
|
|
615
|
+
toggle,
|
|
616
|
+
reset
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/composables/createDataGrid/spanning.ts
|
|
621
|
+
/**
|
|
622
|
+
* @module createDataGrid/spanning
|
|
623
|
+
*
|
|
624
|
+
* @remarks
|
|
625
|
+
* Computes a row span map from visible items. For each cell, determines
|
|
626
|
+
* rowSpan and whether it's hidden (covered by a span from a previous row).
|
|
627
|
+
* Spans do not cross page boundaries. Each row is addressed by a key: either a
|
|
628
|
+
* `key(item, index)` projection (the grid passes the registry ticket id) or,
|
|
629
|
+
* absent that, the row's `itemKey` value. Rows whose resolved key is
|
|
630
|
+
* `undefined` cannot be addressed in the resulting map and are skipped.
|
|
631
|
+
*
|
|
632
|
+
* @internal Consumed only by `createDataGrid`. Fully decoupled (items ref +
|
|
633
|
+
* columns + key + span fn — no grid/table types), so it is a latent composable.
|
|
634
|
+
* Promote it to a standalone `createRowSpanning/` composable on a second
|
|
635
|
+
* consumer. See `.claude/rules/composables.md` §"Sub-modules: inline, private
|
|
636
|
+
* sibling, or promote".
|
|
637
|
+
*/
|
|
638
|
+
/**
|
|
639
|
+
* Computes a row span map from visible items.
|
|
640
|
+
*
|
|
641
|
+
* @param options Row spanning configuration
|
|
642
|
+
* @returns A computed map of item ID to column to SpanEntry
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* const spans = createRowSpanning({
|
|
647
|
+
* items: computed(() => grid.items.value),
|
|
648
|
+
* columns: ['group', 'ticker', 'value'],
|
|
649
|
+
* span: (item, column) => column === 'group' ? 3 : 1,
|
|
650
|
+
* })
|
|
651
|
+
*
|
|
652
|
+
* spans.value.get(id)?.get('group') // { rowSpan: 3, hidden: false }
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
function createRowSpanning(options) {
|
|
656
|
+
const { items, columns, key, itemKey = "id", span } = options;
|
|
657
|
+
return computed(() => {
|
|
658
|
+
const result = /* @__PURE__ */ new Map();
|
|
659
|
+
if (!span) return result;
|
|
660
|
+
const list = items.value;
|
|
661
|
+
const resolved = toValue(columns);
|
|
662
|
+
const covered = Array.from({ length: resolved.length }).fill(0);
|
|
663
|
+
for (let row = 0; row < list.length; row++) {
|
|
664
|
+
const item = list[row];
|
|
665
|
+
const id = key ? key(item, row) : item[itemKey];
|
|
666
|
+
if (/* @__PURE__ */ isUndefined(id)) continue;
|
|
667
|
+
const cells = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const [col, column] of resolved.entries()) if (covered[col] > 0) {
|
|
669
|
+
cells.set(column, {
|
|
670
|
+
rowSpan: 1,
|
|
671
|
+
hidden: true
|
|
672
|
+
});
|
|
673
|
+
covered[col]--;
|
|
674
|
+
} else {
|
|
675
|
+
const raw = span(item, column);
|
|
676
|
+
const rowSpan = Math.min(Math.max(1, /* @__PURE__ */ isNaN(raw) ? 1 : raw), list.length - row);
|
|
677
|
+
cells.set(column, {
|
|
678
|
+
rowSpan,
|
|
679
|
+
hidden: false
|
|
680
|
+
});
|
|
681
|
+
if (rowSpan > 1) covered[col] = rowSpan - 1;
|
|
682
|
+
}
|
|
683
|
+
result.set(id, cells);
|
|
684
|
+
}
|
|
685
|
+
return result;
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
//#region src/composables/createDataGrid/index.ts
|
|
690
|
+
/**
|
|
691
|
+
* @module createDataGrid
|
|
692
|
+
*
|
|
693
|
+
* @see https://0.vuetifyjs.com/composables/data/create-data-grid
|
|
694
|
+
*
|
|
695
|
+
* @remarks
|
|
696
|
+
* Main factory that wires together column layout, cell editing, row ordering,
|
|
697
|
+
* and row spanning on top of a createDataTable pipeline. Row ordering is
|
|
698
|
+
* provided by a createSortable instance synced to the table's row registry
|
|
699
|
+
* via register / unregister events; the ordered ids are applied post-sort,
|
|
700
|
+
* pre-pagination by overriding the `items` projection.
|
|
701
|
+
*
|
|
702
|
+
* Rows are registered through the inherited registry surface (`register`,
|
|
703
|
+
* `onboard`, `unregister`, `clear`) — they are not passed as an `items`
|
|
704
|
+
* option to the factory.
|
|
705
|
+
*
|
|
706
|
+
* Follows the trinity pattern for dependency injection.
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```ts
|
|
710
|
+
* const grid = createDataGrid()
|
|
711
|
+
*
|
|
712
|
+
* grid.columns.onboard([
|
|
713
|
+
* { id: 'name', sortable: true },
|
|
714
|
+
* { id: 'progress', editable: true },
|
|
715
|
+
* ])
|
|
716
|
+
*
|
|
717
|
+
* grid.onboard(rows.map(value => ({ id: value.id, value })))
|
|
718
|
+
* grid.layout.pin('name', 'left')
|
|
719
|
+
* grid.editing.edit(row.id, 'progress')
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
/**
|
|
723
|
+
* Creates a data grid instance with layout, editing, row ordering, and spanning
|
|
724
|
+
* layered on top of the createDataTable pipeline.
|
|
725
|
+
*
|
|
726
|
+
* Rows live on the inherited registry surface — register them with
|
|
727
|
+
* `grid.onboard(rows.map(value => ({ id: value.id, value })))` or
|
|
728
|
+
* `grid.register({ id, value })`.
|
|
729
|
+
*
|
|
730
|
+
* @param options Data grid options
|
|
731
|
+
* @returns Data grid context
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```ts
|
|
735
|
+
* const grid = createDataGrid({ pagination: { initial: 1 } })
|
|
736
|
+
*
|
|
737
|
+
* grid.columns.onboard([
|
|
738
|
+
* { id: 'name', sortable: true },
|
|
739
|
+
* { id: 'progress', editable: true, validate: v => Number(v) >= 0 || 'must be positive' },
|
|
740
|
+
* ])
|
|
741
|
+
*
|
|
742
|
+
* grid.onboard(rows.map(value => ({ id: value.id, value })))
|
|
743
|
+
* grid.layout.pin('name', 'left')
|
|
744
|
+
* grid.rows.move(rowId, 0)
|
|
745
|
+
* grid.editing.edit(row.id, 'progress')
|
|
746
|
+
* ```
|
|
747
|
+
*/
|
|
748
|
+
function createDataGrid(_options = {}) {
|
|
749
|
+
const { editing: editingOptions, preserveRowOrder = false, rowSpanning, ...options } = _options;
|
|
750
|
+
const table = createDataTable(options);
|
|
751
|
+
const sortable = createSortable({ reactive: true });
|
|
752
|
+
const dirty = shallowRef(false);
|
|
753
|
+
table.on("register:ticket", (ticket) => {
|
|
754
|
+
sortable.register({
|
|
755
|
+
id: ticket.id,
|
|
756
|
+
value: ticket.id
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
table.on("unregister:ticket", (ticket) => {
|
|
760
|
+
sortable.unregister(ticket.id);
|
|
761
|
+
});
|
|
762
|
+
table.on("clear:registry", () => {
|
|
763
|
+
sortable.clear();
|
|
764
|
+
dirty.value = false;
|
|
765
|
+
});
|
|
766
|
+
const order = toRef(() => sortable.keys());
|
|
767
|
+
const sortedIds = computed(() => {
|
|
768
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const ticket of table.values()) {
|
|
770
|
+
const value = ticket.value;
|
|
771
|
+
const queue = buckets.get(value);
|
|
772
|
+
if (queue) queue.push(ticket.id);
|
|
773
|
+
else buckets.set(value, [ticket.id]);
|
|
774
|
+
}
|
|
775
|
+
const ids = [];
|
|
776
|
+
for (const value of table.sortedItems.value) {
|
|
777
|
+
const id = buckets.get(value)?.shift();
|
|
778
|
+
if (!/* @__PURE__ */ isUndefined(id)) ids.push(id);
|
|
779
|
+
}
|
|
780
|
+
return ids;
|
|
781
|
+
});
|
|
782
|
+
function move(id, toIndex) {
|
|
783
|
+
if (!dirty.value) {
|
|
784
|
+
const sorted = sortedIds.value;
|
|
785
|
+
const seen = new Set(sorted);
|
|
786
|
+
const rest = sortable.keys().filter((key) => !seen.has(key));
|
|
787
|
+
sortable.reorder([...sorted, ...rest]);
|
|
788
|
+
}
|
|
789
|
+
sortable.move(id, toIndex);
|
|
790
|
+
dirty.value = true;
|
|
791
|
+
}
|
|
792
|
+
function reset() {
|
|
793
|
+
if (sortable.size > 0) sortable.reorder(table.keys());
|
|
794
|
+
dirty.value = false;
|
|
795
|
+
}
|
|
796
|
+
useToggleScope(() => !toValue(preserveRowOrder), () => {
|
|
797
|
+
watch(table.sort.columns, reset, { flush: "sync" });
|
|
798
|
+
});
|
|
799
|
+
const page = computed(() => {
|
|
800
|
+
if (!dirty.value) {
|
|
801
|
+
const start = table.pagination.pageStart.value;
|
|
802
|
+
return {
|
|
803
|
+
items: table.items.value,
|
|
804
|
+
ids: table.total.value > table.sortedItems.value.length ? sortedIds.value : sortedIds.value.slice(start, start + table.items.value.length)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
const sorted = table.sortedItems.value;
|
|
808
|
+
const ids = sortedIds.value;
|
|
809
|
+
const byId = /* @__PURE__ */ new Map();
|
|
810
|
+
for (let i = 0; i < sorted.length; i++) byId.set(ids[i], sorted[i]);
|
|
811
|
+
const seen = new Set(order.value);
|
|
812
|
+
const items = [];
|
|
813
|
+
const ordered = [];
|
|
814
|
+
for (const id of order.value) {
|
|
815
|
+
const value = byId.get(id);
|
|
816
|
+
if (!/* @__PURE__ */ isUndefined(value)) {
|
|
817
|
+
items.push(value);
|
|
818
|
+
ordered.push(id);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const [i, element] of sorted.entries()) {
|
|
822
|
+
const id = ids[i];
|
|
823
|
+
if (!seen.has(id)) {
|
|
824
|
+
items.push(element);
|
|
825
|
+
ordered.push(id);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (table.total.value > sorted.length) return {
|
|
829
|
+
items,
|
|
830
|
+
ids: ordered
|
|
831
|
+
};
|
|
832
|
+
const start = table.pagination.pageStart.value;
|
|
833
|
+
const stop = table.pagination.pageStop.value;
|
|
834
|
+
return {
|
|
835
|
+
items: items.slice(start, stop),
|
|
836
|
+
ids: ordered.slice(start, stop)
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
const pageOrderedItems = toRef(() => page.value.items);
|
|
840
|
+
const layout = createColumnLayout(table.columns);
|
|
841
|
+
function lookup(row) {
|
|
842
|
+
return table.get(row)?.value;
|
|
843
|
+
}
|
|
844
|
+
const editing = createCellEditing({
|
|
845
|
+
columns: () => table.leaves.value.filter((col) => col.editable === true || /* @__PURE__ */ isFunction(col.editable)).map((col) => ({
|
|
846
|
+
id: String(col.id),
|
|
847
|
+
editable: col.editable,
|
|
848
|
+
validate: col.validate
|
|
849
|
+
})),
|
|
850
|
+
lookup,
|
|
851
|
+
registry: table,
|
|
852
|
+
onEdit: editingOptions?.onEdit ? (row, column, value) => {
|
|
853
|
+
const item = lookup(row);
|
|
854
|
+
if (!/* @__PURE__ */ isUndefined(item)) editingOptions.onEdit(row, column, value, item);
|
|
855
|
+
} : void 0
|
|
856
|
+
});
|
|
857
|
+
const spans = createRowSpanning({
|
|
858
|
+
items: pageOrderedItems,
|
|
859
|
+
columns: () => table.leaves.value.map((col) => col.id),
|
|
860
|
+
key: (_item, index) => page.value.ids[index],
|
|
861
|
+
span(item, column) {
|
|
862
|
+
const col = table.leaves.value.find((leaf) => String(leaf.id) === column);
|
|
863
|
+
if (col && /* @__PURE__ */ isFunction(col.span)) return col.span(item);
|
|
864
|
+
return rowSpanning?.(item, column) ?? 1;
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
return {
|
|
868
|
+
...table,
|
|
869
|
+
items: pageOrderedItems,
|
|
870
|
+
layout,
|
|
871
|
+
rows: {
|
|
872
|
+
order,
|
|
873
|
+
move,
|
|
874
|
+
reset
|
|
875
|
+
},
|
|
876
|
+
editing,
|
|
877
|
+
spans,
|
|
878
|
+
get size() {
|
|
879
|
+
return table.size;
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Creates a data grid context with dependency injection support.
|
|
885
|
+
*
|
|
886
|
+
* @param options Data grid context options including namespace
|
|
887
|
+
* @returns A trinity tuple: [useDataGridContext, provideDataGridContext, defaultContext]
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* const [useDataGrid, provideDataGrid, grid] = createDataGridContext({
|
|
892
|
+
* namespace: 'v0:projects',
|
|
893
|
+
* })
|
|
894
|
+
*
|
|
895
|
+
* grid.columns.onboard(columns)
|
|
896
|
+
* grid.onboard(rows.map(value => ({ id: value.id, value })))
|
|
897
|
+
* provideDataGrid()
|
|
898
|
+
* const ctx = useDataGrid()
|
|
899
|
+
* ```
|
|
900
|
+
*/
|
|
901
|
+
function createDataGridContext(_options = {}) {
|
|
902
|
+
const { namespace = "v0:data-grid", ...options } = _options;
|
|
903
|
+
const [useDataGridContext, _provideDataGridContext] = createContext(namespace);
|
|
904
|
+
const context = createDataGrid(options);
|
|
905
|
+
function provideDataGridContext(_context = context, app) {
|
|
906
|
+
return _provideDataGridContext(_context, app);
|
|
907
|
+
}
|
|
908
|
+
return createTrinity(useDataGridContext, provideDataGridContext, context);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Returns the current data grid context from dependency injection.
|
|
912
|
+
*
|
|
913
|
+
* @param namespace The namespace for the data grid context. @default 'v0:data-grid'
|
|
914
|
+
* @returns The current data grid context
|
|
915
|
+
*
|
|
916
|
+
* @example
|
|
917
|
+
* ```ts
|
|
918
|
+
* const grid = useDataGrid<Project>()
|
|
919
|
+
*
|
|
920
|
+
* grid.layout.pin('id', 'left')
|
|
921
|
+
* grid.rows.reset()
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
function useDataGrid(namespace = "v0:data-grid") {
|
|
925
|
+
return useContext(namespace);
|
|
926
|
+
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/composables/createKanban/index.ts
|
|
929
|
+
/**
|
|
930
|
+
* @module createKanban
|
|
931
|
+
*
|
|
932
|
+
* @see https://0.vuetifyjs.com/composables/data/create-kanban
|
|
933
|
+
*
|
|
934
|
+
* @remarks
|
|
935
|
+
* Headless data-flow management for two-level sortable boards. Wraps a column
|
|
936
|
+
* sortable; each column ticket carries its own inner sortable on `column.items`.
|
|
937
|
+
* Adds a `transfer` primitive for cross-column moves with reused `disabled`
|
|
938
|
+
* gating and a destination-side `accept` predicate. Pure logic — no DnD, no DOM.
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```ts
|
|
942
|
+
* import { createKanban } from '@vuetify/v0'
|
|
943
|
+
* import type { SortableTicketInput } from '@vuetify/v0'
|
|
944
|
+
*
|
|
945
|
+
* interface CardTicket extends SortableTicketInput {
|
|
946
|
+
* value: { title: string }
|
|
947
|
+
* }
|
|
948
|
+
*
|
|
949
|
+
* const kanban = createKanban<CardTicket>()
|
|
950
|
+
*
|
|
951
|
+
* const todo = kanban.columns.register({ value: { title: 'Todo' } })
|
|
952
|
+
* const done = kanban.columns.register({ value: { title: 'Done' } })
|
|
953
|
+
*
|
|
954
|
+
* const card = todo.items.register({ value: { title: 'Write spec' } })
|
|
955
|
+
*
|
|
956
|
+
* kanban.on('transfer:ticket', ({ ticket, from, to }) => {
|
|
957
|
+
* console.log(ticket.value.title, from, '→', to)
|
|
958
|
+
* })
|
|
959
|
+
*
|
|
960
|
+
* kanban.transfer(card.id, done.id, 0)
|
|
961
|
+
* ```
|
|
962
|
+
*/
|
|
963
|
+
/**
|
|
964
|
+
* Create a headless two-level sortable orchestrator.
|
|
965
|
+
*
|
|
966
|
+
* @param options Kanban options (currently only `disabled`).
|
|
967
|
+
* @returns A {@link KanbanContext}.
|
|
968
|
+
*
|
|
969
|
+
* @example
|
|
970
|
+
* ```ts
|
|
971
|
+
* import { createKanban } from '@vuetify/v0'
|
|
972
|
+
* import type { KanbanColumnTicketInput, SortableTicketInput } from '@vuetify/v0'
|
|
973
|
+
*
|
|
974
|
+
* interface CardTicket extends SortableTicketInput {
|
|
975
|
+
* value: { title: string }
|
|
976
|
+
* }
|
|
977
|
+
*
|
|
978
|
+
* interface BoardColumn extends KanbanColumnTicketInput<CardTicket> {
|
|
979
|
+
* value: { title: string }
|
|
980
|
+
* }
|
|
981
|
+
*
|
|
982
|
+
* const kanban = createKanban<CardTicket, BoardColumn>()
|
|
983
|
+
*
|
|
984
|
+
* const todo = kanban.columns.register({ value: { title: 'Todo' } })
|
|
985
|
+
* const review = kanban.columns.register({
|
|
986
|
+
* value: { title: 'Review' },
|
|
987
|
+
* accept: card => card.value.title.length > 0,
|
|
988
|
+
* })
|
|
989
|
+
*
|
|
990
|
+
* const spec = todo.items.register({ value: { title: 'Write spec' } })
|
|
991
|
+
* kanban.transfer(spec.id, review.id, 0)
|
|
992
|
+
* ```
|
|
993
|
+
*/
|
|
994
|
+
function createKanban(_options = {}) {
|
|
995
|
+
const { disabled, ...options } = _options;
|
|
996
|
+
const logger = useLogger();
|
|
997
|
+
const _columns = createSortable({
|
|
998
|
+
...options,
|
|
999
|
+
disabled
|
|
1000
|
+
});
|
|
1001
|
+
const lookup = createRegistry({ events: true });
|
|
1002
|
+
const bus = createRegistry({ events: true });
|
|
1003
|
+
const on = bus.on;
|
|
1004
|
+
const off = bus.off;
|
|
1005
|
+
const columns = {
|
|
1006
|
+
..._columns,
|
|
1007
|
+
register(registration) {
|
|
1008
|
+
let ticket;
|
|
1009
|
+
const items = createSortable({ disabled: () => !!toValue(disabled) || !!toValue(ticket?.disabled) });
|
|
1010
|
+
items.on("register:ticket", (t) => {
|
|
1011
|
+
lookup.upsert(t.id, { value: ticket.id });
|
|
1012
|
+
});
|
|
1013
|
+
items.on("unregister:ticket", (t) => {
|
|
1014
|
+
lookup.get(t.id)?.unregister();
|
|
1015
|
+
});
|
|
1016
|
+
ticket = _columns.register({
|
|
1017
|
+
...registration,
|
|
1018
|
+
items
|
|
1019
|
+
});
|
|
1020
|
+
return ticket;
|
|
1021
|
+
},
|
|
1022
|
+
onboard(registrations) {
|
|
1023
|
+
return _columns.batch(() => registrations.map((reg) => columns.register(reg)));
|
|
1024
|
+
},
|
|
1025
|
+
upsert(id, patch = {}, event) {
|
|
1026
|
+
return /* @__PURE__ */ isUndefined(_columns.get(id)) ? columns.register({
|
|
1027
|
+
...patch,
|
|
1028
|
+
id
|
|
1029
|
+
}) : _columns.upsert(id, patch, event);
|
|
1030
|
+
},
|
|
1031
|
+
get size() {
|
|
1032
|
+
return _columns.size;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
function isAccepted(accept, ticket, from, toIndex) {
|
|
1036
|
+
if (/* @__PURE__ */ isUndefined(accept)) return true;
|
|
1037
|
+
try {
|
|
1038
|
+
const result = accept(ticket, from, toIndex);
|
|
1039
|
+
if (/* @__PURE__ */ isThenable(result)) {
|
|
1040
|
+
logger.warn("accept predicate returned a thenable; async predicates are not supported — treating as reject");
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
return Boolean(result);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
logger.error("accept predicate threw; treating as reject", error);
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
columns.on("unregister:ticket", (column) => {
|
|
1050
|
+
column.items.dispose();
|
|
1051
|
+
for (const t of lookup.values()) if (t.value === column.id) t.unregister();
|
|
1052
|
+
});
|
|
1053
|
+
function transfer(id, toColumnId, toIndex) {
|
|
1054
|
+
const fromColumnId = lookup.get(id)?.value;
|
|
1055
|
+
if (/* @__PURE__ */ isUndefined(fromColumnId)) {
|
|
1056
|
+
logger.warn(`unknown ticket id "${String(id)}"`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const source = columns.get(fromColumnId);
|
|
1060
|
+
if (!source) {
|
|
1061
|
+
logger.warn(`unknown source column id "${String(fromColumnId)}"`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const destination = columns.get(toColumnId);
|
|
1065
|
+
if (!destination) {
|
|
1066
|
+
logger.warn(`unknown destination column id "${String(toColumnId)}"`);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const ticket = source.items.get(id);
|
|
1070
|
+
if (!ticket) {
|
|
1071
|
+
logger.warn(`ticket "${String(id)}" missing from source column "${String(fromColumnId)}"`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const fromIndex = ticket.index;
|
|
1075
|
+
if (toColumnId === fromColumnId) return source.items.move(id, toIndex);
|
|
1076
|
+
if (toValue(disabled) || toValue(source.disabled) || toValue(destination.disabled) || toValue(ticket.disabled)) return;
|
|
1077
|
+
if (!isAccepted(destination.accept, ticket, fromColumnId, toIndex)) return void 0;
|
|
1078
|
+
if (!/* @__PURE__ */ isUndefined(destination.items.get(id))) {
|
|
1079
|
+
logger.warn(`id "${String(id)}" already exists in destination column "${String(toColumnId)}"`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
let registered;
|
|
1083
|
+
let moved;
|
|
1084
|
+
destination.items.batch(() => {
|
|
1085
|
+
const [input] = source.items.offboard([id]);
|
|
1086
|
+
if (/* @__PURE__ */ isUndefined(input)) return;
|
|
1087
|
+
registered = destination.items.register({
|
|
1088
|
+
...input,
|
|
1089
|
+
id
|
|
1090
|
+
});
|
|
1091
|
+
moved = registered.index === toIndex ? registered : destination.items.move(registered.id, toIndex);
|
|
1092
|
+
});
|
|
1093
|
+
const final = moved ?? registered;
|
|
1094
|
+
if (final) bus.emit("transfer:ticket", {
|
|
1095
|
+
ticket: final,
|
|
1096
|
+
from: fromColumnId,
|
|
1097
|
+
to: toColumnId,
|
|
1098
|
+
fromIndex,
|
|
1099
|
+
toIndex: final.index
|
|
1100
|
+
});
|
|
1101
|
+
return final;
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
columns,
|
|
1105
|
+
transfer,
|
|
1106
|
+
on,
|
|
1107
|
+
off
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/composables/createOtp/index.ts
|
|
1112
|
+
/**
|
|
1113
|
+
* @module createOtp
|
|
1114
|
+
*
|
|
1115
|
+
* @see https://0.vuetifyjs.com/composables/forms/create-otp
|
|
1116
|
+
*
|
|
1117
|
+
* @remarks
|
|
1118
|
+
* Headless state for a fixed-length one-time-password / verification-code value.
|
|
1119
|
+
* Owns the joined string, per-character pattern matching, length contract,
|
|
1120
|
+
* completion edge detection with a decisional async `onComplete` hook, and
|
|
1121
|
+
* validation through `createInput`. Rendering, focus, and per-element event
|
|
1122
|
+
* wiring are the consumer's responsibility.
|
|
1123
|
+
*
|
|
1124
|
+
* @example
|
|
1125
|
+
* ```ts
|
|
1126
|
+
* import { createOtp } from '@vuetify/v0'
|
|
1127
|
+
*
|
|
1128
|
+
* const otp = createOtp({
|
|
1129
|
+
* length: 6,
|
|
1130
|
+
* pattern: 'numeric',
|
|
1131
|
+
* onComplete: async value => {
|
|
1132
|
+
* const ok = await verify(value)
|
|
1133
|
+
* return ok
|
|
1134
|
+
* },
|
|
1135
|
+
* })
|
|
1136
|
+
*
|
|
1137
|
+
* otp.write(0, '4')
|
|
1138
|
+
* otp.distribute('12345', 1) // returns 5; value becomes '412345'
|
|
1139
|
+
* otp.isComplete.value // true
|
|
1140
|
+
* ```
|
|
1141
|
+
*/
|
|
1142
|
+
const PRESETS = {
|
|
1143
|
+
numeric: /^[0-9]$/,
|
|
1144
|
+
alphanumeric: /^[a-zA-Z0-9]$/,
|
|
1145
|
+
alphabetic: /^[a-zA-Z]$/
|
|
1146
|
+
};
|
|
1147
|
+
const WARNED_PATTERNS = /* @__PURE__ */ new WeakSet();
|
|
1148
|
+
function createOtp(_options = {}) {
|
|
1149
|
+
const { value = shallowRef(""), length = 6, pattern = "numeric", disabled = false, readonly: _readonly = false, onComplete, ...options } = _options;
|
|
1150
|
+
const logger = useLogger();
|
|
1151
|
+
const errorRef = shallowRef(false);
|
|
1152
|
+
const errorMessagesRef = shallowRef(void 0);
|
|
1153
|
+
const input = createInput({
|
|
1154
|
+
...options,
|
|
1155
|
+
value,
|
|
1156
|
+
disabled,
|
|
1157
|
+
readonly: _readonly,
|
|
1158
|
+
error: errorRef,
|
|
1159
|
+
errorMessages: errorMessagesRef
|
|
1160
|
+
});
|
|
1161
|
+
const lengthRef = toRef(() => toValue(length));
|
|
1162
|
+
let lastCompleted = "";
|
|
1163
|
+
function compile(resolved) {
|
|
1164
|
+
if (/* @__PURE__ */ isString(resolved)) return PRESETS[resolved];
|
|
1165
|
+
if (process.env.NODE_ENV !== "production" && !WARNED_PATTERNS.has(resolved)) {
|
|
1166
|
+
WARNED_PATTERNS.add(resolved);
|
|
1167
|
+
if (resolved.test("aa") || resolved.test("00")) logger.warn("createOtp: pattern matches multi-character input; per-character matching may behave unexpectedly");
|
|
1168
|
+
}
|
|
1169
|
+
return resolved;
|
|
1170
|
+
}
|
|
1171
|
+
function accepts(char) {
|
|
1172
|
+
if (char.length !== 1) return false;
|
|
1173
|
+
return compile(toValue(pattern)).test(char);
|
|
1174
|
+
}
|
|
1175
|
+
function filterAccepted(text) {
|
|
1176
|
+
let out = "";
|
|
1177
|
+
for (const ch of text) if (accepts(ch)) out += ch;
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
const isPending = shallowRef(false);
|
|
1181
|
+
function isLocked() {
|
|
1182
|
+
return toValue(disabled) || toValue(_readonly) || isPending.value;
|
|
1183
|
+
}
|
|
1184
|
+
function clearRejection() {
|
|
1185
|
+
if (errorRef.value || errorMessagesRef.value) {
|
|
1186
|
+
errorRef.value = false;
|
|
1187
|
+
errorMessagesRef.value = void 0;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function write(index, char) {
|
|
1191
|
+
if (isLocked()) return;
|
|
1192
|
+
const max = toValue(lengthRef);
|
|
1193
|
+
if (index < 0 || index >= max) return;
|
|
1194
|
+
if (char === "") {
|
|
1195
|
+
clearRejection();
|
|
1196
|
+
value.value = value.value.slice(0, index);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const first = char[0];
|
|
1200
|
+
if (!accepts(first)) return;
|
|
1201
|
+
clearRejection();
|
|
1202
|
+
const current = value.value;
|
|
1203
|
+
const head = current.slice(0, index);
|
|
1204
|
+
const tail = current.slice(index + 1);
|
|
1205
|
+
value.value = (head + first + tail).slice(0, max);
|
|
1206
|
+
}
|
|
1207
|
+
function distribute(text, index = 0) {
|
|
1208
|
+
if (isLocked()) return 0;
|
|
1209
|
+
const max = toValue(lengthRef);
|
|
1210
|
+
const start = /* @__PURE__ */ clamp(index, 0, max);
|
|
1211
|
+
const filtered = filterAccepted(text);
|
|
1212
|
+
if (filtered.length === 0) return 0;
|
|
1213
|
+
clearRejection();
|
|
1214
|
+
const head = value.value.slice(0, start);
|
|
1215
|
+
const next = (head + filtered).slice(0, max);
|
|
1216
|
+
value.value = next;
|
|
1217
|
+
return next.length - head.length;
|
|
1218
|
+
}
|
|
1219
|
+
function clear() {
|
|
1220
|
+
if (isLocked()) return;
|
|
1221
|
+
clearRejection();
|
|
1222
|
+
lastCompleted = "";
|
|
1223
|
+
value.value = "";
|
|
1224
|
+
}
|
|
1225
|
+
function fill(text) {
|
|
1226
|
+
if (isLocked()) return;
|
|
1227
|
+
const max = toValue(lengthRef);
|
|
1228
|
+
const filtered = filterAccepted(text).slice(0, max);
|
|
1229
|
+
if (text.length === 0 || filtered.length > 0) clearRejection();
|
|
1230
|
+
value.value = filtered;
|
|
1231
|
+
}
|
|
1232
|
+
function reject() {
|
|
1233
|
+
errorRef.value = true;
|
|
1234
|
+
errorMessagesRef.value = ["Invalid code"];
|
|
1235
|
+
lastCompleted = "";
|
|
1236
|
+
value.value = "";
|
|
1237
|
+
}
|
|
1238
|
+
const isComplete = toRef(() => {
|
|
1239
|
+
const max = toValue(lengthRef);
|
|
1240
|
+
const v = value.value;
|
|
1241
|
+
if (v.length !== max) return false;
|
|
1242
|
+
const re = compile(toValue(pattern));
|
|
1243
|
+
for (const ch of v) if (!re.test(ch)) return false;
|
|
1244
|
+
return true;
|
|
1245
|
+
});
|
|
1246
|
+
watch(value, (next) => {
|
|
1247
|
+
if (!isComplete.value) {
|
|
1248
|
+
lastCompleted = "";
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!onComplete) return;
|
|
1252
|
+
if (isLocked()) return;
|
|
1253
|
+
if (next === lastCompleted) return;
|
|
1254
|
+
lastCompleted = next;
|
|
1255
|
+
let result;
|
|
1256
|
+
try {
|
|
1257
|
+
result = onComplete(next);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
logger.warn("createOtp: onComplete threw", error);
|
|
1260
|
+
reject();
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (/* @__PURE__ */ isThenable(result)) {
|
|
1264
|
+
isPending.value = true;
|
|
1265
|
+
result.then((ok) => {
|
|
1266
|
+
isPending.value = false;
|
|
1267
|
+
if (ok === false) reject();
|
|
1268
|
+
}, (error) => {
|
|
1269
|
+
isPending.value = false;
|
|
1270
|
+
logger.warn("createOtp: onComplete rejected", error);
|
|
1271
|
+
reject();
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (result === false) reject();
|
|
1276
|
+
});
|
|
1277
|
+
return {
|
|
1278
|
+
value,
|
|
1279
|
+
length: lengthRef,
|
|
1280
|
+
input: {
|
|
1281
|
+
...input,
|
|
1282
|
+
reset() {
|
|
1283
|
+
clearRejection();
|
|
1284
|
+
lastCompleted = "";
|
|
1285
|
+
input.reset();
|
|
1286
|
+
}
|
|
1287
|
+
},
|
|
1288
|
+
isComplete,
|
|
1289
|
+
isValidating: isPending,
|
|
1290
|
+
write,
|
|
1291
|
+
distribute,
|
|
1292
|
+
clear,
|
|
1293
|
+
fill,
|
|
1294
|
+
accepts
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
//#endregion
|
|
1298
|
+
//#region src/composables/createTimeline/index.ts
|
|
1299
|
+
/**
|
|
1300
|
+
* @module createTimeline
|
|
1301
|
+
*
|
|
1302
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
1303
|
+
*
|
|
1304
|
+
* @remarks
|
|
1305
|
+
* Bounded undo/redo system with overflow management.
|
|
1306
|
+
*
|
|
1307
|
+
* Key features:
|
|
1308
|
+
* - Fixed-size history (default: 10 items)
|
|
1309
|
+
* - Undo/redo stack management
|
|
1310
|
+
* - Overflow queue (preserves oldest items)
|
|
1311
|
+
* - Automatic reindexing after operations
|
|
1312
|
+
* - Perfect for command pattern, history tracking
|
|
1313
|
+
*
|
|
1314
|
+
* Extends createRegistry with temporal navigation capabilities.
|
|
1315
|
+
*
|
|
1316
|
+
* @example
|
|
1317
|
+
* ```ts
|
|
1318
|
+
* import { createTimeline } from '@vuetify/v0'
|
|
1319
|
+
*
|
|
1320
|
+
* const timeline = createTimeline({ size: 5 })
|
|
1321
|
+
* timeline.register({ value: 'action-1' })
|
|
1322
|
+
* timeline.register({ value: 'action-2' })
|
|
1323
|
+
* timeline.undo()
|
|
1324
|
+
* ```
|
|
1325
|
+
*/
|
|
1326
|
+
/**
|
|
1327
|
+
* Creates a new timeline instance.
|
|
1328
|
+
*
|
|
1329
|
+
* @param options The options for the timeline instance.
|
|
1330
|
+
* @template Z The type of the timeline ticket.
|
|
1331
|
+
* @template E The type of the timeline context.
|
|
1332
|
+
* @returns A new timeline instance.
|
|
1333
|
+
*
|
|
1334
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
1335
|
+
*
|
|
1336
|
+
* @example
|
|
1337
|
+
* ```ts
|
|
1338
|
+
* import { useTimeline } from '@vuetify/v0'
|
|
1339
|
+
*
|
|
1340
|
+
* const timeline = useTimeline({ size: 3 })
|
|
1341
|
+
*
|
|
1342
|
+
* timeline.onboard([{ id: 'one' }, { id: 'two' }, { id: 'three' }])
|
|
1343
|
+
*
|
|
1344
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }, { id: 'three' }]
|
|
1345
|
+
*
|
|
1346
|
+
* timeline.undo()
|
|
1347
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }]
|
|
1348
|
+
*
|
|
1349
|
+
* timeline.redo()
|
|
1350
|
+
* console.log(timeline.values()) // [{ id: 'one' }, { id: 'two' }, { id: 'three' }]
|
|
1351
|
+
* ```
|
|
1352
|
+
*/
|
|
1353
|
+
function createTimeline(_options = {}) {
|
|
1354
|
+
const { size: _size = 10, ...options } = _options;
|
|
1355
|
+
const size = /* @__PURE__ */ isNaN(_size) ? 10 : Math.max(1, Math.floor(_size));
|
|
1356
|
+
const registry = createRegistry(options);
|
|
1357
|
+
const stack = [];
|
|
1358
|
+
const overflow = [];
|
|
1359
|
+
function register(item) {
|
|
1360
|
+
stack.length = 0;
|
|
1361
|
+
if (registry.size < size) return registry.register({ ...item });
|
|
1362
|
+
const removing = registry.seek("first");
|
|
1363
|
+
if (overflow.length === size) overflow.shift();
|
|
1364
|
+
overflow.push(removing);
|
|
1365
|
+
registry.unregister(removing.id);
|
|
1366
|
+
const ticket = registry.register({ ...item });
|
|
1367
|
+
registry.reindex();
|
|
1368
|
+
return ticket;
|
|
1369
|
+
}
|
|
1370
|
+
function undo() {
|
|
1371
|
+
const item = registry.seek("last");
|
|
1372
|
+
if (!item) return void 0;
|
|
1373
|
+
stack.push(item);
|
|
1374
|
+
registry.unregister(item.id);
|
|
1375
|
+
const restored = overflow.pop();
|
|
1376
|
+
if (restored) {
|
|
1377
|
+
const remaining = registry.values();
|
|
1378
|
+
registry.clear();
|
|
1379
|
+
registry.onboard([restored, ...remaining]);
|
|
1380
|
+
registry.reindex();
|
|
1381
|
+
}
|
|
1382
|
+
return item;
|
|
1383
|
+
}
|
|
1384
|
+
function redo() {
|
|
1385
|
+
if (stack.length === 0) return void 0;
|
|
1386
|
+
const item = stack.pop();
|
|
1387
|
+
if (registry.size >= size) {
|
|
1388
|
+
const oldest = registry.seek("first");
|
|
1389
|
+
if (overflow.length === size) overflow.shift();
|
|
1390
|
+
overflow.push(oldest);
|
|
1391
|
+
registry.unregister(oldest.id);
|
|
1392
|
+
}
|
|
1393
|
+
const ticket = registry.register(item);
|
|
1394
|
+
registry.reindex();
|
|
1395
|
+
return ticket;
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
...registry,
|
|
1399
|
+
register,
|
|
1400
|
+
undo,
|
|
1401
|
+
redo,
|
|
1402
|
+
get size() {
|
|
1403
|
+
return registry.size;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Creates a new timeline context.
|
|
1409
|
+
*
|
|
1410
|
+
* @param options The options for the timeline context.
|
|
1411
|
+
* @template Z The type of the timeline ticket.
|
|
1412
|
+
* @template E The type of the timeline context.
|
|
1413
|
+
* @returns A new timeline context.
|
|
1414
|
+
*
|
|
1415
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
1416
|
+
*
|
|
1417
|
+
* @example
|
|
1418
|
+
* ```ts
|
|
1419
|
+
* import { createTimelineContext } from '@vuetify/v0'
|
|
1420
|
+
*
|
|
1421
|
+
* // With default namespace 'v0:timeline'
|
|
1422
|
+
* export const [useTimeline, provideTimeline, context] = createTimelineContext({ size: 5 })
|
|
1423
|
+
*
|
|
1424
|
+
* context.register({ id: 'example' })
|
|
1425
|
+
*
|
|
1426
|
+
* // In a parent component
|
|
1427
|
+
* provideTimeline()
|
|
1428
|
+
*
|
|
1429
|
+
* // In a child component
|
|
1430
|
+
* const timeline = useTimeline()
|
|
1431
|
+
*
|
|
1432
|
+
* console.log(timeline.values()) // [{ id: 'example' }]
|
|
1433
|
+
* ```
|
|
1434
|
+
*/
|
|
1435
|
+
function createTimelineContext(_options = {}) {
|
|
1436
|
+
const { namespace = "v0:timeline", ...options } = _options;
|
|
1437
|
+
return createTrinity(namespace, createTimeline(options));
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Returns the current timeline instance.
|
|
1441
|
+
*
|
|
1442
|
+
* @param namespace The namespace for the timeline context. Defaults to `'v0:timeline'`.
|
|
1443
|
+
* @returns The current timeline instance.
|
|
1444
|
+
*
|
|
1445
|
+
* @see https://0.vuetifyjs.com/composables/registration/create-timeline
|
|
1446
|
+
*
|
|
1447
|
+
* @example
|
|
1448
|
+
* ```vue
|
|
1449
|
+
* <script setup lang="ts">
|
|
1450
|
+
* import { useTimeline } from '@vuetify/v0'
|
|
1451
|
+
*
|
|
1452
|
+
* const timeline = useTimeline()
|
|
1453
|
+
* <\/script>
|
|
1454
|
+
* ```
|
|
1455
|
+
*/
|
|
1456
|
+
function useTimeline(namespace = "v0:timeline") {
|
|
1457
|
+
return useContext(namespace);
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/composables/createVirtual/index.ts
|
|
1461
|
+
/**
|
|
1462
|
+
* @module createVirtual
|
|
1463
|
+
*
|
|
1464
|
+
* @see https://0.vuetifyjs.com/composables/data/create-virtual
|
|
1465
|
+
*
|
|
1466
|
+
* @remarks
|
|
1467
|
+
* Virtual scrolling composable for efficiently rendering large lists.
|
|
1468
|
+
*
|
|
1469
|
+
* Key features:
|
|
1470
|
+
* - Trinity pattern for dependency injection (`createVirtualContext`, `useVirtual`)
|
|
1471
|
+
* - Renders only visible items (viewport + overscan)
|
|
1472
|
+
* - Dynamic or fixed item heights
|
|
1473
|
+
* - SSR-safe (checks IN_BROWSER)
|
|
1474
|
+
* - Bidirectional scrolling (forward/reverse for chat apps)
|
|
1475
|
+
* - Scroll anchoring (maintains position across data changes)
|
|
1476
|
+
* - Edge detection for infinite scroll
|
|
1477
|
+
* - iOS momentum and elastic scrolling
|
|
1478
|
+
* - Configurable overscan (extra items rendered for smooth scrolling)
|
|
1479
|
+
*
|
|
1480
|
+
* Perfect for large data sets, chat apps, and infinite scroll implementations.
|
|
1481
|
+
*
|
|
1482
|
+
* @example
|
|
1483
|
+
* ```ts
|
|
1484
|
+
* import { shallowRef } from 'vue'
|
|
1485
|
+
* import { createVirtual } from '@vuetify/v0'
|
|
1486
|
+
*
|
|
1487
|
+
* const items = shallowRef(Array.from({ length: 10000 }, (_, i) => ({ id: i })))
|
|
1488
|
+
* const virtual = createVirtual(items, { itemHeight: 40 })
|
|
1489
|
+
* console.log(virtual.items.value)
|
|
1490
|
+
* ```
|
|
1491
|
+
*/
|
|
1492
|
+
/**
|
|
1493
|
+
* Virtual scrolling composable for efficiently rendering large lists
|
|
1494
|
+
*
|
|
1495
|
+
* @param items Reactive array of items to virtualize
|
|
1496
|
+
* @param options Configuration options
|
|
1497
|
+
* @returns Virtual scrolling context
|
|
1498
|
+
*
|
|
1499
|
+
* @see https://0.vuetifyjs.com/composables/data/create-virtual
|
|
1500
|
+
*
|
|
1501
|
+
* @example
|
|
1502
|
+
* ```vue
|
|
1503
|
+
* <script setup lang="ts">
|
|
1504
|
+
* import { createVirtual } from '@vuetify/v0'
|
|
1505
|
+
*
|
|
1506
|
+
* const items = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })))
|
|
1507
|
+
* const virtual = createVirtual(items)
|
|
1508
|
+
* <\/script>
|
|
1509
|
+
*
|
|
1510
|
+
* <template>
|
|
1511
|
+
* <div
|
|
1512
|
+
* ref="virtual.element"
|
|
1513
|
+
* style="height: 600px; overflow-y: auto;"
|
|
1514
|
+
* @scroll="virtual.scroll"
|
|
1515
|
+
* >
|
|
1516
|
+
* <div :style="{ height: `${virtual.offset.value}px` }" />
|
|
1517
|
+
*
|
|
1518
|
+
* <div v-for="item in virtual.items.value" :key="item.index">
|
|
1519
|
+
* {{ item.raw.name }}
|
|
1520
|
+
* </div>
|
|
1521
|
+
*
|
|
1522
|
+
* <div :style="{ height: `${virtual.size.value}px` }" />
|
|
1523
|
+
* </div>
|
|
1524
|
+
* </template>
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1527
|
+
const FALLBACK_HEIGHT = 40;
|
|
1528
|
+
function createVirtual(items, _options = {}) {
|
|
1529
|
+
const { itemHeight: _itemHeight, height, overscan = 5, direction = "forward", anchor = "auto", anchorSmooth = true, onStartReached, onEndReached, startThreshold = 0, endThreshold = 0, momentum: momentumOption, elastic: elasticOption } = _options;
|
|
1530
|
+
const element = ref();
|
|
1531
|
+
const itemHeight = shallowRef(Number.parseFloat(String(_itemHeight || 0)));
|
|
1532
|
+
function estimate() {
|
|
1533
|
+
return itemHeight.value || FALLBACK_HEIGHT;
|
|
1534
|
+
}
|
|
1535
|
+
const heights = shallowRef([]);
|
|
1536
|
+
const offsets = shallowRef([]);
|
|
1537
|
+
const first = shallowRef(0);
|
|
1538
|
+
const last = shallowRef(0);
|
|
1539
|
+
const offset = shallowRef(0);
|
|
1540
|
+
const size = shallowRef(0);
|
|
1541
|
+
const viewportHeight = shallowRef(0);
|
|
1542
|
+
const state = shallowRef("ok");
|
|
1543
|
+
const isIOS = IN_BROWSER && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
1544
|
+
const momentum = momentumOption ?? isIOS;
|
|
1545
|
+
const elastic = elasticOption ?? isIOS;
|
|
1546
|
+
let raf = -1;
|
|
1547
|
+
let rebuildRaf = -1;
|
|
1548
|
+
let edgeRaf = -1;
|
|
1549
|
+
const cachedViewport = Number.parseInt(String(height)) || 0;
|
|
1550
|
+
let anchorIndex = -1;
|
|
1551
|
+
let anchorOffset = 0;
|
|
1552
|
+
const computedItems = computed(() => items.value.slice(first.value, last.value).map((item, i) => ({
|
|
1553
|
+
raw: item,
|
|
1554
|
+
index: i + first.value
|
|
1555
|
+
})));
|
|
1556
|
+
watch(element, (el) => {
|
|
1557
|
+
if (!IN_BROWSER || !el?.style) return;
|
|
1558
|
+
/* v8 ignore start -- browser-only style assignments */
|
|
1559
|
+
if (momentum) el.style.webkitOverflowScrolling = "touch";
|
|
1560
|
+
if (!elastic) el.style.overscrollBehavior = "none";
|
|
1561
|
+
/* v8 ignore stop */
|
|
1562
|
+
});
|
|
1563
|
+
useResizeObserver(element, (entries) => {
|
|
1564
|
+
/* v8 ignore next -- defensive: ResizeObserver always emits at least one entry */
|
|
1565
|
+
if (!entries[0]) return;
|
|
1566
|
+
viewportHeight.value = entries[0].contentRect.height;
|
|
1567
|
+
update();
|
|
1568
|
+
});
|
|
1569
|
+
watch(items, (newItems) => {
|
|
1570
|
+
captureAnchor();
|
|
1571
|
+
const length = newItems.length;
|
|
1572
|
+
const old = heights.value;
|
|
1573
|
+
const newHeights = old.length === length ? old : Array.from({ length }, (_, i) => i < old.length ? old[i] : null);
|
|
1574
|
+
if (heights.value !== newHeights) heights.value = newHeights;
|
|
1575
|
+
rebuild();
|
|
1576
|
+
}, { immediate: true });
|
|
1577
|
+
watch(element, () => {
|
|
1578
|
+
/* v8 ignore next -- defensive: watch fires only when element is present */
|
|
1579
|
+
if (!element.value) return;
|
|
1580
|
+
if (direction === "reverse" && items.value.length > 0) {
|
|
1581
|
+
const lastIndex = items.value.length - 1;
|
|
1582
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || estimate());
|
|
1583
|
+
element.value.scrollTop = totalHeight;
|
|
1584
|
+
}
|
|
1585
|
+
update();
|
|
1586
|
+
});
|
|
1587
|
+
function captureAnchor() {
|
|
1588
|
+
if (!element.value || anchor === "auto") return;
|
|
1589
|
+
if (anchor === "start") {
|
|
1590
|
+
anchorIndex = 0;
|
|
1591
|
+
anchorOffset = 0;
|
|
1592
|
+
} else if (anchor === "end") {
|
|
1593
|
+
anchorIndex = items.value.length - 1;
|
|
1594
|
+
anchorOffset = 0;
|
|
1595
|
+
} else if (/* @__PURE__ */ isFunction(anchor)) {
|
|
1596
|
+
const result = anchor(items.value);
|
|
1597
|
+
/* v8 ignore next 4 -- function-anchor: tested with valid number return; non-number rejection branch is implicit */
|
|
1598
|
+
if (/* @__PURE__ */ isNumber(result)) {
|
|
1599
|
+
anchorIndex = result;
|
|
1600
|
+
anchorOffset = element.value.scrollTop - (offsets.value[result] || 0);
|
|
1601
|
+
}
|
|
1602
|
+
} else {
|
|
1603
|
+
/* v8 ignore start -- default anchor capture */
|
|
1604
|
+
anchorIndex = first.value;
|
|
1605
|
+
anchorOffset = element.value.scrollTop - (offsets.value[first.value] || 0);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function restoreAnchor() {
|
|
1609
|
+
if (!element.value || anchorIndex < 0) return;
|
|
1610
|
+
const newScrollTop = (offsets.value[anchorIndex] || 0) + anchorOffset;
|
|
1611
|
+
if (anchorSmooth && direction === "forward") element.value.scrollTo({
|
|
1612
|
+
top: newScrollTop,
|
|
1613
|
+
behavior: "smooth"
|
|
1614
|
+
});
|
|
1615
|
+
else element.value.scrollTop = newScrollTop;
|
|
1616
|
+
/* v8 ignore stop */
|
|
1617
|
+
anchorIndex = -1;
|
|
1618
|
+
anchorOffset = 0;
|
|
1619
|
+
}
|
|
1620
|
+
function rebuild() {
|
|
1621
|
+
const length = items.value.length;
|
|
1622
|
+
const newOffsets = offsets.value.length === length ? offsets.value : Array.from({ length });
|
|
1623
|
+
let offset = 0;
|
|
1624
|
+
for (let i = 0; i < length; i++) {
|
|
1625
|
+
newOffsets[i] = offset;
|
|
1626
|
+
offset += heights.value[i] || estimate();
|
|
1627
|
+
}
|
|
1628
|
+
if (offsets.value !== newOffsets) offsets.value = newOffsets;
|
|
1629
|
+
update();
|
|
1630
|
+
restoreAnchor();
|
|
1631
|
+
}
|
|
1632
|
+
function findIndex(scrollTop) {
|
|
1633
|
+
const arr = offsets.value;
|
|
1634
|
+
if (arr.length === 0) return 0;
|
|
1635
|
+
let low = 0;
|
|
1636
|
+
let high = arr.length - 1;
|
|
1637
|
+
while (low <= high) {
|
|
1638
|
+
const mid = low + high >> 1;
|
|
1639
|
+
if (arr[mid] <= scrollTop) low = mid + 1;
|
|
1640
|
+
else high = mid - 1;
|
|
1641
|
+
}
|
|
1642
|
+
return Math.max(0, high);
|
|
1643
|
+
}
|
|
1644
|
+
function update() {
|
|
1645
|
+
if (!element.value) return;
|
|
1646
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
1647
|
+
if (!viewport) return;
|
|
1648
|
+
const scrollTop = element.value.scrollTop || 0;
|
|
1649
|
+
const length = items.value.length;
|
|
1650
|
+
const visibleStart = findIndex(scrollTop);
|
|
1651
|
+
const visibleEnd = findIndex(scrollTop + viewport) + 1;
|
|
1652
|
+
const start = /* @__PURE__ */ clamp(visibleStart - overscan, 0, length);
|
|
1653
|
+
const end = /* @__PURE__ */ clamp(visibleEnd + overscan, start, length);
|
|
1654
|
+
first.value = start;
|
|
1655
|
+
last.value = end;
|
|
1656
|
+
offset.value = offsets.value[start] || 0;
|
|
1657
|
+
const lastIndex = length - 1;
|
|
1658
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || estimate());
|
|
1659
|
+
size.value = totalHeight - (offsets.value[end] || totalHeight);
|
|
1660
|
+
}
|
|
1661
|
+
function checkEdges() {
|
|
1662
|
+
if (!element.value) return;
|
|
1663
|
+
const scrollTop = element.value.scrollTop;
|
|
1664
|
+
const scrollHeight = element.value.scrollHeight;
|
|
1665
|
+
const clientHeight = element.value.clientHeight;
|
|
1666
|
+
const distanceFromStart = scrollTop;
|
|
1667
|
+
const distanceFromEnd = scrollHeight - (scrollTop + clientHeight);
|
|
1668
|
+
if (IN_BROWSER) {
|
|
1669
|
+
cancelAnimationFrame(edgeRaf);
|
|
1670
|
+
edgeRaf = requestAnimationFrame(() => {
|
|
1671
|
+
if (onStartReached && distanceFromStart <= startThreshold) onStartReached(distanceFromStart);
|
|
1672
|
+
if (onEndReached && distanceFromEnd <= endThreshold) onEndReached(distanceFromEnd);
|
|
1673
|
+
});
|
|
1674
|
+
} else {
|
|
1675
|
+
if (onStartReached && distanceFromStart <= startThreshold) onStartReached(distanceFromStart);
|
|
1676
|
+
if (onEndReached && distanceFromEnd <= endThreshold) onEndReached(distanceFromEnd);
|
|
1677
|
+
}
|
|
1678
|
+
/* v8 ignore stop */
|
|
1679
|
+
}
|
|
1680
|
+
function resize(index, height) {
|
|
1681
|
+
/* v8 ignore next -- early bail when height is already correct */
|
|
1682
|
+
if (heights.value[index] === height) return;
|
|
1683
|
+
heights.value[index] = height;
|
|
1684
|
+
/* v8 ignore next -- first-call only: itemHeight bootstrapped from initial measurement */
|
|
1685
|
+
if (!itemHeight.value) itemHeight.value = height;
|
|
1686
|
+
if (IN_BROWSER) {
|
|
1687
|
+
cancelAnimationFrame(rebuildRaf);
|
|
1688
|
+
rebuildRaf = requestAnimationFrame(rebuild);
|
|
1689
|
+
} else rebuild();
|
|
1690
|
+
/* v8 ignore stop */
|
|
1691
|
+
}
|
|
1692
|
+
function scroll() {
|
|
1693
|
+
if (IN_BROWSER) {
|
|
1694
|
+
cancelAnimationFrame(raf);
|
|
1695
|
+
raf = requestAnimationFrame(update);
|
|
1696
|
+
checkEdges();
|
|
1697
|
+
} else update();
|
|
1698
|
+
/* v8 ignore stop */
|
|
1699
|
+
}
|
|
1700
|
+
function scrollTo(index, scrollOptions) {
|
|
1701
|
+
/* v8 ignore next -- defensive: scrollTo only invoked after element mounts */
|
|
1702
|
+
if (!element.value) return;
|
|
1703
|
+
const targetOffset = offsets.value[index] || 0;
|
|
1704
|
+
const behavior = scrollOptions?.behavior ?? "auto";
|
|
1705
|
+
const block = scrollOptions?.block ?? "start";
|
|
1706
|
+
const extraOffset = scrollOptions?.offset ?? 0;
|
|
1707
|
+
let scrollTop = targetOffset + extraOffset;
|
|
1708
|
+
switch (block) {
|
|
1709
|
+
case "center": {
|
|
1710
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
1711
|
+
const itemH = heights.value[index] || estimate();
|
|
1712
|
+
scrollTop = targetOffset - viewport / 2 + itemH / 2 + extraOffset;
|
|
1713
|
+
break;
|
|
1714
|
+
}
|
|
1715
|
+
case "end": {
|
|
1716
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
1717
|
+
const itemH = heights.value[index] || estimate();
|
|
1718
|
+
scrollTop = targetOffset - viewport + itemH + extraOffset;
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
case "nearest": {
|
|
1722
|
+
const viewport = viewportHeight.value || cachedViewport;
|
|
1723
|
+
const currentScroll = element.value.scrollTop;
|
|
1724
|
+
const itemH = heights.value[index] || estimate();
|
|
1725
|
+
if (targetOffset < currentScroll) scrollTop = targetOffset + extraOffset;
|
|
1726
|
+
else if (targetOffset + itemH > currentScroll + viewport) scrollTop = targetOffset - viewport + itemH + extraOffset;
|
|
1727
|
+
else return;
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (behavior === "smooth") element.value.scrollTo({
|
|
1732
|
+
top: scrollTop,
|
|
1733
|
+
behavior: "smooth"
|
|
1734
|
+
});
|
|
1735
|
+
else element.value.scrollTop = scrollTop;
|
|
1736
|
+
update();
|
|
1737
|
+
}
|
|
1738
|
+
function reset() {
|
|
1739
|
+
state.value = "ok";
|
|
1740
|
+
anchorIndex = -1;
|
|
1741
|
+
anchorOffset = 0;
|
|
1742
|
+
if (element.value && direction === "reverse" && items.value.length > 0) {
|
|
1743
|
+
const lastIndex = items.value.length - 1;
|
|
1744
|
+
const totalHeight = (offsets.value[lastIndex] || 0) + (heights.value[lastIndex] || estimate());
|
|
1745
|
+
element.value.scrollTop = totalHeight;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
onScopeDispose(() => {
|
|
1749
|
+
if (!IN_BROWSER) return;
|
|
1750
|
+
cancelAnimationFrame(raf);
|
|
1751
|
+
cancelAnimationFrame(rebuildRaf);
|
|
1752
|
+
cancelAnimationFrame(edgeRaf);
|
|
1753
|
+
}, true);
|
|
1754
|
+
return {
|
|
1755
|
+
element,
|
|
1756
|
+
items: computedItems,
|
|
1757
|
+
offset: readonly(offset),
|
|
1758
|
+
size: readonly(size),
|
|
1759
|
+
state,
|
|
1760
|
+
scrollTo,
|
|
1761
|
+
scroll,
|
|
1762
|
+
scrollend: scroll,
|
|
1763
|
+
resize,
|
|
1764
|
+
reset
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Creates a virtual scrolling context with dependency injection support.
|
|
1769
|
+
*
|
|
1770
|
+
* @param items Reactive array of items to virtualize
|
|
1771
|
+
* @param options Configuration options including namespace
|
|
1772
|
+
* @template T The type of the items
|
|
1773
|
+
* @returns Trinity tuple: [useVirtual, provideVirtual, defaultVirtual]
|
|
1774
|
+
*
|
|
1775
|
+
* @see https://0.vuetifyjs.com/composables/data/create-virtual
|
|
1776
|
+
*
|
|
1777
|
+
* @example
|
|
1778
|
+
* ```ts
|
|
1779
|
+
* // Create injectable context
|
|
1780
|
+
* const [useVirtual, provideVirtual, virtual] = createVirtualContext(items, {
|
|
1781
|
+
* namespace: 'my-virtual',
|
|
1782
|
+
* overscan: 10,
|
|
1783
|
+
* })
|
|
1784
|
+
*
|
|
1785
|
+
* // In parent component
|
|
1786
|
+
* provideVirtual()
|
|
1787
|
+
*
|
|
1788
|
+
* // In child component
|
|
1789
|
+
* const virtual = useVirtual()
|
|
1790
|
+
* ```
|
|
1791
|
+
*/
|
|
1792
|
+
function createVirtualContext(items, _options = {}) {
|
|
1793
|
+
const { namespace = "v0:virtual", ...options } = _options;
|
|
1794
|
+
return createTrinity(namespace, createVirtual(items, options));
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Returns the current virtual context from dependency injection.
|
|
1798
|
+
*
|
|
1799
|
+
* @param namespace The namespace for the virtual context. Defaults to `v0:virtual`.
|
|
1800
|
+
* @template T The type of the items
|
|
1801
|
+
* @returns The current virtual context.
|
|
1802
|
+
*
|
|
1803
|
+
* @throws An error if the virtual context is not found and no default is provided.
|
|
1804
|
+
*
|
|
1805
|
+
* @see https://0.vuetifyjs.com/composables/data/create-virtual
|
|
1806
|
+
*
|
|
1807
|
+
* @example
|
|
1808
|
+
* ```vue
|
|
1809
|
+
* <script lang="ts" setup>
|
|
1810
|
+
* import { createVirtual } from '@vuetify/v0'
|
|
1811
|
+
*
|
|
1812
|
+
* // Inject virtual context provided by parent
|
|
1813
|
+
* const virtual = useVirtual()
|
|
1814
|
+
* <\/script>
|
|
1815
|
+
*
|
|
1816
|
+
* <template>
|
|
1817
|
+
* <div>
|
|
1818
|
+
* <p>Visible items: {{ virtual.items.value.length }}</p>
|
|
1819
|
+
* </div>
|
|
1820
|
+
* </template>
|
|
1821
|
+
* ```
|
|
1822
|
+
*/
|
|
1823
|
+
function useVirtual(namespace = "v0:virtual") {
|
|
1824
|
+
return useContext(namespace);
|
|
1825
|
+
}
|
|
1826
|
+
//#endregion
|
|
1827
|
+
//#region src/composables/toHighlight/index.ts
|
|
1828
|
+
/**
|
|
1829
|
+
* @module toHighlight
|
|
1830
|
+
*
|
|
1831
|
+
* @see https://0.vuetifyjs.com/composables/transformers/to-highlight
|
|
1832
|
+
*
|
|
1833
|
+
* @remarks
|
|
1834
|
+
* Pure transformer — no DOM, no state, no registry, no reactivity. Splits text
|
|
1835
|
+
* into matched and unmatched chunks given a query string, an array of query
|
|
1836
|
+
* strings, or pre-computed `[start, end]` match ranges (e.g., from createFilter).
|
|
1837
|
+
* Returns a plain array; wrap the call in `computed()` for reactive recomputation.
|
|
1838
|
+
*
|
|
1839
|
+
* @example
|
|
1840
|
+
* ```ts
|
|
1841
|
+
* import { toHighlight } from '@vuetify/v0'
|
|
1842
|
+
*
|
|
1843
|
+
* const chunks = toHighlight('Hello World', 'World')
|
|
1844
|
+
* // [{ text: 'Hello ', match: false }, { text: 'World', match: true }]
|
|
1845
|
+
* ```
|
|
1846
|
+
*/
|
|
1847
|
+
function merge(ranges) {
|
|
1848
|
+
const sorted = ranges.filter((span) => span[0] < span[1]).toSorted((a, b) => a[0] - b[0]);
|
|
1849
|
+
const merged = [];
|
|
1850
|
+
for (const span of sorted) {
|
|
1851
|
+
const last = merged.at(-1);
|
|
1852
|
+
if (last && span[0] <= last[1]) last[1] = Math.max(last[1], span[1]);
|
|
1853
|
+
else merged.push([span[0], span[1]]);
|
|
1854
|
+
}
|
|
1855
|
+
return merged;
|
|
1856
|
+
}
|
|
1857
|
+
function chunk(text, ranges) {
|
|
1858
|
+
const chunks = [];
|
|
1859
|
+
let cursor = 0;
|
|
1860
|
+
for (const [start, end] of ranges) {
|
|
1861
|
+
if (cursor < start) chunks.push({
|
|
1862
|
+
text: text.slice(cursor, start),
|
|
1863
|
+
match: false
|
|
1864
|
+
});
|
|
1865
|
+
chunks.push({
|
|
1866
|
+
text: text.slice(start, end),
|
|
1867
|
+
match: true
|
|
1868
|
+
});
|
|
1869
|
+
cursor = end;
|
|
1870
|
+
}
|
|
1871
|
+
if (cursor < text.length) chunks.push({
|
|
1872
|
+
text: text.slice(cursor),
|
|
1873
|
+
match: false
|
|
1874
|
+
});
|
|
1875
|
+
return chunks;
|
|
1876
|
+
}
|
|
1877
|
+
function find(text, query, matchAll, ignoreCase) {
|
|
1878
|
+
const terms = (/* @__PURE__ */ toArray(query)).filter(Boolean);
|
|
1879
|
+
const haystack = ignoreCase ? text.toLocaleLowerCase() : text;
|
|
1880
|
+
const spans = [];
|
|
1881
|
+
for (const term of terms) {
|
|
1882
|
+
const needle = ignoreCase ? term.toLocaleLowerCase() : term;
|
|
1883
|
+
let index = haystack.indexOf(needle);
|
|
1884
|
+
if (index !== -1) {
|
|
1885
|
+
spans.push([index, index + term.length]);
|
|
1886
|
+
if (matchAll) {
|
|
1887
|
+
index = haystack.indexOf(needle, index + term.length);
|
|
1888
|
+
while (index !== -1) {
|
|
1889
|
+
spans.push([index, index + term.length]);
|
|
1890
|
+
index = haystack.indexOf(needle, index + term.length);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return merge(spans);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Splits text into matched and unmatched chunks.
|
|
1899
|
+
*
|
|
1900
|
+
* Pure transformer — returns a plain array. Wrap the call in `computed()` for
|
|
1901
|
+
* reactive recomputation.
|
|
1902
|
+
*
|
|
1903
|
+
* Priority: `options.matches` (when non-empty) → `query` → no-match fallback.
|
|
1904
|
+
*
|
|
1905
|
+
* @param text The source string to split.
|
|
1906
|
+
* @param query One or more search terms. Empty strings are ignored. Case sensitivity controlled by `options.ignoreCase`.
|
|
1907
|
+
* @param options Optional `matches`, `matchAll`, `ignoreCase`.
|
|
1908
|
+
* @returns A `HighlightChunk[]` array.
|
|
1909
|
+
*
|
|
1910
|
+
* @see https://0.vuetifyjs.com/composables/transformers/to-highlight
|
|
1911
|
+
*
|
|
1912
|
+
* @example
|
|
1913
|
+
* ```ts
|
|
1914
|
+
* import { computed, shallowRef } from 'vue'
|
|
1915
|
+
* import { toHighlight } from '@vuetify/v0'
|
|
1916
|
+
*
|
|
1917
|
+
* const query = shallowRef('World')
|
|
1918
|
+
* const chunks = computed(() => toHighlight('Hello World', query))
|
|
1919
|
+
*
|
|
1920
|
+
* console.log(chunks.value)
|
|
1921
|
+
* // [{ text: 'Hello ', match: false }, { text: 'World', match: true }]
|
|
1922
|
+
* ```
|
|
1923
|
+
*/
|
|
1924
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
1925
|
+
function toHighlight(text, query, options = {}) {
|
|
1926
|
+
const _text = toValue(text);
|
|
1927
|
+
const _query = toValue(query);
|
|
1928
|
+
const _matches = toValue(options.matches);
|
|
1929
|
+
const matchAll = toValue(options.matchAll) ?? false;
|
|
1930
|
+
const ignoreCase = toValue(options.ignoreCase) ?? false;
|
|
1931
|
+
if (_matches?.length) return chunk(_text, merge(_matches));
|
|
1932
|
+
if (_query) {
|
|
1933
|
+
const ranges = find(_text, _query, matchAll, ignoreCase);
|
|
1934
|
+
return ranges.length > 0 ? chunk(_text, ranges) : [{
|
|
1935
|
+
text: _text,
|
|
1936
|
+
match: false
|
|
1937
|
+
}];
|
|
1938
|
+
}
|
|
1939
|
+
return [{
|
|
1940
|
+
text: _text,
|
|
1941
|
+
match: false
|
|
1942
|
+
}];
|
|
1943
|
+
}
|
|
1944
|
+
//#endregion
|
|
1945
|
+
//#region src/composables/toReactive/index.ts
|
|
1946
|
+
/**
|
|
1947
|
+
* @module toReactive
|
|
1948
|
+
*
|
|
1949
|
+
* @see https://0.vuetifyjs.com/composables/transformers/to-reactive
|
|
1950
|
+
*
|
|
1951
|
+
* @remarks
|
|
1952
|
+
* Utility function to convert values and refs into reactive proxies with ref unwrapping.
|
|
1953
|
+
*
|
|
1954
|
+
* Key features:
|
|
1955
|
+
* - Automatic ref unwrapping
|
|
1956
|
+
* - Deep reactive proxying
|
|
1957
|
+
* - Map and Set support with ref unwrapping
|
|
1958
|
+
* - Nested object/array reactivity
|
|
1959
|
+
* - Type preservation
|
|
1960
|
+
*
|
|
1961
|
+
* Perfect for creating reactive versions of plain objects while automatically unwrapping refs.
|
|
1962
|
+
*
|
|
1963
|
+
* @example
|
|
1964
|
+
* ```ts
|
|
1965
|
+
* import { shallowRef } from 'vue'
|
|
1966
|
+
* import { toReactive } from '@vuetify/v0'
|
|
1967
|
+
*
|
|
1968
|
+
* const source = { name: shallowRef('Alice'), age: 30 }
|
|
1969
|
+
* const reactive = toReactive(source)
|
|
1970
|
+
* console.log(reactive.name) // 'Alice' (auto-unwrapped)
|
|
1971
|
+
* ```
|
|
1972
|
+
*/
|
|
1973
|
+
function createMapProxy(refObject) {
|
|
1974
|
+
let mapProxy;
|
|
1975
|
+
const mapHandlers = {
|
|
1976
|
+
get: (key) => unref(refObject.value.get(key)),
|
|
1977
|
+
set: (key, value) => {
|
|
1978
|
+
const map = refObject.value;
|
|
1979
|
+
const existingValue = map.get(key);
|
|
1980
|
+
if (isRef(existingValue)) existingValue.value = unref(value);
|
|
1981
|
+
else map.set(key, value);
|
|
1982
|
+
return mapProxy;
|
|
1983
|
+
},
|
|
1984
|
+
has: (key) => refObject.value.has(key),
|
|
1985
|
+
delete: (key) => refObject.value.delete(key),
|
|
1986
|
+
clear: () => refObject.value.clear(),
|
|
1987
|
+
keys: () => refObject.value.keys(),
|
|
1988
|
+
*values() {
|
|
1989
|
+
for (const value of refObject.value.values()) yield unref(value);
|
|
1990
|
+
},
|
|
1991
|
+
*entries() {
|
|
1992
|
+
for (const [key, value] of refObject.value.entries()) yield [key, unref(value)];
|
|
1993
|
+
},
|
|
1994
|
+
forEach: (callback, thisArg) => {
|
|
1995
|
+
for (const [key, value] of refObject.value.entries()) callback.call(thisArg, unref(value), key, mapProxy);
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
mapHandlers[Symbol.iterator] = mapHandlers.entries;
|
|
1999
|
+
mapProxy = new Proxy(/* @__PURE__ */ new Map(), { get(_, p) {
|
|
2000
|
+
if (p === "size") return refObject.value.size;
|
|
2001
|
+
if (Object.prototype.hasOwnProperty.call(mapHandlers, p)) return mapHandlers[p];
|
|
2002
|
+
const map = refObject.value;
|
|
2003
|
+
const value = Reflect.get(map, p);
|
|
2004
|
+
return /* @__PURE__ */ isFunction(value) ? value.bind(map) : value;
|
|
2005
|
+
} });
|
|
2006
|
+
return reactive(mapProxy);
|
|
2007
|
+
}
|
|
2008
|
+
function createSetProxy(refObject) {
|
|
2009
|
+
let setProxy;
|
|
2010
|
+
const setHandlers = {
|
|
2011
|
+
add: (value) => {
|
|
2012
|
+
refObject.value.add(value);
|
|
2013
|
+
return setProxy;
|
|
2014
|
+
},
|
|
2015
|
+
has: (value) => refObject.value.has(value),
|
|
2016
|
+
delete: (value) => refObject.value.delete(value),
|
|
2017
|
+
clear: () => refObject.value.clear(),
|
|
2018
|
+
*keys() {
|
|
2019
|
+
for (const value of refObject.value.values()) yield unref(value);
|
|
2020
|
+
},
|
|
2021
|
+
*values() {
|
|
2022
|
+
for (const value of refObject.value.values()) yield unref(value);
|
|
2023
|
+
},
|
|
2024
|
+
*entries() {
|
|
2025
|
+
for (const value of refObject.value.values()) {
|
|
2026
|
+
const unreffedValue = unref(value);
|
|
2027
|
+
yield [unreffedValue, unreffedValue];
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
forEach: (callback, thisArg) => {
|
|
2031
|
+
for (const value of refObject.value) {
|
|
2032
|
+
const unreffedValue = unref(value);
|
|
2033
|
+
callback.call(thisArg, unreffedValue, unreffedValue, setProxy);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
setHandlers[Symbol.iterator] = setHandlers.values;
|
|
2038
|
+
setProxy = new Proxy(/* @__PURE__ */ new Set(), { get(_, p) {
|
|
2039
|
+
if (p === "size") return refObject.value.size;
|
|
2040
|
+
if (Object.prototype.hasOwnProperty.call(setHandlers, p)) return setHandlers[p];
|
|
2041
|
+
const set = refObject.value;
|
|
2042
|
+
const value = Reflect.get(set, p);
|
|
2043
|
+
return /* @__PURE__ */ isFunction(value) ? value.bind(set) : value;
|
|
2044
|
+
} });
|
|
2045
|
+
return reactive(setProxy);
|
|
2046
|
+
}
|
|
2047
|
+
function createObjectProxy(refObject) {
|
|
2048
|
+
return reactive(new Proxy({}, {
|
|
2049
|
+
get(_, p, receiver) {
|
|
2050
|
+
return unref(Reflect.get(refObject.value, p, receiver));
|
|
2051
|
+
},
|
|
2052
|
+
set(_, p, value) {
|
|
2053
|
+
return Reflect.set(refObject.value, p, value);
|
|
2054
|
+
},
|
|
2055
|
+
deleteProperty(_, p) {
|
|
2056
|
+
return Reflect.deleteProperty(refObject.value, p);
|
|
2057
|
+
},
|
|
2058
|
+
has(_, p) {
|
|
2059
|
+
return Reflect.has(refObject.value, p);
|
|
2060
|
+
},
|
|
2061
|
+
ownKeys() {
|
|
2062
|
+
return Reflect.ownKeys(refObject.value);
|
|
2063
|
+
},
|
|
2064
|
+
getOwnPropertyDescriptor(_, p) {
|
|
2065
|
+
const desc = Reflect.getOwnPropertyDescriptor(refObject.value, p);
|
|
2066
|
+
if (!desc) return;
|
|
2067
|
+
const newDesc = {
|
|
2068
|
+
configurable: true,
|
|
2069
|
+
enumerable: desc.enumerable
|
|
2070
|
+
};
|
|
2071
|
+
if ("value" in desc) {
|
|
2072
|
+
newDesc.value = unref(desc.value);
|
|
2073
|
+
newDesc.writable = desc.writable;
|
|
2074
|
+
} else {
|
|
2075
|
+
newDesc.get = desc.get;
|
|
2076
|
+
newDesc.set = desc.set;
|
|
2077
|
+
}
|
|
2078
|
+
return newDesc;
|
|
2079
|
+
}
|
|
2080
|
+
}));
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Converts a `MaybeRef` to a `UnwrapNestedRefs`.
|
|
2084
|
+
*
|
|
2085
|
+
* @param objectRef The object to convert.
|
|
2086
|
+
* @template Z The type of the object.
|
|
2087
|
+
* @returns The converted object.
|
|
2088
|
+
*
|
|
2089
|
+
* @see https://0.vuetifyjs.com/composables/transformers/to-reactive
|
|
2090
|
+
*
|
|
2091
|
+
* @example
|
|
2092
|
+
* ```ts
|
|
2093
|
+
* import { ref } from 'vue'
|
|
2094
|
+
* import { toReactive } from '@vuetify/v0'
|
|
2095
|
+
*
|
|
2096
|
+
* const state = ref({ name: 'John', age: 30 })
|
|
2097
|
+
* const rstate = toReactive(state)
|
|
2098
|
+
*
|
|
2099
|
+
* console.log(rstate.name) // John
|
|
2100
|
+
* ```
|
|
2101
|
+
*/
|
|
2102
|
+
function toReactive(objectRef) {
|
|
2103
|
+
if (!isRef(objectRef)) return reactive(objectRef);
|
|
2104
|
+
const refObject = objectRef;
|
|
2105
|
+
const target = refObject.value;
|
|
2106
|
+
if (target instanceof Map) return createMapProxy(refObject);
|
|
2107
|
+
if (target instanceof Set) return createSetProxy(refObject);
|
|
2108
|
+
return createObjectProxy(refObject);
|
|
2109
|
+
}
|
|
2110
|
+
//#endregion
|
|
2111
|
+
//#region src/composables/useBreakpoints/index.ts
|
|
2112
|
+
/**
|
|
2113
|
+
* @module useBreakpoints
|
|
2114
|
+
*
|
|
2115
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
|
|
2116
|
+
*
|
|
2117
|
+
* @remarks
|
|
2118
|
+
* Responsive breakpoint detection composable with window resize handling.
|
|
2119
|
+
*
|
|
2120
|
+
* Key features:
|
|
2121
|
+
* - matchMedia-based detection for zoom-accurate breakpoints
|
|
2122
|
+
* - Six built-in breakpoints (xs, sm, md, lg, xl, xxl) with Vuetify 4 default widths
|
|
2123
|
+
* - `isMobile` flag and configurable `mobileBreakpoint`
|
|
2124
|
+
* - Automatic resize listener with cleanup
|
|
2125
|
+
* - SSR-safe with hydration flush
|
|
2126
|
+
* - Custom breakpoint configuration
|
|
2127
|
+
*
|
|
2128
|
+
* Perfect for responsive layouts and conditional rendering based on screen size.
|
|
2129
|
+
*
|
|
2130
|
+
* @example
|
|
2131
|
+
* ```ts
|
|
2132
|
+
* import { useBreakpoints } from '@vuetify/v0'
|
|
2133
|
+
*
|
|
2134
|
+
* const { current, isMobile, smAndUp } = useBreakpoints()
|
|
2135
|
+
* console.log(current.value) // 'md'
|
|
2136
|
+
* ```
|
|
2137
|
+
*/
|
|
2138
|
+
/**
|
|
2139
|
+
* Creates default breakpoint configuration.
|
|
2140
|
+
*
|
|
2141
|
+
* @returns The default breakpoint configuration object.
|
|
2142
|
+
*/
|
|
2143
|
+
function createDefaultBreakpoints() {
|
|
2144
|
+
return {
|
|
2145
|
+
mobileBreakpoint: "lg",
|
|
2146
|
+
breakpoints: {
|
|
2147
|
+
xs: 0,
|
|
2148
|
+
sm: 600,
|
|
2149
|
+
md: 840,
|
|
2150
|
+
lg: 1145,
|
|
2151
|
+
xl: 1545,
|
|
2152
|
+
xxl: 2138
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Creates a new breakpoints instance.
|
|
2158
|
+
*
|
|
2159
|
+
* @param options The options for the breakpoints instance.
|
|
2160
|
+
* @returns A new breakpoints instance.
|
|
2161
|
+
*
|
|
2162
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-breakpoints
|
|
2163
|
+
*
|
|
2164
|
+
* @example
|
|
2165
|
+
* ```ts
|
|
2166
|
+
* import { createBreakpoints } from '@vuetify/v0'
|
|
2167
|
+
*
|
|
2168
|
+
* const breakpoints = createBreakpoints({
|
|
2169
|
+
* namespace: 'v0:breakpoints',
|
|
2170
|
+
* mobileBreakpoint: 'md',
|
|
2171
|
+
* breakpoints: {
|
|
2172
|
+
* xs: 0,
|
|
2173
|
+
* sm: 600,
|
|
2174
|
+
* md: 840,
|
|
2175
|
+
* lg: 1145,
|
|
2176
|
+
* xl: 1545,
|
|
2177
|
+
* xxl: 2138,
|
|
2178
|
+
* },
|
|
2179
|
+
* })
|
|
2180
|
+
* ```
|
|
2181
|
+
*/
|
|
2182
|
+
function createBreakpoints(_options = {}) {
|
|
2183
|
+
const { ssr, ...options } = _options;
|
|
2184
|
+
const { mobileBreakpoint, breakpoints } = /* @__PURE__ */ mergeDeep(createDefaultBreakpoints(), options);
|
|
2185
|
+
const sorted = Object.entries(breakpoints).toSorted((a, b) => a[1] - b[1]);
|
|
2186
|
+
const names = sorted.map(([n]) => n);
|
|
2187
|
+
const mb = /* @__PURE__ */ isNumber(mobileBreakpoint) ? mobileBreakpoint : breakpoints[mobileBreakpoint] ?? breakpoints.md;
|
|
2188
|
+
const initialWidth = ssr ? ssr.clientWidth : IN_BROWSER ? window.innerWidth : 0;
|
|
2189
|
+
const initialHeight = ssr ? ssr.clientHeight ?? 0 : IN_BROWSER ? window.innerHeight : 0;
|
|
2190
|
+
let initialName = "xs";
|
|
2191
|
+
for (let i = sorted.length - 1; i >= 0; i--) if (initialWidth >= sorted[i][1]) {
|
|
2192
|
+
initialName = sorted[i][0];
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
const initialIndex = names.indexOf(initialName);
|
|
2196
|
+
const name = shallowRef(initialName);
|
|
2197
|
+
const width = shallowRef(initialWidth);
|
|
2198
|
+
const height = shallowRef(initialHeight);
|
|
2199
|
+
const isMobile = shallowRef(initialWidth < mb);
|
|
2200
|
+
const xs = shallowRef(initialIndex === 0);
|
|
2201
|
+
const sm = shallowRef(initialIndex === 1);
|
|
2202
|
+
const md = shallowRef(initialIndex === 2);
|
|
2203
|
+
const lg = shallowRef(initialIndex === 3);
|
|
2204
|
+
const xl = shallowRef(initialIndex === 4);
|
|
2205
|
+
const xxl = shallowRef(initialIndex === 5);
|
|
2206
|
+
const smAndUp = shallowRef(initialIndex >= 1);
|
|
2207
|
+
const mdAndUp = shallowRef(initialIndex >= 2);
|
|
2208
|
+
const lgAndUp = shallowRef(initialIndex >= 3);
|
|
2209
|
+
const xlAndUp = shallowRef(initialIndex >= 4);
|
|
2210
|
+
const smAndDown = shallowRef(initialIndex <= 1);
|
|
2211
|
+
const mdAndDown = shallowRef(initialIndex <= 2);
|
|
2212
|
+
const lgAndDown = shallowRef(initialIndex <= 3);
|
|
2213
|
+
const xlAndDown = shallowRef(initialIndex <= 4);
|
|
2214
|
+
function update() {
|
|
2215
|
+
if (!IN_BROWSER) return;
|
|
2216
|
+
width.value = window.innerWidth;
|
|
2217
|
+
height.value = window.innerHeight;
|
|
2218
|
+
let current = "xs";
|
|
2219
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
2220
|
+
const px = sorted[i][1];
|
|
2221
|
+
if (SUPPORTS_MATCH_MEDIA ? window.matchMedia(`(min-width: ${px}px)`).matches : width.value >= px) {
|
|
2222
|
+
current = sorted[i][0];
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
name.value = current;
|
|
2227
|
+
const index = names.indexOf(current);
|
|
2228
|
+
isMobile.value = /* @__PURE__ */ isNumber(mb) ? SUPPORTS_MATCH_MEDIA ? !window.matchMedia(`(min-width: ${mb}px)`).matches : width.value < mb : index < names.indexOf(mobileBreakpoint);
|
|
2229
|
+
xs.value = index === 0;
|
|
2230
|
+
sm.value = index === 1;
|
|
2231
|
+
md.value = index === 2;
|
|
2232
|
+
lg.value = index === 3;
|
|
2233
|
+
xl.value = index === 4;
|
|
2234
|
+
xxl.value = index === 5;
|
|
2235
|
+
smAndUp.value = index >= 1;
|
|
2236
|
+
mdAndUp.value = index >= 2;
|
|
2237
|
+
lgAndUp.value = index >= 3;
|
|
2238
|
+
xlAndUp.value = index >= 4;
|
|
2239
|
+
smAndDown.value = index <= 1;
|
|
2240
|
+
mdAndDown.value = index <= 2;
|
|
2241
|
+
lgAndDown.value = index <= 3;
|
|
2242
|
+
xlAndDown.value = index <= 4;
|
|
2243
|
+
}
|
|
2244
|
+
return {
|
|
2245
|
+
breakpoints,
|
|
2246
|
+
mobileBreakpoint,
|
|
2247
|
+
name: readonly(name),
|
|
2248
|
+
width: readonly(width),
|
|
2249
|
+
height: readonly(height),
|
|
2250
|
+
isMobile: readonly(isMobile),
|
|
2251
|
+
xs: readonly(xs),
|
|
2252
|
+
sm: readonly(sm),
|
|
2253
|
+
md: readonly(md),
|
|
2254
|
+
lg: readonly(lg),
|
|
2255
|
+
xl: readonly(xl),
|
|
2256
|
+
xxl: readonly(xxl),
|
|
2257
|
+
smAndUp: readonly(smAndUp),
|
|
2258
|
+
mdAndUp: readonly(mdAndUp),
|
|
2259
|
+
lgAndUp: readonly(lgAndUp),
|
|
2260
|
+
xlAndUp: readonly(xlAndUp),
|
|
2261
|
+
smAndDown: readonly(smAndDown),
|
|
2262
|
+
mdAndDown: readonly(mdAndDown),
|
|
2263
|
+
lgAndDown: readonly(lgAndDown),
|
|
2264
|
+
xlAndDown: readonly(xlAndDown),
|
|
2265
|
+
ssr: !!ssr,
|
|
2266
|
+
update
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function createBreakpointsFallback(options = {}) {
|
|
2270
|
+
if (options.ssr) return createBreakpoints(options);
|
|
2271
|
+
const defaults = createDefaultBreakpoints();
|
|
2272
|
+
return {
|
|
2273
|
+
breakpoints: defaults.breakpoints,
|
|
2274
|
+
mobileBreakpoint: defaults.mobileBreakpoint,
|
|
2275
|
+
name: readonly(shallowRef("xs")),
|
|
2276
|
+
width: readonly(shallowRef(0)),
|
|
2277
|
+
height: readonly(shallowRef(0)),
|
|
2278
|
+
isMobile: readonly(shallowRef(true)),
|
|
2279
|
+
xs: readonly(shallowRef(true)),
|
|
2280
|
+
sm: readonly(shallowRef(false)),
|
|
2281
|
+
md: readonly(shallowRef(false)),
|
|
2282
|
+
lg: readonly(shallowRef(false)),
|
|
2283
|
+
xl: readonly(shallowRef(false)),
|
|
2284
|
+
xxl: readonly(shallowRef(false)),
|
|
2285
|
+
smAndUp: readonly(shallowRef(false)),
|
|
2286
|
+
mdAndUp: readonly(shallowRef(false)),
|
|
2287
|
+
lgAndUp: readonly(shallowRef(false)),
|
|
2288
|
+
xlAndUp: readonly(shallowRef(false)),
|
|
2289
|
+
smAndDown: readonly(shallowRef(true)),
|
|
2290
|
+
mdAndDown: readonly(shallowRef(true)),
|
|
2291
|
+
lgAndDown: readonly(shallowRef(true)),
|
|
2292
|
+
xlAndDown: readonly(shallowRef(true)),
|
|
2293
|
+
ssr: false,
|
|
2294
|
+
update: () => {}
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
const [createBreakpointsContext, createBreakpointsPlugin, useBreakpoints] = createPluginContext("v0:breakpoints", (options) => createBreakpoints(options), {
|
|
2298
|
+
fallback: () => createBreakpointsFallback(),
|
|
2299
|
+
setup: (context, app, _options) => {
|
|
2300
|
+
if (IN_BROWSER && !_options?.ssr) context.update();
|
|
2301
|
+
const { mount } = app;
|
|
2302
|
+
app.mount = (...args) => {
|
|
2303
|
+
const vm = mount(...args);
|
|
2304
|
+
const hydration = useHydration();
|
|
2305
|
+
function listener() {
|
|
2306
|
+
context.update();
|
|
2307
|
+
}
|
|
2308
|
+
const unwatch = watch(hydration.isHydrated, (hydrated) => {
|
|
2309
|
+
if (hydrated) listener();
|
|
2310
|
+
}, { immediate: true });
|
|
2311
|
+
const cleanup = useWindowEventListener("resize", listener, { passive: true });
|
|
2312
|
+
onScopeDispose(() => {
|
|
2313
|
+
cleanup();
|
|
2314
|
+
unwatch();
|
|
2315
|
+
}, true);
|
|
2316
|
+
app.mount = mount;
|
|
2317
|
+
return vm;
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
//#endregion
|
|
2322
|
+
//#region src/composables/useDate/index.ts
|
|
2323
|
+
/**
|
|
2324
|
+
* @module useDate
|
|
2325
|
+
*
|
|
2326
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
2327
|
+
*
|
|
2328
|
+
* @remarks
|
|
2329
|
+
* Date manipulation composable with adapter pattern for date operations.
|
|
2330
|
+
*
|
|
2331
|
+
* Key features:
|
|
2332
|
+
* - Adapter pattern for date library abstraction
|
|
2333
|
+
* - Locale-aware formatting via Intl.DateTimeFormat
|
|
2334
|
+
* - `firstDayOfWeek` derived from locale and propagated to adapter
|
|
2335
|
+
* - Integration with useLocale for automatic locale sync
|
|
2336
|
+
*
|
|
2337
|
+
* @example Using the built-in Temporal adapter
|
|
2338
|
+
* ```ts
|
|
2339
|
+
* import { V0DateAdapter } from '@vuetify/v0/date'
|
|
2340
|
+
* import { createDatePlugin } from '@vuetify/v0'
|
|
2341
|
+
*
|
|
2342
|
+
* const app = createApp(App)
|
|
2343
|
+
* app.use(createDatePlugin({ adapter: new V0DateAdapter() }))
|
|
2344
|
+
*
|
|
2345
|
+
* // In a component:
|
|
2346
|
+
* const { adapter } = useDate()
|
|
2347
|
+
* const today = adapter.date()
|
|
2348
|
+
* const formatted = adapter.format(today, 'fullDate')
|
|
2349
|
+
* ```
|
|
2350
|
+
*
|
|
2351
|
+
* @example Custom adapter
|
|
2352
|
+
* ```ts
|
|
2353
|
+
* import { DateAdapter } from '@vuetify/v0'
|
|
2354
|
+
*
|
|
2355
|
+
* class DateFnsAdapter extends DateAdapter<Date> {
|
|
2356
|
+
* // ... implementation
|
|
2357
|
+
* }
|
|
2358
|
+
*
|
|
2359
|
+
* app.use(createDatePlugin({ adapter: new DateFnsAdapter() }))
|
|
2360
|
+
* ```
|
|
2361
|
+
*/
|
|
2362
|
+
/**
|
|
2363
|
+
* Default short locale codes mapped to full Intl locale strings.
|
|
2364
|
+
* Used when useLocale provides a short code without region (e.g., 'en' instead of 'en-US').
|
|
2365
|
+
*/
|
|
2366
|
+
const defaultLocales = {
|
|
2367
|
+
en: "en-US",
|
|
2368
|
+
es: "es-ES",
|
|
2369
|
+
fr: "fr-FR",
|
|
2370
|
+
de: "de-DE",
|
|
2371
|
+
it: "it-IT",
|
|
2372
|
+
pt: "pt-BR",
|
|
2373
|
+
ja: "ja-JP",
|
|
2374
|
+
ko: "ko-KR",
|
|
2375
|
+
zh: "zh-CN",
|
|
2376
|
+
ru: "ru-RU",
|
|
2377
|
+
ar: "ar-SA"
|
|
2378
|
+
};
|
|
2379
|
+
/**
|
|
2380
|
+
* Derive firstDayOfWeek from an Intl locale string.
|
|
2381
|
+
* Returns 0-6 (0=Sun) or 0 as fallback.
|
|
2382
|
+
*/
|
|
2383
|
+
function deriveFirstDayOfWeek(locale) {
|
|
2384
|
+
try {
|
|
2385
|
+
const info = new Intl.Locale(locale).getWeekInfo?.();
|
|
2386
|
+
/* v8 ignore next -- defensive: getWeekInfo always returns info in Node 22+ */
|
|
2387
|
+
return info ? info.firstDay % 7 : 0;
|
|
2388
|
+
} catch {
|
|
2389
|
+
return 0;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Creates a new date context.
|
|
2394
|
+
*
|
|
2395
|
+
* @param options Adapter and locale configuration.
|
|
2396
|
+
* @template Z The date type used by the adapter.
|
|
2397
|
+
* @template E The date context type.
|
|
2398
|
+
* @returns A date context.
|
|
2399
|
+
*
|
|
2400
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
2401
|
+
*
|
|
2402
|
+
* @example
|
|
2403
|
+
* ```ts
|
|
2404
|
+
* import { V0DateAdapter } from '@vuetify/v0/date'
|
|
2405
|
+
*
|
|
2406
|
+
* const { adapter } = createDate({ adapter: new V0DateAdapter() })
|
|
2407
|
+
* const today = adapter.date()
|
|
2408
|
+
*
|
|
2409
|
+
* // With locale options
|
|
2410
|
+
* const { adapter } = createDate({
|
|
2411
|
+
* adapter: new V0DateAdapter(),
|
|
2412
|
+
* locale: 'de-DE',
|
|
2413
|
+
* })
|
|
2414
|
+
* ```
|
|
2415
|
+
*/
|
|
2416
|
+
function createDate(options) {
|
|
2417
|
+
const { locales = defaultLocales, adapter, locale: initialLocale, firstDayOfWeek: explicitFirstDay } = options;
|
|
2418
|
+
let selectedId;
|
|
2419
|
+
try {
|
|
2420
|
+
if (/* @__PURE__ */ instanceExists()) selectedId = useLocale().selectedId;
|
|
2421
|
+
} catch {}
|
|
2422
|
+
const locale = computed(() => {
|
|
2423
|
+
const selected = selectedId?.value;
|
|
2424
|
+
if (!/* @__PURE__ */ isNullOrUndefined(selected)) {
|
|
2425
|
+
const str = String(selected);
|
|
2426
|
+
return str.includes("-") ? str : locales[str] ?? str;
|
|
2427
|
+
}
|
|
2428
|
+
const fallback = initialLocale ?? adapter.locale ?? "en-US";
|
|
2429
|
+
return fallback.includes("-") ? fallback : locales[fallback] ?? fallback;
|
|
2430
|
+
});
|
|
2431
|
+
const firstDayOfWeek = computed(() => {
|
|
2432
|
+
if (!/* @__PURE__ */ isUndefined(explicitFirstDay)) return explicitFirstDay;
|
|
2433
|
+
const loc = locale.value;
|
|
2434
|
+
return loc ? deriveFirstDayOfWeek(loc) : 0;
|
|
2435
|
+
});
|
|
2436
|
+
if (/* @__PURE__ */ instanceExists()) onScopeDispose(watchEffect(() => {
|
|
2437
|
+
const loc = locale.value;
|
|
2438
|
+
if (loc && adapter.locale !== loc) adapter.locale = loc;
|
|
2439
|
+
const fdow = firstDayOfWeek.value;
|
|
2440
|
+
if (adapter.firstDayOfWeek !== fdow) adapter.firstDayOfWeek = fdow;
|
|
2441
|
+
}));
|
|
2442
|
+
else {
|
|
2443
|
+
const loc = locale.value;
|
|
2444
|
+
if (loc && adapter.locale !== loc) adapter.locale = loc;
|
|
2445
|
+
const fdow = firstDayOfWeek.value;
|
|
2446
|
+
if (adapter.firstDayOfWeek !== fdow) adapter.firstDayOfWeek = fdow;
|
|
2447
|
+
}
|
|
2448
|
+
return {
|
|
2449
|
+
adapter,
|
|
2450
|
+
locale,
|
|
2451
|
+
firstDayOfWeek
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Creates a new date context trinity.
|
|
2456
|
+
*
|
|
2457
|
+
* @param options Adapter, locale, and namespace configuration.
|
|
2458
|
+
* @template Z The date type used by the adapter.
|
|
2459
|
+
* @template E The date context type.
|
|
2460
|
+
* @returns A trinity [useContext, provideContext, defaultContext].
|
|
2461
|
+
*
|
|
2462
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
2463
|
+
*
|
|
2464
|
+
* @example
|
|
2465
|
+
* ```ts
|
|
2466
|
+
* import { V0DateAdapter } from '@vuetify/v0/date'
|
|
2467
|
+
*
|
|
2468
|
+
* const [useAppDate, provideAppDate] = createDateContext({
|
|
2469
|
+
* adapter: new V0DateAdapter(),
|
|
2470
|
+
* namespace: 'app:date',
|
|
2471
|
+
* })
|
|
2472
|
+
* ```
|
|
2473
|
+
*/
|
|
2474
|
+
function createDateContext(_options) {
|
|
2475
|
+
const { namespace = "v0:date", ...options } = _options;
|
|
2476
|
+
return createTrinity(namespace, createDate(options));
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Creates a new date plugin.
|
|
2480
|
+
*
|
|
2481
|
+
* @param options Adapter, locale, and namespace configuration.
|
|
2482
|
+
* @template Z The date type used by the adapter.
|
|
2483
|
+
* @template E The date context type.
|
|
2484
|
+
* @returns A Vue plugin.
|
|
2485
|
+
*
|
|
2486
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
2487
|
+
*
|
|
2488
|
+
* @example
|
|
2489
|
+
* ```ts
|
|
2490
|
+
* import { V0DateAdapter } from '@vuetify/v0/date'
|
|
2491
|
+
*
|
|
2492
|
+
* const app = createApp(App)
|
|
2493
|
+
* app.use(createDatePlugin({ adapter: new V0DateAdapter() }))
|
|
2494
|
+
*
|
|
2495
|
+
* // With locale options
|
|
2496
|
+
* app.use(createDatePlugin({
|
|
2497
|
+
* adapter: new V0DateAdapter(),
|
|
2498
|
+
* locale: 'de-DE',
|
|
2499
|
+
* }))
|
|
2500
|
+
* ```
|
|
2501
|
+
*/
|
|
2502
|
+
function createDatePlugin(_options) {
|
|
2503
|
+
const { namespace = "v0:date", ...options } = _options;
|
|
2504
|
+
const [, provideDateContext, context] = createDateContext({
|
|
2505
|
+
namespace,
|
|
2506
|
+
...options
|
|
2507
|
+
});
|
|
2508
|
+
return createPlugin({
|
|
2509
|
+
namespace,
|
|
2510
|
+
provide: (app) => {
|
|
2511
|
+
provideDateContext(context, app);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Returns the current date context.
|
|
2517
|
+
*
|
|
2518
|
+
* Requires `createDatePlugin` to be installed with an adapter.
|
|
2519
|
+
*
|
|
2520
|
+
* @param namespace The namespace to look up (defaults to 'v0:date').
|
|
2521
|
+
* @template Z The date type used by the adapter.
|
|
2522
|
+
* @template E The date context type.
|
|
2523
|
+
* @returns The current date context.
|
|
2524
|
+
* @throws If called outside a component or without a date plugin installed.
|
|
2525
|
+
*
|
|
2526
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-date
|
|
2527
|
+
*
|
|
2528
|
+
* @example
|
|
2529
|
+
* ```ts
|
|
2530
|
+
* // main.ts
|
|
2531
|
+
* import { V0DateAdapter } from '@vuetify/v0/date'
|
|
2532
|
+
* import { createDatePlugin } from '@vuetify/v0'
|
|
2533
|
+
*
|
|
2534
|
+
* app.use(createDatePlugin({ adapter: new V0DateAdapter() }))
|
|
2535
|
+
* ```
|
|
2536
|
+
*
|
|
2537
|
+
* @example
|
|
2538
|
+
* ```vue
|
|
2539
|
+
* <script setup lang="ts">
|
|
2540
|
+
* import { useDate } from '@vuetify/v0'
|
|
2541
|
+
*
|
|
2542
|
+
* const { adapter, locale } = useDate()
|
|
2543
|
+
* const today = adapter.date()
|
|
2544
|
+
* <\/script>
|
|
2545
|
+
* ```
|
|
2546
|
+
*/
|
|
2547
|
+
function useDate(namespace = "v0:date") {
|
|
2548
|
+
if (!/* @__PURE__ */ instanceExists()) throw new V0Error("[v0] useDate() must be called inside a Vue component with createDatePlugin installed.\n\nExample:\n import { V0DateAdapter } from '@vuetify/v0/date'\n import { createDatePlugin } from '@vuetify/v0'\n\n app.use(createDatePlugin({ adapter: new V0DateAdapter() }))", {
|
|
2549
|
+
code: "V0_PLUGIN_MISSING",
|
|
2550
|
+
plugin: "createDatePlugin"
|
|
2551
|
+
});
|
|
2552
|
+
return useContext(namespace);
|
|
2553
|
+
}
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region src/composables/useMutationObserver/index.ts
|
|
2556
|
+
/**
|
|
2557
|
+
* @module useMutationObserver
|
|
2558
|
+
*
|
|
2559
|
+
* @see https://0.vuetifyjs.com/composables/system/use-mutation-observer
|
|
2560
|
+
*
|
|
2561
|
+
* @remarks
|
|
2562
|
+
* MutationObserver composable with lifecycle management.
|
|
2563
|
+
*
|
|
2564
|
+
* Key features:
|
|
2565
|
+
* - MutationObserver API wrapper
|
|
2566
|
+
* - `isActive` reactive flag
|
|
2567
|
+
* - `once` option for single-fire observation
|
|
2568
|
+
* - Pause/resume/stop functionality
|
|
2569
|
+
* - Automatic cleanup on unmount
|
|
2570
|
+
* - SSR-safe (checks SUPPORTS_MUTATION_OBSERVER)
|
|
2571
|
+
* - Hydration-aware
|
|
2572
|
+
* - Configurable observation options (childList, attributes, characterData, etc.)
|
|
2573
|
+
*
|
|
2574
|
+
* Perfect for detecting DOM changes and responding to mutations.
|
|
2575
|
+
*
|
|
2576
|
+
* @example
|
|
2577
|
+
* ```ts
|
|
2578
|
+
* import { useTemplateRef } from 'vue'
|
|
2579
|
+
* import { useMutationObserver } from '@vuetify/v0'
|
|
2580
|
+
*
|
|
2581
|
+
* const target = useTemplateRef('target')
|
|
2582
|
+
* useMutationObserver(target, records => {
|
|
2583
|
+
* console.log(records)
|
|
2584
|
+
* }, { childList: true })
|
|
2585
|
+
* ```
|
|
2586
|
+
*/
|
|
2587
|
+
/**
|
|
2588
|
+
* A composable that uses the Mutation Observer API to detect changes in the DOM.
|
|
2589
|
+
*
|
|
2590
|
+
* @param target The element to observe.
|
|
2591
|
+
* @param callback The callback to execute when a mutation is observed.
|
|
2592
|
+
* @param options The options for the Mutation Observer.
|
|
2593
|
+
* @returns An object with methods to control the observer.
|
|
2594
|
+
*
|
|
2595
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
|
2596
|
+
* @see https://0.vuetifyjs.com/composables/system/use-mutation-observer
|
|
2597
|
+
*
|
|
2598
|
+
* @example
|
|
2599
|
+
* ```ts
|
|
2600
|
+
* import { ref } from 'vue'
|
|
2601
|
+
* import { useMutationObserver } from '@vuetify/v0'
|
|
2602
|
+
*
|
|
2603
|
+
* const container = ref<HTMLElement>()
|
|
2604
|
+
*
|
|
2605
|
+
* const { pause, resume, isPaused } = useMutationObserver(
|
|
2606
|
+
* container,
|
|
2607
|
+
* (mutations) => {
|
|
2608
|
+
* mutations.forEach((mutation) => {
|
|
2609
|
+
* if (mutation.type === 'childList') {
|
|
2610
|
+
* console.log('Children changed:', mutation.addedNodes, mutation.removedNodes)
|
|
2611
|
+
* } else if (mutation.type === 'attributes') {
|
|
2612
|
+
* console.log('Attribute changed:', mutation.attributeName)
|
|
2613
|
+
* }
|
|
2614
|
+
* })
|
|
2615
|
+
* },
|
|
2616
|
+
* {
|
|
2617
|
+
* childList: true,
|
|
2618
|
+
* attributes: true,
|
|
2619
|
+
* subtree: true
|
|
2620
|
+
* }
|
|
2621
|
+
* )
|
|
2622
|
+
*
|
|
2623
|
+
* // Pause observation
|
|
2624
|
+
* pause()
|
|
2625
|
+
*
|
|
2626
|
+
* // Resume observation
|
|
2627
|
+
* resume()
|
|
2628
|
+
* ```
|
|
2629
|
+
*/
|
|
2630
|
+
function useMutationObserver(target, callback, options = {}) {
|
|
2631
|
+
const observerOptions = {
|
|
2632
|
+
childList: options.childList ?? true,
|
|
2633
|
+
attributes: options.attributes ?? false,
|
|
2634
|
+
characterData: options.characterData ?? false,
|
|
2635
|
+
subtree: options.subtree ?? false,
|
|
2636
|
+
attributeOldValue: options.attributeOldValue ?? false,
|
|
2637
|
+
characterDataOldValue: options.characterDataOldValue ?? false,
|
|
2638
|
+
attributeFilter: options.attributeFilter
|
|
2639
|
+
};
|
|
2640
|
+
return createObserver(target, callback, {
|
|
2641
|
+
supports: SUPPORTS_MUTATION_OBSERVER,
|
|
2642
|
+
once: options.once,
|
|
2643
|
+
create: (cb) => new MutationObserver((mutations) => {
|
|
2644
|
+
cb(mutations.map((m) => ({
|
|
2645
|
+
type: m.type,
|
|
2646
|
+
target: m.target,
|
|
2647
|
+
addedNodes: m.addedNodes,
|
|
2648
|
+
removedNodes: m.removedNodes,
|
|
2649
|
+
previousSibling: m.previousSibling,
|
|
2650
|
+
nextSibling: m.nextSibling,
|
|
2651
|
+
attributeName: m.attributeName,
|
|
2652
|
+
attributeNamespace: m.attributeNamespace,
|
|
2653
|
+
oldValue: m.oldValue
|
|
2654
|
+
})));
|
|
2655
|
+
}),
|
|
2656
|
+
observe: (obs, el) => obs.observe(el, observerOptions),
|
|
2657
|
+
immediate: options.immediate ? (el) => {
|
|
2658
|
+
const emptyNodeList = {
|
|
2659
|
+
length: 0,
|
|
2660
|
+
item: () => null,
|
|
2661
|
+
forEach: () => {},
|
|
2662
|
+
*[Symbol.iterator]() {}
|
|
2663
|
+
};
|
|
2664
|
+
return [{
|
|
2665
|
+
type: "childList",
|
|
2666
|
+
target: el,
|
|
2667
|
+
addedNodes: emptyNodeList,
|
|
2668
|
+
removedNodes: emptyNodeList,
|
|
2669
|
+
previousSibling: null,
|
|
2670
|
+
nextSibling: null,
|
|
2671
|
+
attributeName: null,
|
|
2672
|
+
attributeNamespace: null,
|
|
2673
|
+
oldValue: null
|
|
2674
|
+
}];
|
|
2675
|
+
} : void 0,
|
|
2676
|
+
onceIncludesImmediate: true
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
//#endregion
|
|
2680
|
+
//#region src/composables/useDragDrop/adapters/adapter.ts
|
|
2681
|
+
/**
|
|
2682
|
+
* @module useDragDrop/adapters/adapter
|
|
2683
|
+
*
|
|
2684
|
+
* @remarks
|
|
2685
|
+
* Adapter contract and abstract base for useDragDrop. Concrete adapters extend
|
|
2686
|
+
* `DragDropAdapter<Z>` to share the `cleanup` field, `dispose()` lifecycle, and
|
|
2687
|
+
* the `locate()` DOM-walk helper.
|
|
2688
|
+
*/
|
|
2689
|
+
/**
|
|
2690
|
+
* Abstract base class for useDragDrop adapters. Provides a `cleanup` slot, a
|
|
2691
|
+
* `dispose()` lifecycle, and a `locate()` helper that walks the DOM to find the
|
|
2692
|
+
* draggable ticket whose `el` ancestors a given event target.
|
|
2693
|
+
*
|
|
2694
|
+
* @example
|
|
2695
|
+
* ```ts
|
|
2696
|
+
* import { DragDropAdapter, type DragDropAdapterContext } from '@vuetify/v0'
|
|
2697
|
+
*
|
|
2698
|
+
* class MyAdapter extends DragDropAdapter {
|
|
2699
|
+
* setup (context: DragDropAdapterContext) {
|
|
2700
|
+
* const stop = useEventListener(document, 'mousedown', event => {
|
|
2701
|
+
* const ticket = this.locate(event.target, context)
|
|
2702
|
+
* if (ticket) context.emit.start(ticket, { x: event.clientX, y: event.clientY }, 'pointer')
|
|
2703
|
+
* })
|
|
2704
|
+
* this.cleanup = () => stop()
|
|
2705
|
+
* }
|
|
2706
|
+
* }
|
|
2707
|
+
* ```
|
|
2708
|
+
*/
|
|
2709
|
+
var DragDropAdapter = class {
|
|
2710
|
+
cleanup = null;
|
|
2711
|
+
dispose() {
|
|
2712
|
+
this.cleanup?.();
|
|
2713
|
+
this.cleanup = null;
|
|
2714
|
+
}
|
|
2715
|
+
locate(target, context) {
|
|
2716
|
+
let element = /* @__PURE__ */ isElement(target) ? target : null;
|
|
2717
|
+
while (element) {
|
|
2718
|
+
for (const ticket of context.draggables.values()) if (ticket.el.value === element) return ticket;
|
|
2719
|
+
element = element.parentElement;
|
|
2720
|
+
}
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
2724
|
+
//#endregion
|
|
2725
|
+
//#region src/composables/useDragDrop/adapters/pointer.ts
|
|
2726
|
+
/**
|
|
2727
|
+
* @module useDragDrop/adapters/pointer
|
|
2728
|
+
*
|
|
2729
|
+
* @remarks
|
|
2730
|
+
* Default pointer adapter for useDragDrop. Listens for `pointerdown` /
|
|
2731
|
+
* `pointermove` / `pointerup` / `pointercancel` on the document and emits the
|
|
2732
|
+
* drag lifecycle. Activation distance is configurable via `threshold`.
|
|
2733
|
+
*/
|
|
2734
|
+
/**
|
|
2735
|
+
* Default pointer adapter. Wires `pointerdown` / `pointermove` / `pointerup` /
|
|
2736
|
+
* `pointercancel` on the document and converts them into drag lifecycle calls.
|
|
2737
|
+
*
|
|
2738
|
+
* @example
|
|
2739
|
+
* ```ts
|
|
2740
|
+
* import { PointerAdapter, useDragDrop } from '@vuetify/v0'
|
|
2741
|
+
*
|
|
2742
|
+
* useDragDrop({ adapters: [new PointerAdapter({ threshold: 10 })] })
|
|
2743
|
+
* ```
|
|
2744
|
+
*/
|
|
2745
|
+
var PointerAdapter = class extends DragDropAdapter {
|
|
2746
|
+
threshold;
|
|
2747
|
+
constructor(options = {}) {
|
|
2748
|
+
super();
|
|
2749
|
+
this.threshold = options.threshold ?? 0;
|
|
2750
|
+
}
|
|
2751
|
+
setup(context) {
|
|
2752
|
+
const logger = useLogger();
|
|
2753
|
+
if (this.cleanup) {
|
|
2754
|
+
logger.warn("PointerAdapter setup called twice; previous registration will be replaced");
|
|
2755
|
+
this.dispose();
|
|
2756
|
+
}
|
|
2757
|
+
if (!IN_BROWSER) return;
|
|
2758
|
+
let downSource = null;
|
|
2759
|
+
let downOrigin = null;
|
|
2760
|
+
let downId = null;
|
|
2761
|
+
let started = false;
|
|
2762
|
+
function reset() {
|
|
2763
|
+
downSource = null;
|
|
2764
|
+
downOrigin = null;
|
|
2765
|
+
downId = null;
|
|
2766
|
+
started = false;
|
|
2767
|
+
}
|
|
2768
|
+
const onDown = (event) => {
|
|
2769
|
+
if (event.button !== 0) return;
|
|
2770
|
+
if (downSource) return;
|
|
2771
|
+
const ticket = this.locate(event.target, context);
|
|
2772
|
+
if (!ticket) return;
|
|
2773
|
+
downSource = ticket;
|
|
2774
|
+
downOrigin = {
|
|
2775
|
+
x: event.clientX,
|
|
2776
|
+
y: event.clientY
|
|
2777
|
+
};
|
|
2778
|
+
downId = event.pointerId;
|
|
2779
|
+
started = false;
|
|
2780
|
+
};
|
|
2781
|
+
const onMove = (event) => {
|
|
2782
|
+
if (!downSource || !downOrigin || event.pointerId !== downId) return;
|
|
2783
|
+
const point = {
|
|
2784
|
+
x: event.clientX,
|
|
2785
|
+
y: event.clientY
|
|
2786
|
+
};
|
|
2787
|
+
if (!started) {
|
|
2788
|
+
const dx = point.x - downOrigin.x;
|
|
2789
|
+
const dy = point.y - downOrigin.y;
|
|
2790
|
+
if (this.threshold > 0 && Math.hypot(dx, dy) < this.threshold) return;
|
|
2791
|
+
context.emit.start(downSource, downOrigin, "pointer");
|
|
2792
|
+
started = true;
|
|
2793
|
+
}
|
|
2794
|
+
context.emit.move(point);
|
|
2795
|
+
};
|
|
2796
|
+
function onUp(event) {
|
|
2797
|
+
if (event.pointerId !== downId) return;
|
|
2798
|
+
if (downSource && started) context.emit.drop();
|
|
2799
|
+
reset();
|
|
2800
|
+
}
|
|
2801
|
+
function onCancel(event) {
|
|
2802
|
+
if (event.pointerId !== downId) return;
|
|
2803
|
+
if (downSource && started) context.emit.cancel();
|
|
2804
|
+
reset();
|
|
2805
|
+
}
|
|
2806
|
+
const stopDown = useEventListener(document, "pointerdown", onDown);
|
|
2807
|
+
const stopMove = useEventListener(document, "pointermove", onMove);
|
|
2808
|
+
const stopUp = useEventListener(document, "pointerup", onUp);
|
|
2809
|
+
const stopCancel = useEventListener(document, "pointercancel", onCancel);
|
|
2810
|
+
this.cleanup = () => {
|
|
2811
|
+
stopDown();
|
|
2812
|
+
stopMove();
|
|
2813
|
+
stopUp();
|
|
2814
|
+
stopCancel();
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
//#endregion
|
|
2819
|
+
//#region src/composables/useDragDrop/adapters/keyboard.ts
|
|
2820
|
+
/**
|
|
2821
|
+
* @module useDragDrop/adapters/keyboard
|
|
2822
|
+
*
|
|
2823
|
+
* @remarks
|
|
2824
|
+
* Default keyboard adapter for useDragDrop. Listens for `keydown` on the
|
|
2825
|
+
* document. Activation keys (default Space and Enter) start and drop drags;
|
|
2826
|
+
* arrow keys nudge the drag point by `step` px; Escape cancels.
|
|
2827
|
+
*/
|
|
2828
|
+
function isEditable(element) {
|
|
2829
|
+
if (/* @__PURE__ */ isNull(element)) return false;
|
|
2830
|
+
const tag = element.tagName;
|
|
2831
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
2832
|
+
if (element.isContentEditable) return true;
|
|
2833
|
+
const role = element.getAttribute("role");
|
|
2834
|
+
return role === "textbox" || role === "searchbox" || role === "combobox" || role === "spinbutton";
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Default keyboard adapter. Listens for `keydown` on the document; activation
|
|
2838
|
+
* keys start/drop drags, arrow keys nudge the drag point, Escape cancels.
|
|
2839
|
+
*
|
|
2840
|
+
* @example
|
|
2841
|
+
* ```ts
|
|
2842
|
+
* import { KeyboardAdapter, useDragDrop } from '@vuetify/v0'
|
|
2843
|
+
*
|
|
2844
|
+
* useDragDrop({ adapters: [new KeyboardAdapter({ activate: ['x'], step: 32 })] })
|
|
2845
|
+
* ```
|
|
2846
|
+
*/
|
|
2847
|
+
var KeyboardAdapter = class extends DragDropAdapter {
|
|
2848
|
+
activate;
|
|
2849
|
+
step;
|
|
2850
|
+
constructor(options = {}) {
|
|
2851
|
+
super();
|
|
2852
|
+
this.activate = options.activate ?? [" ", "Enter"];
|
|
2853
|
+
this.step = options.step ?? 16;
|
|
2854
|
+
}
|
|
2855
|
+
setup(context) {
|
|
2856
|
+
const logger = useLogger();
|
|
2857
|
+
if (this.cleanup) {
|
|
2858
|
+
logger.warn("KeyboardAdapter setup called twice; previous registration will be replaced");
|
|
2859
|
+
this.dispose();
|
|
2860
|
+
}
|
|
2861
|
+
if (!IN_BROWSER) return;
|
|
2862
|
+
const onKeydown = (event) => {
|
|
2863
|
+
const focused = document.activeElement;
|
|
2864
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
|
|
2865
|
+
if (isEditable(focused)) return;
|
|
2866
|
+
const ticket = this.locate(focused, context);
|
|
2867
|
+
const isActive = !/* @__PURE__ */ isNull(context.active.value);
|
|
2868
|
+
if (this.activate.includes(event.key)) {
|
|
2869
|
+
if (isActive) {
|
|
2870
|
+
event.preventDefault();
|
|
2871
|
+
context.emit.drop();
|
|
2872
|
+
} else if (ticket) {
|
|
2873
|
+
const el = ticket.el.value;
|
|
2874
|
+
if (!el) {
|
|
2875
|
+
logger.warn("KeyboardAdapter: cannot start drag — element is not mounted");
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
event.preventDefault();
|
|
2879
|
+
const rect = el.getBoundingClientRect();
|
|
2880
|
+
context.emit.start(ticket, {
|
|
2881
|
+
x: rect.left + rect.width / 2,
|
|
2882
|
+
y: rect.top + rect.height / 2
|
|
2883
|
+
}, "keyboard");
|
|
2884
|
+
}
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (event.key === "Escape" && isActive) {
|
|
2888
|
+
event.preventDefault();
|
|
2889
|
+
context.emit.cancel();
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (isActive && event.key.startsWith("Arrow")) {
|
|
2893
|
+
const active = context.active.value;
|
|
2894
|
+
if (/* @__PURE__ */ isNull(active)) return;
|
|
2895
|
+
const current = active.current;
|
|
2896
|
+
let point = null;
|
|
2897
|
+
switch (event.key) {
|
|
2898
|
+
case "ArrowUp":
|
|
2899
|
+
point = {
|
|
2900
|
+
x: current.x,
|
|
2901
|
+
y: current.y - this.step
|
|
2902
|
+
};
|
|
2903
|
+
break;
|
|
2904
|
+
case "ArrowDown":
|
|
2905
|
+
point = {
|
|
2906
|
+
x: current.x,
|
|
2907
|
+
y: current.y + this.step
|
|
2908
|
+
};
|
|
2909
|
+
break;
|
|
2910
|
+
case "ArrowLeft":
|
|
2911
|
+
point = {
|
|
2912
|
+
x: current.x - this.step,
|
|
2913
|
+
y: current.y
|
|
2914
|
+
};
|
|
2915
|
+
break;
|
|
2916
|
+
case "ArrowRight":
|
|
2917
|
+
point = {
|
|
2918
|
+
x: current.x + this.step,
|
|
2919
|
+
y: current.y
|
|
2920
|
+
};
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
if (point) {
|
|
2924
|
+
event.preventDefault();
|
|
2925
|
+
context.emit.move(point);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
const stop = useEventListener(document, "keydown", onKeydown);
|
|
2930
|
+
this.cleanup = () => stop();
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
//#endregion
|
|
2934
|
+
//#region src/composables/useDragDrop/index.ts
|
|
2935
|
+
/**
|
|
2936
|
+
* @module useDragDrop
|
|
2937
|
+
*
|
|
2938
|
+
* @see https://0.vuetifyjs.com/composables/system/use-drag-drop
|
|
2939
|
+
*
|
|
2940
|
+
* @remarks
|
|
2941
|
+
* Headless drag-and-drop primitive. Owns two registries (draggables and zones)
|
|
2942
|
+
* plus the active-drag state. Pointer and keyboard adapters ship by default;
|
|
2943
|
+
* lifecycle hooks and a plugin array provide extension points.
|
|
2944
|
+
*
|
|
2945
|
+
* @example
|
|
2946
|
+
* ```ts
|
|
2947
|
+
* import { useDragDrop } from '@vuetify/v0'
|
|
2948
|
+
*
|
|
2949
|
+
* const dnd = useDragDrop()
|
|
2950
|
+
* ```
|
|
2951
|
+
*/
|
|
2952
|
+
/**
|
|
2953
|
+
* Pure math for resolving where in an oriented zone a pointer would drop.
|
|
2954
|
+
* Internal — consumers rely on the `position.index` / `position.indicator`
|
|
2955
|
+
* fields delivered to `onDrop`.
|
|
2956
|
+
*/
|
|
2957
|
+
function resolveDropPosition(point, rects, orientation) {
|
|
2958
|
+
if (rects.length === 0) return null;
|
|
2959
|
+
const axis = orientation === "vertical" ? "y" : "x";
|
|
2960
|
+
const start = orientation === "vertical" ? "top" : "left";
|
|
2961
|
+
const end = orientation === "vertical" ? "bottom" : "right";
|
|
2962
|
+
const coord = point[axis];
|
|
2963
|
+
if (coord < rects[0][start]) return {
|
|
2964
|
+
index: 0,
|
|
2965
|
+
edge: "before",
|
|
2966
|
+
rect: rects[0]
|
|
2967
|
+
};
|
|
2968
|
+
const last = rects.at(-1);
|
|
2969
|
+
if (coord > last[end]) return {
|
|
2970
|
+
index: rects.length,
|
|
2971
|
+
edge: "after",
|
|
2972
|
+
rect: last
|
|
2973
|
+
};
|
|
2974
|
+
for (const [index, rect] of rects.entries()) if (coord >= rect[start] && coord <= rect[end]) {
|
|
2975
|
+
if (coord < rect[start] + (rect[end] - rect[start]) / 2) return {
|
|
2976
|
+
index,
|
|
2977
|
+
edge: "before",
|
|
2978
|
+
rect
|
|
2979
|
+
};
|
|
2980
|
+
return {
|
|
2981
|
+
index: index + 1,
|
|
2982
|
+
edge: "after",
|
|
2983
|
+
rect
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
for (let index = 0; index < rects.length - 1; index++) {
|
|
2987
|
+
const a = rects[index];
|
|
2988
|
+
const b = rects[index + 1];
|
|
2989
|
+
if (coord > a[end] && coord < b[start]) {
|
|
2990
|
+
if (coord <= (a[end] + b[start]) / 2) return {
|
|
2991
|
+
index: index + 1,
|
|
2992
|
+
edge: "after",
|
|
2993
|
+
rect: a
|
|
2994
|
+
};
|
|
2995
|
+
return {
|
|
2996
|
+
index: index + 1,
|
|
2997
|
+
edge: "before",
|
|
2998
|
+
rect: b
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
index: rects.length,
|
|
3004
|
+
edge: "after",
|
|
3005
|
+
rect: last
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Create a headless drag-and-drop context.
|
|
3010
|
+
*
|
|
3011
|
+
* @param options - Adapters, plugins, and lifecycle hooks. All optional.
|
|
3012
|
+
* @returns A `DragDropContext<Z>` exposing `draggables` / `zones` registries,
|
|
3013
|
+
* the `active` drag, the `isDragging` boolean, and a programmatic `cancel()`.
|
|
3014
|
+
*
|
|
3015
|
+
* @see https://0.vuetifyjs.com/composables/system/use-drag-drop
|
|
3016
|
+
*
|
|
3017
|
+
* @example
|
|
3018
|
+
* ```ts
|
|
3019
|
+
* import { useDragDrop } from '@vuetify/v0'
|
|
3020
|
+
* import { useTemplateRef } from 'vue'
|
|
3021
|
+
*
|
|
3022
|
+
* const dnd = useDragDrop<{ type: 'card', value: string }>()
|
|
3023
|
+
*
|
|
3024
|
+
* const draggable = useTemplateRef<HTMLElement>('draggable')
|
|
3025
|
+
* const dropzone = useTemplateRef<HTMLElement>('dropzone')
|
|
3026
|
+
*
|
|
3027
|
+
* dnd.draggables.register({ el: draggable, type: 'card', value: 'card-1' })
|
|
3028
|
+
* dnd.zones.register({
|
|
3029
|
+
* el: dropzone,
|
|
3030
|
+
* accept: ['card'],
|
|
3031
|
+
* onDrop: (drag, position) => console.log(drag.value, position.index),
|
|
3032
|
+
* })
|
|
3033
|
+
* ```
|
|
3034
|
+
*/
|
|
3035
|
+
function useDragDrop(options = {}) {
|
|
3036
|
+
const { plugins = [] } = options;
|
|
3037
|
+
const logger = useLogger();
|
|
3038
|
+
const _draggables = createRegistry({ events: true });
|
|
3039
|
+
const _zones = createRegistry({ events: true });
|
|
3040
|
+
const active = shallowRef(null);
|
|
3041
|
+
const isDragging = toRef(() => !/* @__PURE__ */ isNull(active.value));
|
|
3042
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
3043
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
3044
|
+
function next(fn, ...args) {
|
|
3045
|
+
if (/* @__PURE__ */ isUndefined(fn)) return;
|
|
3046
|
+
try {
|
|
3047
|
+
fn(...args);
|
|
3048
|
+
} catch (error) {
|
|
3049
|
+
logger.error("useDragDrop hook threw", error);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
function safeAccept(accept, drag) {
|
|
3053
|
+
try {
|
|
3054
|
+
if (/* @__PURE__ */ isNull(drag)) return false;
|
|
3055
|
+
if (/* @__PURE__ */ isUndefined(accept)) return true;
|
|
3056
|
+
if (/* @__PURE__ */ isArray(accept)) return accept.includes(drag.type);
|
|
3057
|
+
const result = accept(drag);
|
|
3058
|
+
if (/* @__PURE__ */ isThenable(result)) {
|
|
3059
|
+
logger.warn("useDragDrop accept predicate returned a thenable; async predicates are not supported — treating as reject");
|
|
3060
|
+
return false;
|
|
3061
|
+
}
|
|
3062
|
+
return Boolean(result);
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
logger.error("useDragDrop accept predicate threw; treating as reject", error);
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
const draggables = {
|
|
3069
|
+
..._draggables,
|
|
3070
|
+
register(registration) {
|
|
3071
|
+
const id = registration.id ?? /* @__PURE__ */ useId$1();
|
|
3072
|
+
const el = toRef(() => toValue(registration.el));
|
|
3073
|
+
const dragging = toRef(() => active.value?.id === id);
|
|
3074
|
+
const input = {
|
|
3075
|
+
...registration,
|
|
3076
|
+
id,
|
|
3077
|
+
el,
|
|
3078
|
+
isDragging: dragging
|
|
3079
|
+
};
|
|
3080
|
+
return _draggables.register(input);
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
const zones = {
|
|
3084
|
+
..._zones,
|
|
3085
|
+
register(registration) {
|
|
3086
|
+
const id = registration.id ?? /* @__PURE__ */ useId$1();
|
|
3087
|
+
if (_zones.has(id)) return _zones.register(registration);
|
|
3088
|
+
const el = toRef(() => toValue(registration.el));
|
|
3089
|
+
const isOver = toRef(() => active.value?.over === id);
|
|
3090
|
+
const willAccept = toRef(() => safeAccept(registration.accept, active.value));
|
|
3091
|
+
const rects = shallowRef([]);
|
|
3092
|
+
function refresh() {
|
|
3093
|
+
const zoneEl = el.value;
|
|
3094
|
+
rects.value = zoneEl ? Array.from(zoneEl.children).map((child) => child.getBoundingClientRect()) : [];
|
|
3095
|
+
}
|
|
3096
|
+
const scope = effectScope();
|
|
3097
|
+
scopes.set(id, scope);
|
|
3098
|
+
scope.run(() => {
|
|
3099
|
+
if (registration.orientation) {
|
|
3100
|
+
watch(el, refresh, {
|
|
3101
|
+
immediate: true,
|
|
3102
|
+
flush: "post"
|
|
3103
|
+
});
|
|
3104
|
+
useResizeObserver(el, refresh);
|
|
3105
|
+
useMutationObserver(el, refresh, { childList: true });
|
|
3106
|
+
watch(() => active.value?.id, (id) => {
|
|
3107
|
+
if (id) refresh();
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
watch(el, (current) => {
|
|
3111
|
+
if (/* @__PURE__ */ isNull(current)) return;
|
|
3112
|
+
nodes.set(current, id);
|
|
3113
|
+
onWatcherCleanup(() => nodes.delete(current));
|
|
3114
|
+
}, { immediate: true });
|
|
3115
|
+
});
|
|
3116
|
+
const indicator = computed(() => {
|
|
3117
|
+
if (!registration.orientation || !isOver.value || !active.value) return null;
|
|
3118
|
+
return resolveDropPosition(active.value.current, rects.value, registration.orientation);
|
|
3119
|
+
});
|
|
3120
|
+
const input = {
|
|
3121
|
+
...registration,
|
|
3122
|
+
id,
|
|
3123
|
+
el,
|
|
3124
|
+
isOver,
|
|
3125
|
+
willAccept,
|
|
3126
|
+
indicator
|
|
3127
|
+
};
|
|
3128
|
+
return _zones.register(input);
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
function at(point) {
|
|
3132
|
+
if (!IN_BROWSER) return null;
|
|
3133
|
+
let element = document.elementFromPoint(point.x, point.y);
|
|
3134
|
+
while (element) {
|
|
3135
|
+
const id = nodes.get(element);
|
|
3136
|
+
if (!/* @__PURE__ */ isUndefined(id)) {
|
|
3137
|
+
const zone = _zones.get(id);
|
|
3138
|
+
if (zone && !toValue(zone.disabled)) return id;
|
|
3139
|
+
}
|
|
3140
|
+
element = element.parentElement;
|
|
3141
|
+
}
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
function position(zoneId, drag) {
|
|
3145
|
+
const zone = _zones.get(zoneId);
|
|
3146
|
+
const out = { pointer: drag.current };
|
|
3147
|
+
if (zone?.orientation) {
|
|
3148
|
+
const resolved = zone.indicator?.value ?? null;
|
|
3149
|
+
if (resolved) {
|
|
3150
|
+
out.index = resolved.index;
|
|
3151
|
+
out.indicator = resolved;
|
|
3152
|
+
} else out.index = 0;
|
|
3153
|
+
}
|
|
3154
|
+
return out;
|
|
3155
|
+
}
|
|
3156
|
+
function bail(drag, reason = "cancel") {
|
|
3157
|
+
active.value = null;
|
|
3158
|
+
if (!/* @__PURE__ */ isNull(drag.over)) next(_zones.get(drag.over)?.onLeave, drag);
|
|
3159
|
+
next(_draggables.get(drag.id)?.onCancel, drag, reason);
|
|
3160
|
+
next(options.onCancel, drag, reason);
|
|
3161
|
+
}
|
|
3162
|
+
function onStart(source, origin, via) {
|
|
3163
|
+
if (toValue(_draggables.get(source.id)?.disabled)) return;
|
|
3164
|
+
const draft = {
|
|
3165
|
+
id: source.id,
|
|
3166
|
+
type: source.type,
|
|
3167
|
+
value: source.value,
|
|
3168
|
+
origin,
|
|
3169
|
+
current: origin,
|
|
3170
|
+
delta: {
|
|
3171
|
+
x: 0,
|
|
3172
|
+
y: 0
|
|
3173
|
+
},
|
|
3174
|
+
over: null,
|
|
3175
|
+
willAccept: false,
|
|
3176
|
+
via
|
|
3177
|
+
};
|
|
3178
|
+
try {
|
|
3179
|
+
if (_draggables.get(source.id)?.onBeforeStart?.(draft) === false) return;
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
logger.error("useDragDrop onBeforeStart threw; treating as veto", error);
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
try {
|
|
3185
|
+
if (options.onBeforeStart?.(draft) === false) return;
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
logger.error("useDragDrop onBeforeStart threw; treating as veto", error);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
active.value = draft;
|
|
3191
|
+
}
|
|
3192
|
+
function onMove(point) {
|
|
3193
|
+
if (!active.value) return;
|
|
3194
|
+
const over = at(point);
|
|
3195
|
+
const draft = {
|
|
3196
|
+
...active.value,
|
|
3197
|
+
current: point,
|
|
3198
|
+
delta: {
|
|
3199
|
+
x: point.x - active.value.origin.x,
|
|
3200
|
+
y: point.y - active.value.origin.y
|
|
3201
|
+
},
|
|
3202
|
+
over,
|
|
3203
|
+
willAccept: false
|
|
3204
|
+
};
|
|
3205
|
+
draft.willAccept = !/* @__PURE__ */ isNull(over) && safeAccept(_zones.get(over)?.accept, draft);
|
|
3206
|
+
const previous = active.value.over;
|
|
3207
|
+
active.value = draft;
|
|
3208
|
+
if (previous !== over) {
|
|
3209
|
+
if (!/* @__PURE__ */ isNull(previous)) next(_zones.get(previous)?.onLeave, draft);
|
|
3210
|
+
if (!/* @__PURE__ */ isNull(over)) next(_zones.get(over)?.onEnter, draft);
|
|
3211
|
+
}
|
|
3212
|
+
next(_draggables.get(draft.id)?.onMove, draft);
|
|
3213
|
+
next(options.onMove, draft);
|
|
3214
|
+
}
|
|
3215
|
+
function onDrop() {
|
|
3216
|
+
if (!active.value) return;
|
|
3217
|
+
const drag = active.value;
|
|
3218
|
+
const zoneId = drag.over;
|
|
3219
|
+
if (/* @__PURE__ */ isNull(zoneId)) {
|
|
3220
|
+
bail(drag);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
const dropAt = position(zoneId, drag);
|
|
3224
|
+
const zone = _zones.get(zoneId);
|
|
3225
|
+
function vetoed(fn, label) {
|
|
3226
|
+
if (!fn) return false;
|
|
3227
|
+
try {
|
|
3228
|
+
return fn(drag, dropAt) === false;
|
|
3229
|
+
} catch (error) {
|
|
3230
|
+
logger.error(`useDragDrop ${label} threw; treating as veto`, error);
|
|
3231
|
+
return true;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
if (vetoed(zone?.onBeforeDrop, "zone onBeforeDrop") || vetoed(options.onBeforeDrop, "options onBeforeDrop")) {
|
|
3235
|
+
bail(drag, "reject");
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
active.value = null;
|
|
3239
|
+
next(zone?.onDrop, drag, dropAt);
|
|
3240
|
+
next(options.onDrop, drag, dropAt);
|
|
3241
|
+
}
|
|
3242
|
+
function cancel() {
|
|
3243
|
+
if (!active.value) return;
|
|
3244
|
+
bail(active.value);
|
|
3245
|
+
}
|
|
3246
|
+
const context = {
|
|
3247
|
+
draggables,
|
|
3248
|
+
zones,
|
|
3249
|
+
active: shallowReadonly(active),
|
|
3250
|
+
isDragging,
|
|
3251
|
+
cancel
|
|
3252
|
+
};
|
|
3253
|
+
const disposers = [];
|
|
3254
|
+
const adapters = options.adapters ?? [new PointerAdapter(), new KeyboardAdapter()];
|
|
3255
|
+
const emit = {
|
|
3256
|
+
start: (source, origin, via) => onStart(source, origin, via),
|
|
3257
|
+
move: (point) => onMove(point),
|
|
3258
|
+
drop: () => onDrop(),
|
|
3259
|
+
cancel: () => cancel()
|
|
3260
|
+
};
|
|
3261
|
+
const adapterContext = {
|
|
3262
|
+
...context,
|
|
3263
|
+
emit
|
|
3264
|
+
};
|
|
3265
|
+
for (const adapter of adapters) try {
|
|
3266
|
+
adapter.setup(adapterContext);
|
|
3267
|
+
disposers.push(() => adapter.dispose());
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
logger.error("useDragDrop adapter setup threw; skipping adapter", error);
|
|
3270
|
+
try {
|
|
3271
|
+
adapter.dispose();
|
|
3272
|
+
} catch (disposeError) {
|
|
3273
|
+
logger.error("useDragDrop adapter dispose threw after setup failure", disposeError);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
for (const plugin of plugins) try {
|
|
3277
|
+
const dispose = plugin(context);
|
|
3278
|
+
if (/* @__PURE__ */ isFunction(dispose)) disposers.push(dispose);
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
logger.error("useDragDrop plugin install threw; skipping plugin", error);
|
|
3281
|
+
}
|
|
3282
|
+
_draggables.on("unregister:ticket", (ticket) => {
|
|
3283
|
+
if (active.value?.id === ticket.id) cancel();
|
|
3284
|
+
});
|
|
3285
|
+
_zones.on("unregister:ticket", (ticket) => {
|
|
3286
|
+
const scope = scopes.get(ticket.id);
|
|
3287
|
+
if (scope) {
|
|
3288
|
+
scope.stop();
|
|
3289
|
+
scopes.delete(ticket.id);
|
|
3290
|
+
}
|
|
3291
|
+
if (active.value?.over === ticket.id) {
|
|
3292
|
+
next(ticket.onLeave, active.value);
|
|
3293
|
+
active.value = {
|
|
3294
|
+
...active.value,
|
|
3295
|
+
over: null,
|
|
3296
|
+
willAccept: false
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
onScopeDispose(() => {
|
|
3301
|
+
if (!/* @__PURE__ */ isNull(active.value)) cancel();
|
|
3302
|
+
for (const dispose of disposers.toReversed()) try {
|
|
3303
|
+
dispose();
|
|
3304
|
+
} catch (error) {
|
|
3305
|
+
logger.error("useDragDrop disposer threw", error);
|
|
3306
|
+
}
|
|
3307
|
+
for (const scope of scopes.values()) try {
|
|
3308
|
+
scope.stop();
|
|
3309
|
+
} catch (error) {
|
|
3310
|
+
logger.error("useDragDrop zone scope dispose threw", error);
|
|
3311
|
+
}
|
|
3312
|
+
scopes.clear();
|
|
3313
|
+
nodes.clear();
|
|
3314
|
+
try {
|
|
3315
|
+
_draggables.dispose();
|
|
3316
|
+
} catch (error) {
|
|
3317
|
+
logger.error("useDragDrop draggables dispose threw", error);
|
|
3318
|
+
}
|
|
3319
|
+
try {
|
|
3320
|
+
_zones.dispose();
|
|
3321
|
+
} catch (error) {
|
|
3322
|
+
logger.error("useDragDrop zones dispose threw", error);
|
|
3323
|
+
}
|
|
3324
|
+
}, true);
|
|
3325
|
+
return context;
|
|
3326
|
+
}
|
|
3327
|
+
//#endregion
|
|
3328
|
+
//#region src/composables/useHotkey/aliases.ts
|
|
3329
|
+
/**
|
|
3330
|
+
* Key alias mapping for consistent key normalization across the hotkey system.
|
|
3331
|
+
*
|
|
3332
|
+
* Maps user-friendly aliases to canonical key names that match
|
|
3333
|
+
* KeyboardEvent.key values (in lowercase) where possible.
|
|
3334
|
+
*/
|
|
3335
|
+
const keyAliasMap = {
|
|
3336
|
+
control: "ctrl",
|
|
3337
|
+
command: "cmd",
|
|
3338
|
+
option: "alt",
|
|
3339
|
+
up: "arrowup",
|
|
3340
|
+
down: "arrowdown",
|
|
3341
|
+
left: "arrowleft",
|
|
3342
|
+
right: "arrowright",
|
|
3343
|
+
esc: "escape",
|
|
3344
|
+
spacebar: " ",
|
|
3345
|
+
space: " ",
|
|
3346
|
+
return: "enter",
|
|
3347
|
+
del: "delete",
|
|
3348
|
+
plus: "+",
|
|
3349
|
+
slash: "/",
|
|
3350
|
+
underscore: "_",
|
|
3351
|
+
minus: "-",
|
|
3352
|
+
hyphen: "-"
|
|
3353
|
+
};
|
|
3354
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
3355
|
+
function normalizeKey(key) {
|
|
3356
|
+
const lowerKey = key.toLowerCase();
|
|
3357
|
+
return keyAliasMap[lowerKey] ?? lowerKey;
|
|
3358
|
+
}
|
|
3359
|
+
//#endregion
|
|
3360
|
+
//#region src/composables/useHotkey/parsing.ts
|
|
3361
|
+
const logger = /* @__PURE__ */ useLogger();
|
|
3362
|
+
const MODIFIERS = [
|
|
3363
|
+
"ctrl",
|
|
3364
|
+
"shift",
|
|
3365
|
+
"alt",
|
|
3366
|
+
"meta",
|
|
3367
|
+
"cmd"
|
|
3368
|
+
];
|
|
3369
|
+
/**
|
|
3370
|
+
* Splits a single combination string into individual key parts.
|
|
3371
|
+
*
|
|
3372
|
+
* A combination is a set of keys that must be pressed simultaneously.
|
|
3373
|
+
* e.g. `ctrl+k`, `shift+-`
|
|
3374
|
+
*
|
|
3375
|
+
* @param combination - The combination string to parse
|
|
3376
|
+
* @param isInternal - Whether this is an internal call (suppresses warnings)
|
|
3377
|
+
* @returns Object with keys array and separators array
|
|
3378
|
+
*/
|
|
3379
|
+
function splitKeyCombination(combination, isInternal = false) {
|
|
3380
|
+
const emptyResult = {
|
|
3381
|
+
keys: [],
|
|
3382
|
+
separators: []
|
|
3383
|
+
};
|
|
3384
|
+
if (!combination) {
|
|
3385
|
+
if (!isInternal) logger.warn("Invalid hotkey combination: empty string provided");
|
|
3386
|
+
return emptyResult;
|
|
3387
|
+
}
|
|
3388
|
+
if (combination.length > 1 && [
|
|
3389
|
+
"+",
|
|
3390
|
+
"/",
|
|
3391
|
+
"_"
|
|
3392
|
+
].some((v) => combination.startsWith(v)) && ![
|
|
3393
|
+
"++",
|
|
3394
|
+
"//",
|
|
3395
|
+
"__"
|
|
3396
|
+
].some((v) => combination.startsWith(v)) || combination.includes("++") || combination.includes("//") || combination.includes("__") || combination === "+" || combination === "/" || combination === "_" || combination.length > 1 && (combination.endsWith("+") || combination.endsWith("/") || combination.endsWith("_")) && combination.at(-2) !== combination.at(-1) || combination === "++" || combination === "--" || combination === "__") {
|
|
3397
|
+
if (!isInternal) logger.warn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
|
3398
|
+
return emptyResult;
|
|
3399
|
+
}
|
|
3400
|
+
const keys = [];
|
|
3401
|
+
const separators = [];
|
|
3402
|
+
let buffer = "";
|
|
3403
|
+
function flushBuffer(separator) {
|
|
3404
|
+
if (buffer) {
|
|
3405
|
+
if (separator) separators.push(separator);
|
|
3406
|
+
keys.push(/* @__PURE__ */ normalizeKey(buffer));
|
|
3407
|
+
buffer = "";
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
for (let i = 0; i < combination.length; i++) {
|
|
3411
|
+
const char = combination[i];
|
|
3412
|
+
const nextChar = combination[i + 1];
|
|
3413
|
+
if ([
|
|
3414
|
+
"+",
|
|
3415
|
+
"/",
|
|
3416
|
+
"_",
|
|
3417
|
+
"-"
|
|
3418
|
+
].includes(char))
|
|
3419
|
+
/* v8 ignore start -- edge case: doubled special chars */
|
|
3420
|
+
if (char === nextChar) {
|
|
3421
|
+
flushBuffer(char);
|
|
3422
|
+
keys.push(char);
|
|
3423
|
+
i++;
|
|
3424
|
+
} else if ([
|
|
3425
|
+
"+",
|
|
3426
|
+
"/",
|
|
3427
|
+
"_"
|
|
3428
|
+
].includes(char)) flushBuffer(char);
|
|
3429
|
+
else buffer += char;
|
|
3430
|
+
else buffer += char;
|
|
3431
|
+
}
|
|
3432
|
+
flushBuffer();
|
|
3433
|
+
/* v8 ignore start -- edge case: invalid minus in combination */
|
|
3434
|
+
if (keys.some((key) => key.length > 1 && key.includes("-") && key !== "--")) {
|
|
3435
|
+
if (!isInternal) logger.warn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
|
3436
|
+
return emptyResult;
|
|
3437
|
+
}
|
|
3438
|
+
if (keys.length === 0 && combination) return {
|
|
3439
|
+
keys: [/* @__PURE__ */ normalizeKey(combination)],
|
|
3440
|
+
separators
|
|
3441
|
+
};
|
|
3442
|
+
/* v8 ignore stop */
|
|
3443
|
+
return {
|
|
3444
|
+
keys,
|
|
3445
|
+
separators
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Splits a hotkey string into its constituent combination groups.
|
|
3450
|
+
*
|
|
3451
|
+
* A sequence is a series of combinations that must be pressed in order.
|
|
3452
|
+
* e.g. `a-b`, `ctrl+k-p`
|
|
3453
|
+
*
|
|
3454
|
+
* @param str - The sequence string to parse
|
|
3455
|
+
* @returns Array of combination strings
|
|
3456
|
+
*/
|
|
3457
|
+
function splitKeySequence(str) {
|
|
3458
|
+
if (!str) {
|
|
3459
|
+
logger.warn("Invalid hotkey sequence: empty string provided");
|
|
3460
|
+
return [];
|
|
3461
|
+
}
|
|
3462
|
+
const hasInvalidStart = str.startsWith("-") && ![
|
|
3463
|
+
"-",
|
|
3464
|
+
"---",
|
|
3465
|
+
"--+"
|
|
3466
|
+
].includes(str);
|
|
3467
|
+
const hasInvalidEnd = str.endsWith("-") && !str.endsWith("+-") && !str.endsWith("_-") && str !== "-" && str !== "---";
|
|
3468
|
+
if (hasInvalidStart || hasInvalidEnd) {
|
|
3469
|
+
logger.warn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
|
3470
|
+
return [];
|
|
3471
|
+
}
|
|
3472
|
+
const result = [];
|
|
3473
|
+
let buffer = "";
|
|
3474
|
+
let i = 0;
|
|
3475
|
+
while (i < str.length) {
|
|
3476
|
+
const char = str[i];
|
|
3477
|
+
if (char === "-") {
|
|
3478
|
+
const prevChar = str[i - 1] ?? "";
|
|
3479
|
+
const prevPrevChar = i > 1 ? str[i - 2] : void 0;
|
|
3480
|
+
if (["+", "_"].includes(prevChar) && !["+", "/"].includes(prevPrevChar ?? "")) {
|
|
3481
|
+
buffer += char;
|
|
3482
|
+
i++;
|
|
3483
|
+
} else {
|
|
3484
|
+
if (buffer) {
|
|
3485
|
+
result.push(buffer);
|
|
3486
|
+
buffer = "";
|
|
3487
|
+
} else result.push("-");
|
|
3488
|
+
i++;
|
|
3489
|
+
}
|
|
3490
|
+
} else {
|
|
3491
|
+
buffer += char;
|
|
3492
|
+
i++;
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
if (buffer) result.push(buffer);
|
|
3496
|
+
const collapsed = [];
|
|
3497
|
+
let minusCount = 0;
|
|
3498
|
+
for (const part of result)
|
|
3499
|
+
/* v8 ignore start -- edge case: minus collapsing */
|
|
3500
|
+
if (part === "-") {
|
|
3501
|
+
if (minusCount % 2 === 0) collapsed.push("-");
|
|
3502
|
+
minusCount++;
|
|
3503
|
+
} else {
|
|
3504
|
+
minusCount = 0;
|
|
3505
|
+
collapsed.push(part);
|
|
3506
|
+
}
|
|
3507
|
+
/* v8 ignore start -- edge case: invalid combination in sequence */
|
|
3508
|
+
if (!collapsed.every((s) => splitKeyCombination(s, true).keys.length > 0)) {
|
|
3509
|
+
logger.warn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
|
3510
|
+
return [];
|
|
3511
|
+
}
|
|
3512
|
+
/* v8 ignore stop */
|
|
3513
|
+
return collapsed;
|
|
3514
|
+
}
|
|
3515
|
+
//#endregion
|
|
3516
|
+
//#region src/composables/useHotkey/index.ts
|
|
3517
|
+
/**
|
|
3518
|
+
* @module useHotkey
|
|
3519
|
+
*
|
|
3520
|
+
* @see https://0.vuetifyjs.com/composables/system/use-hotkey
|
|
3521
|
+
*
|
|
3522
|
+
* @remarks
|
|
3523
|
+
* Hotkey listener composable with key combination and sequence support.
|
|
3524
|
+
*
|
|
3525
|
+
* Key features:
|
|
3526
|
+
* - Key combination support (`ctrl+k`, `shift+enter`)
|
|
3527
|
+
* - Key sequence support (`g-h` for GitHub-style shortcuts)
|
|
3528
|
+
* - Platform-aware modifiers (Mac: cmd→meta, others: cmd→ctrl)
|
|
3529
|
+
* - Input focus detection (skip when typing in inputs)
|
|
3530
|
+
* - Sequence timeout with automatic reset
|
|
3531
|
+
* - Key and symbol word alias normalization (`plus` → `+`, `esc` → `escape`, etc.)
|
|
3532
|
+
* - Pause/resume/stop functionality
|
|
3533
|
+
* - Automatic cleanup on scope disposal
|
|
3534
|
+
* - SSR-safe (no-op when not in browser)
|
|
3535
|
+
*
|
|
3536
|
+
* Builds on useEventListener for lifecycle-managed keyboard events.
|
|
3537
|
+
*
|
|
3538
|
+
* @example
|
|
3539
|
+
* ```ts
|
|
3540
|
+
* import { useHotkey } from '@vuetify/v0'
|
|
3541
|
+
*
|
|
3542
|
+
* useHotkey('ctrl+k', () => console.log('opened palette'))
|
|
3543
|
+
* useHotkey('g-h', () => console.log('go home'))
|
|
3544
|
+
* ```
|
|
3545
|
+
*/
|
|
3546
|
+
const isMac = IN_BROWSER && /mac|iphone|ipad|ipod/i.test(navigator?.userAgent ?? "");
|
|
3547
|
+
/**
|
|
3548
|
+
* A composable that listens for hotkey combinations and sequences.
|
|
3549
|
+
*
|
|
3550
|
+
* @param keys - The hotkey string (e.g., 'ctrl+k', 'g-h')
|
|
3551
|
+
* @param callback - The function to call when the hotkey is triggered
|
|
3552
|
+
* @param options - Configuration options
|
|
3553
|
+
* @returns An object with state refs and control methods
|
|
3554
|
+
*
|
|
3555
|
+
* @see https://0.vuetifyjs.com/composables/system/use-hotkey
|
|
3556
|
+
*
|
|
3557
|
+
* @example
|
|
3558
|
+
* ```ts
|
|
3559
|
+
* import { useHotkey } from '@vuetify/v0'
|
|
3560
|
+
*
|
|
3561
|
+
* // Simple combination
|
|
3562
|
+
* const { isActive, pause, resume } = useHotkey('ctrl+k', () => {
|
|
3563
|
+
* console.log('Command palette opened')
|
|
3564
|
+
* })
|
|
3565
|
+
*
|
|
3566
|
+
* // Key sequence (GitHub-style)
|
|
3567
|
+
* useHotkey('g-h', () => console.log('Go home'))
|
|
3568
|
+
*
|
|
3569
|
+
* // With options
|
|
3570
|
+
* useHotkey('escape', closeModal, { inputs: true })
|
|
3571
|
+
*
|
|
3572
|
+
* // Pause/resume control
|
|
3573
|
+
* pause() // Temporarily disable
|
|
3574
|
+
* resume() // Re-enable
|
|
3575
|
+
* ```
|
|
3576
|
+
*/
|
|
3577
|
+
function useHotkey(keys, callback, options = {}, _platform) {
|
|
3578
|
+
const platformIsMac = _platform?.isMac ?? isMac;
|
|
3579
|
+
const { event = "keydown", inputs = false, preventDefault = true, stopPropagation = false, sequenceTimeout = 1e3 } = options;
|
|
3580
|
+
const isPaused = shallowRef(false);
|
|
3581
|
+
const cleanup = shallowRef(null);
|
|
3582
|
+
const isActive = toRef(() => !/* @__PURE__ */ isNull(cleanup.value));
|
|
3583
|
+
let sequenceTimer;
|
|
3584
|
+
let keyGroups = [];
|
|
3585
|
+
let isSequence = false;
|
|
3586
|
+
let groupIndex = 0;
|
|
3587
|
+
function isInputFocused() {
|
|
3588
|
+
/* v8 ignore next -- IN_BROWSER always true in happy-dom test env */
|
|
3589
|
+
if (!IN_BROWSER) return false;
|
|
3590
|
+
if (toValue(inputs)) return false;
|
|
3591
|
+
const activeElement = document.activeElement;
|
|
3592
|
+
/* v8 ignore next -- defensive: document.activeElement is always set in happy-dom */
|
|
3593
|
+
if (!activeElement) return false;
|
|
3594
|
+
const role = activeElement.getAttribute("role");
|
|
3595
|
+
if (role === "textbox" || role === "searchbox" || role === "combobox" || role === "spinbutton") return true;
|
|
3596
|
+
if (activeElement.tagName === "INPUT") {
|
|
3597
|
+
const type = activeElement.type?.toLowerCase();
|
|
3598
|
+
return [
|
|
3599
|
+
"text",
|
|
3600
|
+
"email",
|
|
3601
|
+
"password",
|
|
3602
|
+
"search",
|
|
3603
|
+
"tel",
|
|
3604
|
+
"url",
|
|
3605
|
+
"number",
|
|
3606
|
+
"date",
|
|
3607
|
+
"datetime-local",
|
|
3608
|
+
"time",
|
|
3609
|
+
"month",
|
|
3610
|
+
"week"
|
|
3611
|
+
].includes(type);
|
|
3612
|
+
}
|
|
3613
|
+
return activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable;
|
|
3614
|
+
}
|
|
3615
|
+
function resetSequence() {
|
|
3616
|
+
groupIndex = 0;
|
|
3617
|
+
clearTimeout(sequenceTimer);
|
|
3618
|
+
}
|
|
3619
|
+
function handler(e) {
|
|
3620
|
+
const group = keyGroups[groupIndex];
|
|
3621
|
+
if (!group || isInputFocused()) return;
|
|
3622
|
+
if (!matchesKeyGroup(e, group, platformIsMac)) {
|
|
3623
|
+
if (isSequence) resetSequence();
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
if (toValue(stopPropagation)) e.stopPropagation();
|
|
3627
|
+
if (toValue(preventDefault)) e.preventDefault();
|
|
3628
|
+
if (!isSequence) {
|
|
3629
|
+
callback(e);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
clearTimeout(sequenceTimer);
|
|
3633
|
+
groupIndex++;
|
|
3634
|
+
if (groupIndex === keyGroups.length) {
|
|
3635
|
+
callback(e);
|
|
3636
|
+
resetSequence();
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
sequenceTimer = setTimeout(resetSequence, toValue(sequenceTimeout));
|
|
3640
|
+
}
|
|
3641
|
+
function setup() {
|
|
3642
|
+
if (!IN_BROWSER || isPaused.value) return;
|
|
3643
|
+
const currentKeys = toValue(keys);
|
|
3644
|
+
if (!currentKeys) return;
|
|
3645
|
+
const groups = splitKeySequence(currentKeys.toLowerCase());
|
|
3646
|
+
/* v8 ignore next -- defensive: splitKeySequence rejects empty strings earlier */
|
|
3647
|
+
if (groups.length === 0) return;
|
|
3648
|
+
isSequence = groups.length > 1;
|
|
3649
|
+
keyGroups = groups;
|
|
3650
|
+
resetSequence();
|
|
3651
|
+
cleanup.value = useWindowEventListener(toValue(event), handler);
|
|
3652
|
+
}
|
|
3653
|
+
function dispose() {
|
|
3654
|
+
if (cleanup.value) {
|
|
3655
|
+
cleanup.value();
|
|
3656
|
+
cleanup.value = null;
|
|
3657
|
+
}
|
|
3658
|
+
clearTimeout(sequenceTimer);
|
|
3659
|
+
keyGroups = [];
|
|
3660
|
+
isSequence = false;
|
|
3661
|
+
groupIndex = 0;
|
|
3662
|
+
}
|
|
3663
|
+
function pause() {
|
|
3664
|
+
isPaused.value = true;
|
|
3665
|
+
dispose();
|
|
3666
|
+
}
|
|
3667
|
+
function resume() {
|
|
3668
|
+
isPaused.value = false;
|
|
3669
|
+
setup();
|
|
3670
|
+
}
|
|
3671
|
+
function stop() {
|
|
3672
|
+
dispose();
|
|
3673
|
+
}
|
|
3674
|
+
watch(() => toValue(keys), () => {
|
|
3675
|
+
if (isPaused.value) return;
|
|
3676
|
+
dispose();
|
|
3677
|
+
setup();
|
|
3678
|
+
}, { immediate: true });
|
|
3679
|
+
watch(() => toValue(event), () => {
|
|
3680
|
+
if (isPaused.value || !toValue(keys)) return;
|
|
3681
|
+
dispose();
|
|
3682
|
+
setup();
|
|
3683
|
+
});
|
|
3684
|
+
onScopeDispose(stop, true);
|
|
3685
|
+
return {
|
|
3686
|
+
isActive: shallowReadonly(isActive),
|
|
3687
|
+
isPaused: shallowReadonly(isPaused),
|
|
3688
|
+
pause,
|
|
3689
|
+
resume,
|
|
3690
|
+
stop
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
function isModifier(key) {
|
|
3694
|
+
return MODIFIERS.includes(key);
|
|
3695
|
+
}
|
|
3696
|
+
function parseKeyGroup(group) {
|
|
3697
|
+
const { keys: parts } = splitKeyCombination(group.toLowerCase());
|
|
3698
|
+
const modifiers = Object.fromEntries(MODIFIERS.map((m) => [m, false]));
|
|
3699
|
+
/* v8 ignore next 3 -- defensive: parseKeyGroup is not invoked with empty parts */
|
|
3700
|
+
if (parts.length === 0) return {
|
|
3701
|
+
modifiers,
|
|
3702
|
+
actualKey: void 0
|
|
3703
|
+
};
|
|
3704
|
+
let actualKey;
|
|
3705
|
+
for (const part of parts) if (isModifier(part)) modifiers[part] = true;
|
|
3706
|
+
else actualKey = part;
|
|
3707
|
+
return {
|
|
3708
|
+
modifiers,
|
|
3709
|
+
actualKey
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function matchesKeyGroup(e, group, platformIsMac) {
|
|
3713
|
+
const { modifiers, actualKey } = parseKeyGroup(group);
|
|
3714
|
+
const expectCtrl = modifiers.ctrl || !platformIsMac && (modifiers.cmd || modifiers.meta);
|
|
3715
|
+
const expectMeta = platformIsMac && (modifiers.cmd || modifiers.meta);
|
|
3716
|
+
const modifiersMatch = e.ctrlKey === expectCtrl && e.metaKey === expectMeta && e.shiftKey === modifiers.shift && e.altKey === modifiers.alt;
|
|
3717
|
+
if (/* @__PURE__ */ isUndefined(actualKey)) return modifiersMatch;
|
|
3718
|
+
return modifiersMatch && e.key.toLowerCase() === actualKey.toLowerCase();
|
|
3719
|
+
}
|
|
3720
|
+
//#endregion
|
|
3721
|
+
//#region src/composables/useMediaQuery/index.ts
|
|
3722
|
+
/**
|
|
3723
|
+
* @module useMediaQuery
|
|
3724
|
+
*
|
|
3725
|
+
* @see https://0.vuetifyjs.com/composables/system/use-media-query
|
|
3726
|
+
*
|
|
3727
|
+
* @remarks
|
|
3728
|
+
* Reactive media query composable with automatic cleanup.
|
|
3729
|
+
*
|
|
3730
|
+
* Key features:
|
|
3731
|
+
* - Synchronous matchMedia read on client mount (prevents FOUC)
|
|
3732
|
+
* - Window matchMedia integration
|
|
3733
|
+
* - Reactive query string support
|
|
3734
|
+
* - SSR-safe (returns false on server)
|
|
3735
|
+
* - Hydration-aware (defers client updates until hydrated)
|
|
3736
|
+
* - Automatic listener cleanup on scope disposal
|
|
3737
|
+
* - Supports both static and dynamic queries
|
|
3738
|
+
*
|
|
3739
|
+
* Perfect for responsive conditionals beyond breakpoint detection.
|
|
3740
|
+
*
|
|
3741
|
+
* @example
|
|
3742
|
+
* ```ts
|
|
3743
|
+
* import { useMediaQuery } from '@vuetify/v0'
|
|
3744
|
+
*
|
|
3745
|
+
* const { matches } = useMediaQuery('(prefers-color-scheme: dark)')
|
|
3746
|
+
* console.log(matches.value) // true/false
|
|
3747
|
+
* ```
|
|
3748
|
+
*/
|
|
3749
|
+
/**
|
|
3750
|
+
* Reactive media query matching.
|
|
3751
|
+
*
|
|
3752
|
+
* @param query CSS media query string (reactive).
|
|
3753
|
+
* @returns The media query context.
|
|
3754
|
+
*
|
|
3755
|
+
* @see https://0.vuetifyjs.com/composables/system/use-media-query
|
|
3756
|
+
*
|
|
3757
|
+
* @example
|
|
3758
|
+
* ```ts
|
|
3759
|
+
* // Static query
|
|
3760
|
+
* const { matches } = useMediaQuery('(prefers-color-scheme: dark)')
|
|
3761
|
+
*
|
|
3762
|
+
* // Dynamic query
|
|
3763
|
+
* const minWidth = ref(768)
|
|
3764
|
+
* const { matches } = useMediaQuery(() => `(min-width: ${minWidth.value}px)`)
|
|
3765
|
+
*
|
|
3766
|
+
* // Manual cleanup
|
|
3767
|
+
* const { matches, stop } = useMediaQuery('(hover: hover)')
|
|
3768
|
+
* stop() // Remove listener early
|
|
3769
|
+
* ```
|
|
3770
|
+
*/
|
|
3771
|
+
function useMediaQuery(query) {
|
|
3772
|
+
const resolvedQuery = toRef(() => toValue(query));
|
|
3773
|
+
const initialQuery = toValue(query);
|
|
3774
|
+
const initialMql = IN_BROWSER && SUPPORTS_MATCH_MEDIA ? window.matchMedia(initialQuery) : null;
|
|
3775
|
+
const matches = shallowRef(initialMql?.matches ?? false);
|
|
3776
|
+
const mediaQueryList = shallowRef(initialMql);
|
|
3777
|
+
const hydration = useHydration();
|
|
3778
|
+
let cleanup = null;
|
|
3779
|
+
function listen() {
|
|
3780
|
+
if (!IN_BROWSER || !SUPPORTS_MATCH_MEDIA) return;
|
|
3781
|
+
cleanup?.();
|
|
3782
|
+
cleanup = null;
|
|
3783
|
+
const mql = window.matchMedia(resolvedQuery.value);
|
|
3784
|
+
mediaQueryList.value = mql;
|
|
3785
|
+
matches.value = mql.matches;
|
|
3786
|
+
function handler(e) {
|
|
3787
|
+
matches.value = e.matches;
|
|
3788
|
+
}
|
|
3789
|
+
mql.addEventListener("change", handler);
|
|
3790
|
+
cleanup = () => mql.removeEventListener("change", handler);
|
|
3791
|
+
}
|
|
3792
|
+
const stopWatch = watch([resolvedQuery, () => hydration.isHydrated.value], ([_, hydrated]) => {
|
|
3793
|
+
if (hydrated) listen();
|
|
3794
|
+
}, { immediate: true });
|
|
3795
|
+
function stop() {
|
|
3796
|
+
stopWatch();
|
|
3797
|
+
cleanup?.();
|
|
3798
|
+
cleanup = null;
|
|
3799
|
+
}
|
|
3800
|
+
onScopeDispose(stop, true);
|
|
3801
|
+
return {
|
|
3802
|
+
matches: shallowReadonly(matches),
|
|
3803
|
+
query: resolvedQuery,
|
|
3804
|
+
mediaQueryList: shallowReadonly(mediaQueryList),
|
|
3805
|
+
stop
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
/**
|
|
3809
|
+
* Check if the user prefers dark color scheme.
|
|
3810
|
+
*
|
|
3811
|
+
* @returns The media query context.
|
|
3812
|
+
*
|
|
3813
|
+
* @example
|
|
3814
|
+
* ```ts
|
|
3815
|
+
* const { matches: prefersDark } = usePrefersDark()
|
|
3816
|
+
* ```
|
|
3817
|
+
*/
|
|
3818
|
+
function usePrefersDark() {
|
|
3819
|
+
return useMediaQuery("(prefers-color-scheme: dark)");
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Check if the user prefers reduced motion.
|
|
3823
|
+
*
|
|
3824
|
+
* @returns The media query context.
|
|
3825
|
+
*
|
|
3826
|
+
* @example
|
|
3827
|
+
* ```ts
|
|
3828
|
+
* const { matches: prefersReducedMotion } = usePrefersReducedMotion()
|
|
3829
|
+
* ```
|
|
3830
|
+
*/
|
|
3831
|
+
function usePrefersReducedMotion() {
|
|
3832
|
+
return useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Check if the user prefers more contrast.
|
|
3836
|
+
*
|
|
3837
|
+
* @returns The media query context.
|
|
3838
|
+
*
|
|
3839
|
+
* @example
|
|
3840
|
+
* ```ts
|
|
3841
|
+
* const { matches: prefersContrast } = usePrefersContrast()
|
|
3842
|
+
* ```
|
|
3843
|
+
*/
|
|
3844
|
+
function usePrefersContrast() {
|
|
3845
|
+
return useMediaQuery("(prefers-contrast: more)");
|
|
3846
|
+
}
|
|
3847
|
+
//#endregion
|
|
3848
|
+
//#region src/composables/useReducedMotion/adapters/adapter.ts
|
|
3849
|
+
var ReducedMotionAdapter = class {
|
|
3850
|
+
/**
|
|
3851
|
+
* Teardown for any side-effects started in `setup`. The plugin registers this on
|
|
3852
|
+
* `app.onUnmount`, so adapter effects are cleaned up even though `setup` runs
|
|
3853
|
+
* outside an effect scope (where `onScopeDispose` would silently no-op). It is
|
|
3854
|
+
* read lazily at app unmount, so adapters may assign it at any point during or
|
|
3855
|
+
* after `setup`.
|
|
3856
|
+
*/
|
|
3857
|
+
dispose;
|
|
3858
|
+
};
|
|
3859
|
+
//#endregion
|
|
3860
|
+
//#region src/composables/useReducedMotion/adapters/v0.ts
|
|
3861
|
+
function payload(reduced) {
|
|
3862
|
+
return reduced ? "reduce" : "no-preference";
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Default reduced-motion adapter. In the browser it writes
|
|
3866
|
+
* `document.body.dataset.reducedMotion`; on the server it renders the same
|
|
3867
|
+
* attribute through `@unhead` so the value is present in the initial HTML.
|
|
3868
|
+
*/
|
|
3869
|
+
var V0ReducedMotionAdapter = class extends ReducedMotionAdapter {
|
|
3870
|
+
setup(app, context) {
|
|
3871
|
+
if (IN_BROWSER) {
|
|
3872
|
+
function sync(reduced) {
|
|
3873
|
+
document.body.dataset.reducedMotion = payload(reduced);
|
|
3874
|
+
}
|
|
3875
|
+
sync(context.isReduced.value);
|
|
3876
|
+
this.dispose = watch(context.isReduced, sync);
|
|
3877
|
+
} else {
|
|
3878
|
+
const head = app._context?.provides?.usehead ?? app._context?.provides?.head;
|
|
3879
|
+
if (head?.push) {
|
|
3880
|
+
const entry = head.push({ bodyAttrs: { "data-reduced-motion": payload(context.isReduced.value) } });
|
|
3881
|
+
const stop = watch(context.isReduced, (reduced) => entry.patch?.({ bodyAttrs: { "data-reduced-motion": payload(reduced) } }));
|
|
3882
|
+
this.dispose = () => {
|
|
3883
|
+
stop();
|
|
3884
|
+
entry.dispose?.();
|
|
3885
|
+
};
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
};
|
|
3890
|
+
//#endregion
|
|
3891
|
+
//#region src/composables/useReducedMotion/index.ts
|
|
3892
|
+
/**
|
|
3893
|
+
* @module useReducedMotion
|
|
3894
|
+
*
|
|
3895
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-reduced-motion
|
|
3896
|
+
*
|
|
3897
|
+
* @remarks
|
|
3898
|
+
* Reduced-motion plugin composable for respecting or overriding the
|
|
3899
|
+
* `prefers-reduced-motion` media query.
|
|
3900
|
+
*
|
|
3901
|
+
* Key features:
|
|
3902
|
+
* - Three-way `mode`: `'system'` (honor OS setting), `'always'` (force reduce),
|
|
3903
|
+
* `'never'` (force full motion)
|
|
3904
|
+
* - Reactive `isReduced` boolean — true when motion should be minimized
|
|
3905
|
+
* - When installed as a plugin, the default `V0ReducedMotionAdapter` writes
|
|
3906
|
+
* `document.body.dataset.reducedMotion = 'reduce' | 'no-preference'` in the browser.
|
|
3907
|
+
* On the server it renders the same attribute via `@unhead` with the configured
|
|
3908
|
+
* mode — the OS preference is only readable in the browser, so in `'system'` mode
|
|
3909
|
+
* the server value is always `'no-preference'` until client takeover; pair the
|
|
3910
|
+
* attribute selector with a native `@media (prefers-reduced-motion: reduce)` rule
|
|
3911
|
+
* for pre-hydration coverage
|
|
3912
|
+
* - SSR-safe: no `window`/`document` access on the server
|
|
3913
|
+
*
|
|
3914
|
+
* @example
|
|
3915
|
+
* ```ts
|
|
3916
|
+
* import { createReducedMotionPlugin, useReducedMotion } from '@vuetify/v0'
|
|
3917
|
+
*
|
|
3918
|
+
* // install once
|
|
3919
|
+
* app.use(createReducedMotionPlugin({ mode: 'system' }))
|
|
3920
|
+
*
|
|
3921
|
+
* // consume anywhere
|
|
3922
|
+
* const motion = useReducedMotion()
|
|
3923
|
+
* console.log(motion.isReduced.value) // true | false
|
|
3924
|
+
* motion.select('never') // override
|
|
3925
|
+
* ```
|
|
3926
|
+
*/
|
|
3927
|
+
/**
|
|
3928
|
+
* Creates a standalone reduced-motion context.
|
|
3929
|
+
*
|
|
3930
|
+
* @param options Initial options.
|
|
3931
|
+
* @returns A `ReducedMotionContext`.
|
|
3932
|
+
*
|
|
3933
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-reduced-motion
|
|
3934
|
+
*
|
|
3935
|
+
* @example
|
|
3936
|
+
* ```ts
|
|
3937
|
+
* import { createReducedMotion } from '@vuetify/v0'
|
|
3938
|
+
*
|
|
3939
|
+
* const motion = createReducedMotion({ mode: 'system' })
|
|
3940
|
+
*
|
|
3941
|
+
* console.log(motion.isReduced.value) // true | false
|
|
3942
|
+
* motion.select('always')
|
|
3943
|
+
* ```
|
|
3944
|
+
*/
|
|
3945
|
+
function createReducedMotion(options = {}) {
|
|
3946
|
+
const selectedMode = shallowRef(options.mode ?? "system");
|
|
3947
|
+
const media = usePrefersReducedMotion();
|
|
3948
|
+
const isReduced = toRef(() => {
|
|
3949
|
+
if (selectedMode.value === "always") return true;
|
|
3950
|
+
if (selectedMode.value === "never") return false;
|
|
3951
|
+
return media.matches.value;
|
|
3952
|
+
});
|
|
3953
|
+
function select(value) {
|
|
3954
|
+
selectedMode.value = value;
|
|
3955
|
+
}
|
|
3956
|
+
return {
|
|
3957
|
+
selectedMode: shallowReadonly(selectedMode),
|
|
3958
|
+
isReduced,
|
|
3959
|
+
select,
|
|
3960
|
+
dispose: media.stop
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
function createReducedMotionFallback() {
|
|
3964
|
+
return {
|
|
3965
|
+
selectedMode: shallowReadonly(shallowRef("system")),
|
|
3966
|
+
isReduced: shallowReadonly(shallowRef(false)),
|
|
3967
|
+
select: () => {},
|
|
3968
|
+
dispose: () => {}
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
const [createReducedMotionContext, createReducedMotionPlugin, useReducedMotion] = createPluginContext("v0:reduced-motion", (options) => createReducedMotion(options), {
|
|
3972
|
+
fallback: () => createReducedMotionFallback(),
|
|
3973
|
+
persist: (context) => context.selectedMode.value,
|
|
3974
|
+
restore: (context, saved) => {
|
|
3975
|
+
if (saved === "system" || saved === "always" || saved === "never") context.select(saved);
|
|
3976
|
+
},
|
|
3977
|
+
setup: (context, app, { adapter = new V0ReducedMotionAdapter() }) => {
|
|
3978
|
+
app.onUnmount(() => context.dispose());
|
|
3979
|
+
adapter.setup(app, context);
|
|
3980
|
+
app.onUnmount(() => adapter.dispose?.());
|
|
3981
|
+
}
|
|
3982
|
+
});
|
|
3983
|
+
//#endregion
|
|
3984
|
+
//#region src/composables/useRtl/adapters/adapter.ts
|
|
3985
|
+
var RtlAdapter = class {
|
|
3986
|
+
/**
|
|
3987
|
+
* Teardown registered by `setup`. Called once via `app.onUnmount` on the
|
|
3988
|
+
* plugin path and available as a reachable handle on standalone paths.
|
|
3989
|
+
*/
|
|
3990
|
+
dispose;
|
|
3991
|
+
};
|
|
3992
|
+
//#endregion
|
|
3993
|
+
//#region src/composables/useRtl/adapters/v0.ts
|
|
3994
|
+
var V0RtlAdapter = class extends RtlAdapter {
|
|
3995
|
+
setup(app, context, target) {
|
|
3996
|
+
if (IN_BROWSER) {
|
|
3997
|
+
if (/* @__PURE__ */ isNull(target)) return;
|
|
3998
|
+
const targetEl = target instanceof HTMLElement ? target : /* @__PURE__ */ isString(target) ? document.querySelector(target) : document.documentElement;
|
|
3999
|
+
if (!targetEl) return;
|
|
4000
|
+
targetEl.dir = context.isRtl.value ? "rtl" : "ltr";
|
|
4001
|
+
this.dispose = watch(context.isRtl, (value) => {
|
|
4002
|
+
targetEl.dir = value ? "rtl" : "ltr";
|
|
4003
|
+
});
|
|
4004
|
+
} else {
|
|
4005
|
+
const head = app._context?.provides?.usehead ?? app._context?.provides?.head;
|
|
4006
|
+
if (head?.push) {
|
|
4007
|
+
const entry = head.push({ htmlAttrs: { dir: context.isRtl.value ? "rtl" : "ltr" } });
|
|
4008
|
+
if (entry?.dispose) this.dispose = entry.dispose;
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
//#endregion
|
|
4014
|
+
//#region src/composables/useRtl/index.ts
|
|
4015
|
+
/**
|
|
4016
|
+
* @module useRtl
|
|
4017
|
+
*
|
|
4018
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-rtl
|
|
4019
|
+
*
|
|
4020
|
+
* @remarks
|
|
4021
|
+
* RTL (right-to-left) direction composable with adapter pattern.
|
|
4022
|
+
*
|
|
4023
|
+
* Key features:
|
|
4024
|
+
* - Reactive boolean direction state
|
|
4025
|
+
* - Plugin trinity pattern (createRtlContext, createRtlPlugin, useRtl)
|
|
4026
|
+
* - Adapter pattern for DOM integration (dir attribute)
|
|
4027
|
+
* - Subtree overrides via context provision
|
|
4028
|
+
*
|
|
4029
|
+
* Independent from useLocale — Vuetify connects them via adapter.
|
|
4030
|
+
*
|
|
4031
|
+
* @example
|
|
4032
|
+
* ```ts
|
|
4033
|
+
* import { useRtl } from '@vuetify/v0'
|
|
4034
|
+
*
|
|
4035
|
+
* const { isRtl, toggle } = useRtl()
|
|
4036
|
+
* toggle() // flip direction
|
|
4037
|
+
* ```
|
|
4038
|
+
*/
|
|
4039
|
+
/**
|
|
4040
|
+
* Creates a new RTL direction instance.
|
|
4041
|
+
*
|
|
4042
|
+
* @param options The options for the RTL instance.
|
|
4043
|
+
* @returns A new RTL context with reactive direction state.
|
|
4044
|
+
*
|
|
4045
|
+
* @see https://0.vuetifyjs.com/composables/plugins/use-rtl
|
|
4046
|
+
*/
|
|
4047
|
+
function createRtl(options = {}) {
|
|
4048
|
+
const isRtl = shallowRef(options.default ?? false);
|
|
4049
|
+
function toggle() {
|
|
4050
|
+
isRtl.value = !isRtl.value;
|
|
4051
|
+
}
|
|
4052
|
+
return {
|
|
4053
|
+
isRtl,
|
|
4054
|
+
toggle,
|
|
4055
|
+
dispose: () => {}
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
function createRtlFallback() {
|
|
4059
|
+
return {
|
|
4060
|
+
isRtl: shallowRef(false),
|
|
4061
|
+
toggle: () => {},
|
|
4062
|
+
dispose: () => {}
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
const [createRtlContext, createRtlPlugin, useRtl] = createPluginContext("v0:rtl", (options) => createRtl(options), {
|
|
4066
|
+
fallback: () => createRtlFallback(),
|
|
4067
|
+
setup: (context, app, { adapter = new V0RtlAdapter(), target }) => {
|
|
4068
|
+
adapter.setup(app, context, target);
|
|
4069
|
+
app.onUnmount(() => adapter.dispose?.());
|
|
4070
|
+
},
|
|
4071
|
+
persist: (ctx) => ctx.isRtl.value,
|
|
4072
|
+
restore: (ctx, saved) => {
|
|
4073
|
+
if (typeof saved === "boolean") ctx.isRtl.value = saved;
|
|
4074
|
+
}
|
|
4075
|
+
});
|
|
4076
|
+
//#endregion
|
|
4077
|
+
export { createBreakpointsPlugin as A, createOtp as B, useMutationObserver as C, useDate as D, createDatePlugin as E, createVirtualContext as F, createSortable as G, createDataGrid as H, useVirtual as I, ServerComboboxAdapter as K, createTimeline as L, toReactive as M, toHighlight as N, createBreakpoints as O, createVirtual as P, createTimelineContext as R, DragDropAdapter as S, createDateContext as T, createDataGridContext as U, createKanban as V, useDataGrid as W, usePrefersReducedMotion as _, useRtl as a, KeyboardAdapter as b, createReducedMotion as c, useReducedMotion as d, V0ReducedMotionAdapter as f, usePrefersDark as g, usePrefersContrast as h, createRtlPlugin as i, useBreakpoints as j, createBreakpointsContext as k, createReducedMotionContext as l, useMediaQuery as m, createRtlContext as n, V0RtlAdapter as o, ReducedMotionAdapter as p, createRtlFallback as r, RtlAdapter as s, createRtl as t, createReducedMotionPlugin as u, useHotkey as v, createDate as w, PointerAdapter as x, useDragDrop as y, useTimeline as z };
|