@brickflow/ui 0.0.57 → 0.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.d.mts +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +33 -7
- package/dist/runtime/brickflow-ui.d.ts +1 -0
- package/dist/runtime/components/Button/index.vue +5 -3
- package/dist/runtime/components/Dialog/DemoContent.vue +33 -0
- package/dist/runtime/components/Dialog/DemoForm.vue +54 -0
- package/dist/runtime/components/Dialog/DemoSimple.vue +23 -0
- package/dist/runtime/components/Dialog/DemoStack.vue +41 -0
- package/dist/runtime/components/Dialog/Host.vue +654 -0
- package/dist/runtime/components/Dialog/basic.demo.vue +27 -0
- package/dist/runtime/components/Dialog/form.demo.vue +34 -0
- package/dist/runtime/components/Dialog/index.vue +53 -0
- package/dist/runtime/components/Dialog/scroll.demo.vue +27 -0
- package/dist/runtime/components/Dialog/stack.demo.vue +19 -0
- package/dist/runtime/components/Form/rules.demo.vue +108 -0
- package/dist/runtime/components/Icon/gallery.demo.vue +1 -1
- package/dist/runtime/components/Icon/index.vue +3 -1
- package/dist/runtime/components/Input/index.vue +7 -5
- package/dist/runtime/components/InputNumber/controls.demo.vue +145 -0
- package/dist/runtime/components/InputNumber/index.vue +4 -3
- package/dist/runtime/composables/useDialog.d.ts +38 -0
- package/dist/runtime/composables/useDialog.js +40 -0
- package/dist/runtime/composables/useScreen.d.ts +8 -0
- package/dist/runtime/plugins/dialog.d.ts +3 -0
- package/dist/runtime/plugins/dialog.js +22 -0
- package/dist/runtime/tailwind.d.ts +1 -1
- package/dist/runtime/tailwind.js +2 -2
- package/dist/types.d.mts +2 -0
- package/package.json +6 -6
- package/dist/runtime/icon-font.d.ts +0 -4
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
DialogController,
|
|
6
|
+
DialogEntry,
|
|
7
|
+
DialogPlacement,
|
|
8
|
+
DialogPlacementConfig,
|
|
9
|
+
} from '../../composables/useDialog'
|
|
10
|
+
|
|
11
|
+
import { useScreen } from '../../composables/useScreen'
|
|
12
|
+
import Icon from '../Icon/index.vue'
|
|
13
|
+
import Dialog, { dialogConfig } from './index.vue'
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{ dialog: DialogController }>()
|
|
16
|
+
const screen = useScreen()
|
|
17
|
+
const placementClasses = {
|
|
18
|
+
bottom: UI_STYLE.layerPlacementClasses.bottom,
|
|
19
|
+
center: UI_STYLE.layerPlacementClasses.center,
|
|
20
|
+
fullscreen: UI_STYLE.layerPlacementClasses.fullscreen,
|
|
21
|
+
}
|
|
22
|
+
const screenKeys = ['ms', 'mb', 'tb', 'lp', 'dk'] as const
|
|
23
|
+
function resolvePlacement(config: DialogPlacementConfig): DialogPlacement {
|
|
24
|
+
if (typeof config === 'string') {
|
|
25
|
+
return config
|
|
26
|
+
}
|
|
27
|
+
const key = screenKeys.find((candidate) => screen[candidate])
|
|
28
|
+
return key ? config[key] : 'center'
|
|
29
|
+
}
|
|
30
|
+
const dragY = ref(0)
|
|
31
|
+
const renderedDialogs = ref<
|
|
32
|
+
{
|
|
33
|
+
entry: DialogEntry
|
|
34
|
+
loadingVisible: boolean
|
|
35
|
+
ready: boolean
|
|
36
|
+
visible: boolean
|
|
37
|
+
}[]
|
|
38
|
+
>([])
|
|
39
|
+
const loadingTimers = new Map<number, number>()
|
|
40
|
+
const draggedId = ref<number>()
|
|
41
|
+
const dismissDistance = ref(320)
|
|
42
|
+
const isDragging = ref(false)
|
|
43
|
+
let startY = 0
|
|
44
|
+
let dragFrame: number | undefined
|
|
45
|
+
let pendingDragY = 0
|
|
46
|
+
let pointerId: number | undefined
|
|
47
|
+
let pointerDialogId: number | undefined
|
|
48
|
+
let pointerStartX = 0
|
|
49
|
+
let pointerStartY = 0
|
|
50
|
+
let settleTimer: number | undefined
|
|
51
|
+
let suppressBackdropClickUntil = 0
|
|
52
|
+
let dragMoved = false
|
|
53
|
+
let touchId: number | undefined
|
|
54
|
+
let touchDialogId: number | undefined
|
|
55
|
+
let touchLastY = 0
|
|
56
|
+
let touchStartX = 0
|
|
57
|
+
let touchStartY = 0
|
|
58
|
+
let touchTarget: HTMLElement | undefined
|
|
59
|
+
let previousOverflow = ''
|
|
60
|
+
let focusObserver: MutationObserver | undefined
|
|
61
|
+
let deniedAnimation: Animation | undefined
|
|
62
|
+
const completedLeaves = new Map<number, Set<'backdrop' | 'panel'>>()
|
|
63
|
+
const focusOrigins = new Map<number, HTMLElement>()
|
|
64
|
+
|
|
65
|
+
const backdropOpacity = computed(() => Math.max(0, 1 - Math.max(0, dragY.value) / dismissDistance.value))
|
|
66
|
+
const topId = (): number | undefined => props.dialog.stack.at(-1)?.id
|
|
67
|
+
|
|
68
|
+
function beginDrag(id: number, clientY: number, target: HTMLElement): void {
|
|
69
|
+
draggedId.value = id
|
|
70
|
+
isDragging.value = true
|
|
71
|
+
window.getSelection()?.removeAllRanges()
|
|
72
|
+
const layer = target.parentElement
|
|
73
|
+
const panel = layer?.querySelector('[data-dialog-panel]')
|
|
74
|
+
const panelTop = panel?.getBoundingClientRect().top
|
|
75
|
+
dismissDistance.value =
|
|
76
|
+
panelTop === undefined ? 320 : Math.max(1, Math.ceil(window.innerHeight - panelTop) + 1)
|
|
77
|
+
startY = clientY
|
|
78
|
+
dragMoved = false
|
|
79
|
+
pendingDragY = 0
|
|
80
|
+
dragY.value = 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clearGesture(): void {
|
|
84
|
+
if (dragFrame !== undefined) {
|
|
85
|
+
window.cancelAnimationFrame(dragFrame)
|
|
86
|
+
dragFrame = undefined
|
|
87
|
+
}
|
|
88
|
+
pendingDragY = 0
|
|
89
|
+
if (settleTimer !== undefined) {
|
|
90
|
+
window.clearTimeout(settleTimer)
|
|
91
|
+
settleTimer = undefined
|
|
92
|
+
}
|
|
93
|
+
draggedId.value = undefined
|
|
94
|
+
dragY.value = 0
|
|
95
|
+
isDragging.value = false
|
|
96
|
+
pointerId = undefined
|
|
97
|
+
pointerDialogId = undefined
|
|
98
|
+
touchId = undefined
|
|
99
|
+
touchDialogId = undefined
|
|
100
|
+
touchTarget = undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function denyClose(id: number): void {
|
|
104
|
+
if (id !== topId() || !props.dialog.stack.at(-1)?.options.preventClose) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const panel = topPanel()
|
|
108
|
+
if (!panel || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
deniedAnimation?.cancel()
|
|
112
|
+
deniedAnimation = panel.animate(
|
|
113
|
+
[
|
|
114
|
+
{ transform: 'translateX(0)' },
|
|
115
|
+
{ transform: 'translateX(-12px)' },
|
|
116
|
+
{ transform: 'translateX(10px)' },
|
|
117
|
+
{ transform: 'translateX(-7px)' },
|
|
118
|
+
{ transform: 'translateX(4px)' },
|
|
119
|
+
{ transform: 'translateX(0)' },
|
|
120
|
+
],
|
|
121
|
+
{ duration: 380, easing: 'ease-in-out' },
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function endDrag(event: PointerEvent): void {
|
|
126
|
+
if (event.pointerId !== pointerId) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
pointerId = undefined
|
|
130
|
+
pointerDialogId = undefined
|
|
131
|
+
if (draggedId.value !== undefined) {
|
|
132
|
+
finishDrag(event.type === 'pointercancel', event.currentTarget as HTMLElement)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function endTouch(event: TouchEvent): void {
|
|
137
|
+
const touch = Array.from(event.changedTouches).find((item) => item.identifier === touchId)
|
|
138
|
+
if (!touch) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
touchId = undefined
|
|
142
|
+
touchDialogId = undefined
|
|
143
|
+
touchTarget = undefined
|
|
144
|
+
if (draggedId.value !== undefined) {
|
|
145
|
+
finishDrag(event.type === 'touchcancel', event.currentTarget as HTMLElement)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function finishDrag(cancelled: boolean, target: HTMLElement): void {
|
|
150
|
+
flushDrag()
|
|
151
|
+
if (dragMoved) {
|
|
152
|
+
suppressBackdropClickUntil = performance.now() + 400
|
|
153
|
+
}
|
|
154
|
+
const id = draggedId.value
|
|
155
|
+
const layer = target.parentElement
|
|
156
|
+
const shouldDismiss = !cancelled && dragY.value > 100 && id !== undefined
|
|
157
|
+
const prevented = shouldDismiss && props.dialog.stack.at(-1)?.options.preventClose
|
|
158
|
+
isDragging.value = false
|
|
159
|
+
nextTick(() => {
|
|
160
|
+
if (draggedId.value !== id) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
layer?.querySelector('[data-dialog-panel]')?.getBoundingClientRect()
|
|
164
|
+
dragY.value = shouldDismiss && !prevented ? Math.max(dragY.value, dismissDistance.value) : 0
|
|
165
|
+
settleTimer = window.setTimeout(() => {
|
|
166
|
+
if (shouldDismiss && id !== undefined) {
|
|
167
|
+
if (prevented) {
|
|
168
|
+
denyClose(id)
|
|
169
|
+
} else {
|
|
170
|
+
props.dialog.close(id)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
clearGesture()
|
|
175
|
+
}, 320)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function flushDrag(): void {
|
|
180
|
+
if (dragFrame !== undefined) {
|
|
181
|
+
window.cancelAnimationFrame(dragFrame)
|
|
182
|
+
dragFrame = undefined
|
|
183
|
+
dragY.value = pendingDragY
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function focusableElements(panel: HTMLElement): HTMLElement[] {
|
|
188
|
+
return Array.from(
|
|
189
|
+
panel.querySelectorAll<HTMLElement>(
|
|
190
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
191
|
+
),
|
|
192
|
+
).filter((element) => element.getClientRects().length > 0)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function focusPanel(): void {
|
|
196
|
+
const panel = topPanel()
|
|
197
|
+
if (panel) {
|
|
198
|
+
const first = focusableElements(panel)[0]
|
|
199
|
+
if (first) {
|
|
200
|
+
first.focus()
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
panel.focus()
|
|
204
|
+
focusObserver = new MutationObserver(() => {
|
|
205
|
+
if (topPanel() !== panel || document.activeElement !== panel) {
|
|
206
|
+
focusObserver?.disconnect()
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
const next = focusableElements(panel)[0]
|
|
210
|
+
if (next) {
|
|
211
|
+
next.focus()
|
|
212
|
+
focusObserver?.disconnect()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
focusObserver.observe(panel, { childList: true, subtree: true })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function moveDrag(event: PointerEvent): void {
|
|
220
|
+
if (event.pointerId !== pointerId) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (draggedId.value === undefined && pointerDialogId !== undefined) {
|
|
224
|
+
const distanceY = event.clientY - pointerStartY
|
|
225
|
+
if (Math.abs(distanceY) <= 4 || Math.abs(event.clientX - pointerStartX) > Math.abs(distanceY)) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
beginDrag(pointerDialogId, pointerStartY, event.currentTarget as HTMLElement)
|
|
229
|
+
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
|
|
230
|
+
}
|
|
231
|
+
if (draggedId.value !== undefined) {
|
|
232
|
+
event.preventDefault()
|
|
233
|
+
}
|
|
234
|
+
updateDrag(event.clientY)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function moveTouch(event: TouchEvent): void {
|
|
238
|
+
const touch = Array.from(event.changedTouches).find((item) => item.identifier === touchId)
|
|
239
|
+
if (!touch || touchDialogId === undefined) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
const deltaY = touch.clientY - touchLastY
|
|
243
|
+
touchLastY = touch.clientY
|
|
244
|
+
if (draggedId.value === touchDialogId) {
|
|
245
|
+
event.preventDefault()
|
|
246
|
+
updateDrag(touch.clientY)
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
const totalY = touch.clientY - touchStartY
|
|
250
|
+
if (Math.abs(totalY) <= 4 || Math.abs(touch.clientX - touchStartX) > Math.abs(totalY) || deltaY === 0) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
const panel = (event.currentTarget as HTMLElement).parentElement?.querySelector('[data-dialog-panel]')
|
|
254
|
+
if (!panel || !touchTarget) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const scrollables = scrollableAncestors(touchTarget, panel)
|
|
258
|
+
const remaining = scrollables.reduce(
|
|
259
|
+
(total, element) =>
|
|
260
|
+
total + (deltaY > 0 ? element.scrollTop : element.scrollHeight - element.clientHeight - element.scrollTop),
|
|
261
|
+
0,
|
|
262
|
+
)
|
|
263
|
+
if (remaining > 1) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
event.preventDefault()
|
|
267
|
+
beginDrag(touchDialogId, touch.clientY - deltaY, event.currentTarget as HTMLElement)
|
|
268
|
+
updateDrag(touch.clientY)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function onAfterLeave(id: number, part: 'backdrop' | 'panel'): void {
|
|
272
|
+
const completed = completedLeaves.get(id) ?? new Set<'backdrop' | 'panel'>()
|
|
273
|
+
completed.add(part)
|
|
274
|
+
if (completed.size < 2) {
|
|
275
|
+
completedLeaves.set(id, completed)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
completedLeaves.delete(id)
|
|
279
|
+
renderedDialogs.value = renderedDialogs.value.filter(({ entry }) => entry.id !== id)
|
|
280
|
+
if (id === draggedId.value) {
|
|
281
|
+
clearGesture()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function onBackdropClick(id: number): void {
|
|
286
|
+
if (performance.now() < suppressBackdropClickUntil) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (id === topId()) {
|
|
290
|
+
if (props.dialog.stack.at(-1)?.options.preventClose) {
|
|
291
|
+
denyClose(id)
|
|
292
|
+
} else {
|
|
293
|
+
props.dialog.close(id)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function onBeforeBackdropLeave(element: Element): void {
|
|
299
|
+
const backdrop = element as HTMLElement
|
|
300
|
+
const id = Number(backdrop.parentElement?.dataset.dialogId)
|
|
301
|
+
if (id === draggedId.value && backdropOpacity.value < 1) {
|
|
302
|
+
backdrop.style.opacity = String(backdropOpacity.value)
|
|
303
|
+
} else {
|
|
304
|
+
backdrop.style.removeProperty('opacity')
|
|
305
|
+
}
|
|
306
|
+
removeStyleClasses(backdrop, UI_STYLE.animation.settleBackdrop)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function onBeforePanelLeave(element: Element): void {
|
|
310
|
+
removeStyleClasses(element, UI_STYLE.animation.settlePanel)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function onContentResolved(id: number): void {
|
|
314
|
+
const rendered = renderedDialogs.value.find(({ entry }) => entry.id === id)
|
|
315
|
+
if (rendered) {
|
|
316
|
+
rendered.ready = true
|
|
317
|
+
}
|
|
318
|
+
stopLoading(id)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
322
|
+
if (event.key === 'Tab') {
|
|
323
|
+
const panel = topPanel()
|
|
324
|
+
if (panel) {
|
|
325
|
+
const focusables = focusableElements(panel)
|
|
326
|
+
const first = focusables[0] ?? panel
|
|
327
|
+
const last = focusables.at(-1) ?? panel
|
|
328
|
+
if (!panel.contains(document.activeElement) || document.activeElement === panel) {
|
|
329
|
+
event.preventDefault()
|
|
330
|
+
;(event.shiftKey ? last : first).focus()
|
|
331
|
+
} else if (event.shiftKey && document.activeElement === first) {
|
|
332
|
+
event.preventDefault()
|
|
333
|
+
last.focus()
|
|
334
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
335
|
+
event.preventDefault()
|
|
336
|
+
first.focus()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const topEntry = props.dialog.stack.at(-1)
|
|
341
|
+
if (event.key === 'Escape' && topEntry) {
|
|
342
|
+
event.preventDefault()
|
|
343
|
+
if (topEntry.options.preventClose) {
|
|
344
|
+
denyClose(topEntry.id)
|
|
345
|
+
} else {
|
|
346
|
+
props.dialog.close()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function panelHiddenClass(placement: DialogPlacement): string {
|
|
352
|
+
return placement === 'bottom' ? UI_STYLE.animation.bottomPanelHidden : UI_STYLE.animation.panelHidden
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function removeStyleClasses(element: Element, classes: string): void {
|
|
356
|
+
element.classList.remove(...classes.split(' '))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function scrollableAncestors(target: HTMLElement, panel: Element): HTMLElement[] {
|
|
360
|
+
const ancestors: HTMLElement[] = []
|
|
361
|
+
let current: HTMLElement | null = target
|
|
362
|
+
while (current && panel.contains(current)) {
|
|
363
|
+
const overflowY = window.getComputedStyle(current).overflowY
|
|
364
|
+
if (/auto|scroll|overlay/u.test(overflowY) && current.scrollHeight > current.clientHeight + 1) {
|
|
365
|
+
ancestors.push(current)
|
|
366
|
+
}
|
|
367
|
+
current = current.parentElement
|
|
368
|
+
}
|
|
369
|
+
return ancestors
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function startDrag(event: PointerEvent, id: number): void {
|
|
373
|
+
if (
|
|
374
|
+
event.pointerType === 'touch' ||
|
|
375
|
+
!event.isPrimary ||
|
|
376
|
+
event.button !== 0 ||
|
|
377
|
+
(event.target instanceof Element && event.target.closest('input, textarea, [contenteditable]')) ||
|
|
378
|
+
id !== topId() ||
|
|
379
|
+
draggedId.value !== undefined ||
|
|
380
|
+
resolvePlacement(props.dialog.stack.at(-1)?.options.placement ?? 'center') !== 'bottom'
|
|
381
|
+
) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
pointerId = event.pointerId
|
|
385
|
+
pointerDialogId = id
|
|
386
|
+
pointerStartX = event.clientX
|
|
387
|
+
pointerStartY = event.clientY
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function startLoadingTimer(id: number): void {
|
|
391
|
+
if (typeof window === 'undefined') {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
const timer = window.setTimeout(() => {
|
|
395
|
+
loadingTimers.delete(id)
|
|
396
|
+
const rendered = renderedDialogs.value.find(({ entry }) => entry.id === id)
|
|
397
|
+
if (rendered?.visible && !rendered.ready) {
|
|
398
|
+
rendered.loadingVisible = true
|
|
399
|
+
}
|
|
400
|
+
}, dialogConfig.loadingDelay)
|
|
401
|
+
loadingTimers.set(id, timer)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function startTouch(event: TouchEvent, id: number): void {
|
|
405
|
+
if (
|
|
406
|
+
touchId !== undefined ||
|
|
407
|
+
draggedId.value !== undefined ||
|
|
408
|
+
id !== topId() ||
|
|
409
|
+
resolvePlacement(props.dialog.stack.at(-1)?.options.placement ?? 'center') !== 'bottom'
|
|
410
|
+
) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
const touch = event.changedTouches[0]
|
|
414
|
+
if (!touch) {
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
touchId = touch.identifier
|
|
418
|
+
touchDialogId = id
|
|
419
|
+
touchStartX = touch.clientX
|
|
420
|
+
touchStartY = touch.clientY
|
|
421
|
+
touchLastY = touch.clientY
|
|
422
|
+
touchTarget = event.target as HTMLElement
|
|
423
|
+
if (
|
|
424
|
+
touchTarget.closest('[data-dialog-handle]') ||
|
|
425
|
+
(event.currentTarget as HTMLElement).hasAttribute('data-dialog-backdrop')
|
|
426
|
+
) {
|
|
427
|
+
beginDrag(id, touch.clientY, event.currentTarget as HTMLElement)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function stopLoading(id: number): void {
|
|
432
|
+
const timer = loadingTimers.get(id)
|
|
433
|
+
if (timer !== undefined) {
|
|
434
|
+
clearTimeout(timer)
|
|
435
|
+
loadingTimers.delete(id)
|
|
436
|
+
}
|
|
437
|
+
const rendered = renderedDialogs.value.find(({ entry }) => entry.id === id)
|
|
438
|
+
if (rendered) {
|
|
439
|
+
rendered.loadingVisible = false
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function topPanel(): HTMLElement | null {
|
|
444
|
+
const id = topId()
|
|
445
|
+
return id === undefined ? null : document.querySelector(`[data-dialog-id="${id}"] [data-dialog-panel]`)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function updateDrag(clientY: number): void {
|
|
449
|
+
const distance = clientY - startY
|
|
450
|
+
dragMoved ||= Math.abs(distance) > 4
|
|
451
|
+
pendingDragY = distance < 0 ? -72 * (1 - Math.exp(distance / 120)) : distance
|
|
452
|
+
if (dragFrame === undefined) {
|
|
453
|
+
dragFrame = window.requestAnimationFrame(() => {
|
|
454
|
+
dragFrame = undefined
|
|
455
|
+
dragY.value = pendingDragY
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
watch(
|
|
461
|
+
() => props.dialog.stack.length,
|
|
462
|
+
(length) => {
|
|
463
|
+
if (length) {
|
|
464
|
+
if (!previousOverflow) {
|
|
465
|
+
previousOverflow = document.body.style.overflow || 'auto'
|
|
466
|
+
}
|
|
467
|
+
document.body.style.overflow = 'hidden'
|
|
468
|
+
} else if (previousOverflow) {
|
|
469
|
+
document.body.style.overflow = previousOverflow === 'auto' ? '' : previousOverflow
|
|
470
|
+
previousOverflow = ''
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
watch(
|
|
476
|
+
() => props.dialog.stack.map((entry) => entry.id),
|
|
477
|
+
(ids, previousIds) => {
|
|
478
|
+
focusObserver?.disconnect()
|
|
479
|
+
const added = ids.find((id) => !previousIds.includes(id))
|
|
480
|
+
if (added !== undefined && document.activeElement instanceof HTMLElement) {
|
|
481
|
+
focusOrigins.set(added, document.activeElement)
|
|
482
|
+
}
|
|
483
|
+
const removed = previousIds.filter((id) => !ids.includes(id))
|
|
484
|
+
const restoreId = ids.length ? previousIds.at(-1) : previousIds[0]
|
|
485
|
+
const restore = restoreId === undefined ? undefined : focusOrigins.get(restoreId)
|
|
486
|
+
for (const id of removed) {
|
|
487
|
+
focusOrigins.delete(id)
|
|
488
|
+
}
|
|
489
|
+
nextTick(() => {
|
|
490
|
+
if (ids.length) {
|
|
491
|
+
if (removed.length && restore?.isConnected && topPanel()?.contains(restore)) {
|
|
492
|
+
restore.focus()
|
|
493
|
+
} else {
|
|
494
|
+
focusPanel()
|
|
495
|
+
}
|
|
496
|
+
} else if (restore?.isConnected) {
|
|
497
|
+
restore.focus()
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
watch(
|
|
504
|
+
() => props.dialog.stack.map((entry) => entry.id),
|
|
505
|
+
(ids) => {
|
|
506
|
+
for (const rendered of renderedDialogs.value) {
|
|
507
|
+
if (!ids.includes(rendered.entry.id)) {
|
|
508
|
+
rendered.visible = false
|
|
509
|
+
stopLoading(rendered.entry.id)
|
|
510
|
+
if (!rendered.ready) {
|
|
511
|
+
onAfterLeave(rendered.entry.id, 'panel')
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const entry of props.dialog.stack) {
|
|
516
|
+
if (!renderedDialogs.value.some((rendered) => rendered.entry.id === entry.id)) {
|
|
517
|
+
renderedDialogs.value.push({ entry, loadingVisible: false, ready: false, visible: true })
|
|
518
|
+
startLoadingTimer(entry.id)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
{ immediate: true },
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
onMounted(() => {
|
|
526
|
+
document.addEventListener('keydown', onKeydown)
|
|
527
|
+
for (const rendered of renderedDialogs.value) {
|
|
528
|
+
if (!rendered.ready && !loadingTimers.has(rendered.entry.id)) {
|
|
529
|
+
startLoadingTimer(rendered.entry.id)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
onBeforeUnmount(() => {
|
|
534
|
+
document.removeEventListener('keydown', onKeydown)
|
|
535
|
+
for (const timer of loadingTimers.values()) {
|
|
536
|
+
clearTimeout(timer)
|
|
537
|
+
}
|
|
538
|
+
loadingTimers.clear()
|
|
539
|
+
deniedAnimation?.cancel()
|
|
540
|
+
focusObserver?.disconnect()
|
|
541
|
+
clearGesture()
|
|
542
|
+
if (previousOverflow) {
|
|
543
|
+
document.body.style.overflow = previousOverflow === 'auto' ? '' : previousOverflow
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
</script>
|
|
547
|
+
|
|
548
|
+
<template>
|
|
549
|
+
<Teleport to="body">
|
|
550
|
+
<template
|
|
551
|
+
v-for="({ entry, loadingVisible, visible }, index) in renderedDialogs"
|
|
552
|
+
:key="entry.id"
|
|
553
|
+
>
|
|
554
|
+
<div
|
|
555
|
+
:data-dialog-id="entry.id"
|
|
556
|
+
:class="[
|
|
557
|
+
UI_STYLE.layer,
|
|
558
|
+
placementClasses[resolvePlacement(entry.options.placement)],
|
|
559
|
+
entry.id === draggedId && isDragging && 'select-none',
|
|
560
|
+
]"
|
|
561
|
+
:data-placement="resolvePlacement(entry.options.placement)"
|
|
562
|
+
:style="{ zIndex: 1000 + index * 2 }"
|
|
563
|
+
>
|
|
564
|
+
<Transition
|
|
565
|
+
appear
|
|
566
|
+
:enterActiveClass="UI_STYLE.animation.backdropEnterActive"
|
|
567
|
+
:enterFromClass="UI_STYLE.animation.backdropHidden"
|
|
568
|
+
:leaveActiveClass="UI_STYLE.animation.backdropLeaveActive"
|
|
569
|
+
:leaveToClass="UI_STYLE.animation.backdropHidden"
|
|
570
|
+
@beforeLeave="onBeforeBackdropLeave"
|
|
571
|
+
@afterLeave="onAfterLeave(entry.id, 'backdrop')"
|
|
572
|
+
>
|
|
573
|
+
<div
|
|
574
|
+
v-if="visible"
|
|
575
|
+
:class="[
|
|
576
|
+
UI_STYLE.backdrop,
|
|
577
|
+
entry.id === draggedId && !isDragging && UI_STYLE.animation.settleBackdrop,
|
|
578
|
+
]"
|
|
579
|
+
data-dialog-backdrop
|
|
580
|
+
aria-hidden="true"
|
|
581
|
+
:style="entry.id === draggedId ? { opacity: backdropOpacity } : undefined"
|
|
582
|
+
@click="onBackdropClick(entry.id)"
|
|
583
|
+
@pointerdown="startDrag($event, entry.id)"
|
|
584
|
+
@pointermove="moveDrag"
|
|
585
|
+
@pointerup="endDrag"
|
|
586
|
+
@pointercancel="endDrag"
|
|
587
|
+
@touchstart="startTouch($event, entry.id)"
|
|
588
|
+
@touchmove="moveTouch"
|
|
589
|
+
@touchend="endTouch"
|
|
590
|
+
@touchcancel="endTouch"
|
|
591
|
+
/>
|
|
592
|
+
</Transition>
|
|
593
|
+
<Suspense @resolve="onContentResolved(entry.id)">
|
|
594
|
+
<template #default>
|
|
595
|
+
<Transition
|
|
596
|
+
appear
|
|
597
|
+
:enterActiveClass="UI_STYLE.animation.panelEnterActive"
|
|
598
|
+
:enterFromClass="panelHiddenClass(resolvePlacement(entry.options.placement))"
|
|
599
|
+
:leaveActiveClass="UI_STYLE.animation.panelLeaveActive"
|
|
600
|
+
:leaveToClass="panelHiddenClass(resolvePlacement(entry.options.placement))"
|
|
601
|
+
@beforeLeave="onBeforePanelLeave"
|
|
602
|
+
@afterLeave="onAfterLeave(entry.id, 'panel')"
|
|
603
|
+
>
|
|
604
|
+
<Dialog
|
|
605
|
+
v-if="visible"
|
|
606
|
+
:placement="resolvePlacement(entry.options.placement)"
|
|
607
|
+
:elasticHeight="entry.options.elasticHeight"
|
|
608
|
+
:dragY="entry.id === draggedId ? dragY : 0"
|
|
609
|
+
:class="entry.id === draggedId && !isDragging ? UI_STYLE.animation.settlePanel : undefined"
|
|
610
|
+
@pointerdown="startDrag($event, entry.id)"
|
|
611
|
+
@pointermove="moveDrag"
|
|
612
|
+
@pointerup="endDrag"
|
|
613
|
+
@pointercancel="endDrag"
|
|
614
|
+
@touchstart="startTouch($event, entry.id)"
|
|
615
|
+
@touchmove="moveTouch"
|
|
616
|
+
@touchend="endTouch"
|
|
617
|
+
@touchcancel="endTouch"
|
|
618
|
+
>
|
|
619
|
+
<component
|
|
620
|
+
:is="entry.component"
|
|
621
|
+
v-bind="entry.props"
|
|
622
|
+
:class="[
|
|
623
|
+
UI_STYLE.content,
|
|
624
|
+
resolvePlacement(entry.options.placement) === 'bottom' &&
|
|
625
|
+
entry.options.elasticHeight &&
|
|
626
|
+
UI_STYLE.elasticHeightStretch,
|
|
627
|
+
entry.id === draggedId &&
|
|
628
|
+
!isDragging &&
|
|
629
|
+
entry.options.elasticHeight &&
|
|
630
|
+
UI_STYLE.animation.settleElasticHeight,
|
|
631
|
+
]"
|
|
632
|
+
/>
|
|
633
|
+
</Dialog>
|
|
634
|
+
</Transition>
|
|
635
|
+
</template>
|
|
636
|
+
</Suspense>
|
|
637
|
+
<Transition :name="dialogConfig.loadingIconTransition">
|
|
638
|
+
<div
|
|
639
|
+
v-if="loadingVisible"
|
|
640
|
+
:class="UI_STYLE.loader"
|
|
641
|
+
role="status"
|
|
642
|
+
>
|
|
643
|
+
<Icon
|
|
644
|
+
:name="dialogConfig.loadingIconName"
|
|
645
|
+
:class="UI_STYLE.loadingIcon"
|
|
646
|
+
aria-hidden="true"
|
|
647
|
+
/>
|
|
648
|
+
<span class="sr-only">Loading dialog</span>
|
|
649
|
+
</div>
|
|
650
|
+
</Transition>
|
|
651
|
+
</div>
|
|
652
|
+
</template>
|
|
653
|
+
</Teleport>
|
|
654
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export const uiDemo = {
|
|
3
|
+
title: 'Placement and motion',
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { defineAsyncComponent } from 'vue'
|
|
9
|
+
|
|
10
|
+
import { useDialog } from '../../composables/useDialog'
|
|
11
|
+
import Button from '../Button/index.vue'
|
|
12
|
+
|
|
13
|
+
const dialog = useDialog()
|
|
14
|
+
const LazyDemoDialog = defineAsyncComponent(() => import('./DemoSimple.vue'))
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="flex flex-wrap gap-2">
|
|
19
|
+
<Button @click="dialog.open(LazyDemoDialog, { title: 'Responsive dialog' })">Default placement</Button>
|
|
20
|
+
<Button @click="dialog.open(LazyDemoDialog, { title: 'Bottom sheet' }, { placement: 'bottom' })">
|
|
21
|
+
Bottom sheet
|
|
22
|
+
</Button>
|
|
23
|
+
<Button @click="dialog.open(LazyDemoDialog, { title: 'Fullscreen dialog' }, { placement: 'fullscreen' })">
|
|
24
|
+
Fullscreen
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|