@hina-ui/vue 1.3.1 → 1.4.0
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/index.js +3990 -2016
- package/dist/index.js.map +1 -1
- package/dist/types/components/hover-card/HoverCard.vue.d.ts +7 -7
- package/dist/types/components/hover-card/HoverCard.vue.d.ts.map +1 -1
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts +27 -0
- package/dist/types/components/hover-card/HoverCardAnchor.vue.d.ts.map +1 -0
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts +17 -0
- package/dist/types/components/hover-card/composables/useHoverCardAnchor.d.ts.map +1 -0
- package/dist/types/components/image/Image.vue.d.ts +3 -1
- package/dist/types/components/image/Image.vue.d.ts.map +1 -1
- package/dist/types/components/image/composables/useImageGroupState.d.ts.map +1 -1
- package/dist/types/components/image/image.variants.d.ts +1 -1
- package/dist/types/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/types/components/lightbox/LightboxChrome.vue.d.ts +4 -0
- package/dist/types/components/lightbox/LightboxChrome.vue.d.ts.map +1 -1
- package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts +4 -0
- package/dist/types/components/lightbox/LightboxToolbar.vue.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts +4 -1
- package/dist/types/components/lightbox/composables/useLightboxFrames.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts +1 -0
- package/dist/types/components/lightbox/composables/useLightboxLarge.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts +17 -0
- package/dist/types/components/lightbox/composables/useLightboxLayout.d.ts.map +1 -0
- package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts +1 -1
- package/dist/types/components/lightbox/composables/useLightboxPhase.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxPose.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts +5 -1
- package/dist/types/components/lightbox/composables/useLightboxRotation.d.ts.map +1 -1
- package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts +10 -1
- package/dist/types/components/lightbox/composables/useLightboxZoom.d.ts.map +1 -1
- package/dist/types/components/lightbox/lightbox.variants.d.ts +2 -0
- package/dist/types/components/lightbox/lightbox.variants.d.ts.map +1 -0
- package/dist/types/components/lightbox/types.d.ts +7 -1
- package/dist/types/components/lightbox/types.d.ts.map +1 -1
- package/dist/types/components/lightbox/utils/pose.d.ts +2 -2
- package/dist/types/components/lightbox/utils/pose.d.ts.map +1 -1
- package/dist/types/components/lightbox/utils/source.d.ts +4 -0
- package/dist/types/components/lightbox/utils/source.d.ts.map +1 -0
- package/dist/types/components/lightbox/utils/zoom.d.ts +8 -4
- package/dist/types/components/lightbox/utils/zoom.d.ts.map +1 -1
- package/dist/types/components/pagination/Pagination.vue.d.ts +59 -0
- package/dist/types/components/pagination/Pagination.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationContent.vue.d.ts +23 -0
- package/dist/types/components/pagination/PaginationContent.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationControl.vue.d.ts +10 -0
- package/dist/types/components/pagination/PaginationControl.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts +23 -0
- package/dist/types/components/pagination/PaginationEllipsis.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationInfo.vue.d.ts +24 -0
- package/dist/types/components/pagination/PaginationInfo.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationJump.vue.d.ts +7 -0
- package/dist/types/components/pagination/PaginationJump.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationPage.vue.d.ts +20 -0
- package/dist/types/components/pagination/PaginationPage.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationPages.vue.d.ts +27 -0
- package/dist/types/components/pagination/PaginationPages.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationPopup.vue.d.ts +8 -0
- package/dist/types/components/pagination/PaginationPopup.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/PaginationSize.vue.d.ts +7 -0
- package/dist/types/components/pagination/PaginationSize.vue.d.ts.map +1 -0
- package/dist/types/components/pagination/composables/usePagination.d.ts +25 -0
- package/dist/types/components/pagination/composables/usePagination.d.ts.map +1 -0
- package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts +34 -0
- package/dist/types/components/pagination/composables/usePaginationEllipsis.d.ts.map +1 -0
- package/dist/types/components/pagination/composables/usePaginationJump.d.ts +8 -0
- package/dist/types/components/pagination/composables/usePaginationJump.d.ts.map +1 -0
- package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts +3 -0
- package/dist/types/components/pagination/composables/usePaginationPageTooltip.d.ts.map +1 -0
- package/dist/types/components/pagination/composables/usePaginationWindow.d.ts +188 -0
- package/dist/types/components/pagination/composables/usePaginationWindow.d.ts.map +1 -0
- package/dist/types/components/pagination/context.d.ts +19 -0
- package/dist/types/components/pagination/context.d.ts.map +1 -0
- package/dist/types/components/pagination/pagination.variants.d.ts +68 -0
- package/dist/types/components/pagination/pagination.variants.d.ts.map +1 -0
- package/dist/types/components/pagination/types.d.ts +23 -0
- package/dist/types/components/pagination/types.d.ts.map +1 -0
- package/dist/types/components/pagination/utils/ranges.d.ts +3 -0
- package/dist/types/components/pagination/utils/ranges.d.ts.map +1 -0
- package/dist/types/components/range-slider/RangeSlider.vue.d.ts +1 -0
- package/dist/types/components/range-slider/RangeSlider.vue.d.ts.map +1 -1
- package/dist/types/components/slider/Slider.vue.d.ts +1 -0
- package/dist/types/components/slider/Slider.vue.d.ts.map +1 -1
- package/dist/types/components/slider/slider.variants.d.ts.map +1 -1
- package/dist/types/components/tooltip/Tooltip.vue.d.ts +4 -15
- package/dist/types/components/tooltip/Tooltip.vue.d.ts.map +1 -1
- package/dist/types/components/tooltip/TooltipBubble.vue.d.ts +20 -0
- package/dist/types/components/tooltip/TooltipBubble.vue.d.ts.map +1 -0
- package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts +33 -0
- package/dist/types/components/tooltip/TooltipDirectiveRoot.d.ts.map +1 -0
- package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts +2 -0
- package/dist/types/components/tooltip/composables/useTooltipTarget.d.ts.map +1 -0
- package/dist/types/components/tooltip/directive.d.ts +4 -0
- package/dist/types/components/tooltip/directive.d.ts.map +1 -0
- package/dist/types/components/tooltip/types.d.ts +14 -0
- package/dist/types/components/tooltip/types.d.ts.map +1 -0
- package/dist/types/components/tooltip/utils/directiveContext.d.ts +10 -0
- package/dist/types/components/tooltip/utils/directiveContext.d.ts.map +1 -0
- package/dist/types/components/tree-select/TreeSelect.vue.d.ts +6 -1
- package/dist/types/components/tree-select/TreeSelect.vue.d.ts.map +1 -1
- package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts +22 -0
- package/dist/types/components/tree-select/TreeSelectSearch.vue.d.ts.map +1 -0
- package/dist/types/components/tree-select/composables/useTreeSelect.d.ts +35 -0
- package/dist/types/components/tree-select/composables/useTreeSelect.d.ts.map +1 -0
- package/dist/types/components/tree-select/tree-select.variants.d.ts +1 -1
- package/dist/types/components/tree-select/tree-select.variants.d.ts.map +1 -1
- package/dist/types/components/tree-select/utils/filterTree.d.ts +7 -0
- package/dist/types/components/tree-select/utils/filterTree.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/overlay-positioner.d.ts +3 -0
- package/dist/types/lib/overlay-positioner.d.ts.map +1 -0
- package/dist/types/lib/pointer-corridor.d.ts +7 -0
- package/dist/types/lib/pointer-corridor.d.ts.map +1 -0
- package/dist/types/lib/useDirection.d.ts +7 -0
- package/dist/types/lib/useDirection.d.ts.map +1 -0
- package/dist/types/locale/en-US.d.ts.map +1 -1
- package/dist/types/locale/index.d.ts.map +1 -1
- package/dist/types/locale/types.d.ts +13 -0
- package/dist/types/locale/types.d.ts.map +1 -1
- package/dist/types/locale/zh-CN.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/badge/badge.variants.ts +1 -1
- package/src/components/hover-card/HoverCard.vue +34 -16
- package/src/components/hover-card/HoverCardAnchor.vue +16 -0
- package/src/components/hover-card/composables/useHoverCardAnchor.ts +160 -0
- package/src/components/image/Image.vue +2 -0
- package/src/components/image/composables/useImageGroupState.ts +9 -1
- package/src/components/image/image.variants.ts +1 -1
- package/src/components/lightbox/Lightbox.vue +23 -17
- package/src/components/lightbox/LightboxChrome.vue +7 -1
- package/src/components/lightbox/LightboxToolbar.vue +16 -2
- package/src/components/lightbox/composables/useLightboxFrames.ts +108 -12
- package/src/components/lightbox/composables/useLightboxLarge.ts +4 -1
- package/src/components/lightbox/composables/useLightboxLayout.ts +44 -0
- package/src/components/lightbox/composables/useLightboxPhase.ts +12 -4
- package/src/components/lightbox/composables/useLightboxPose.ts +17 -19
- package/src/components/lightbox/composables/useLightboxRotation.ts +14 -4
- package/src/components/lightbox/composables/useLightboxZoom.ts +51 -9
- package/src/components/lightbox/lightbox.variants.ts +5 -0
- package/src/components/lightbox/types.ts +5 -1
- package/src/components/lightbox/utils/pose.ts +13 -6
- package/src/components/lightbox/utils/source.ts +17 -0
- package/src/components/lightbox/utils/zoom.ts +21 -7
- package/src/components/pagination/Pagination.vue +118 -0
- package/src/components/pagination/PaginationContent.vue +29 -0
- package/src/components/pagination/PaginationControl.vue +42 -0
- package/src/components/pagination/PaginationEllipsis.vue +67 -0
- package/src/components/pagination/PaginationInfo.vue +18 -0
- package/src/components/pagination/PaginationJump.vue +32 -0
- package/src/components/pagination/PaginationPage.vue +34 -0
- package/src/components/pagination/PaginationPages.vue +43 -0
- package/src/components/pagination/PaginationPopup.vue +92 -0
- package/src/components/pagination/PaginationSize.vue +30 -0
- package/src/components/pagination/composables/usePagination.ts +76 -0
- package/src/components/pagination/composables/usePaginationEllipsis.ts +269 -0
- package/src/components/pagination/composables/usePaginationJump.ts +23 -0
- package/src/components/pagination/composables/usePaginationPageTooltip.ts +23 -0
- package/src/components/pagination/composables/usePaginationWindow.ts +116 -0
- package/src/components/pagination/context.ts +23 -0
- package/src/components/pagination/pagination.variants.ts +32 -0
- package/src/components/pagination/types.ts +19 -0
- package/src/components/pagination/utils/ranges.ts +12 -0
- package/src/components/range-slider/RangeSlider.vue +6 -0
- package/src/components/slider/Slider.vue +6 -0
- package/src/components/slider/slider.variants.ts +1 -0
- package/src/components/tooltip/Tooltip.vue +10 -33
- package/src/components/tooltip/TooltipBubble.vue +36 -0
- package/src/components/tooltip/TooltipDirectiveRoot.ts +61 -0
- package/src/components/tooltip/composables/useTooltipTarget.ts +56 -0
- package/src/components/tooltip/directive.ts +54 -0
- package/src/components/tooltip/types.ts +15 -0
- package/src/components/tooltip/utils/directiveContext.ts +32 -0
- package/src/components/tree-select/TreeSelect.vue +90 -61
- package/src/components/tree-select/TreeSelectSearch.vue +48 -0
- package/src/components/tree-select/composables/useTreeSelect.ts +130 -0
- package/src/components/tree-select/tree-select.variants.ts +2 -2
- package/src/components/tree-select/utils/filterTree.ts +19 -0
- package/src/index.ts +9 -1
- package/src/lib/overlay-positioner.ts +25 -0
- package/src/lib/pointer-corridor.ts +26 -0
- package/src/lib/useDirection.ts +31 -0
- package/src/locale/en-US.ts +15 -1
- package/src/locale/index.ts +1 -0
- package/src/locale/types.ts +13 -0
- package/src/locale/zh-CN.ts +15 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { computed, onScopeDispose, shallowRef, watch, type ComponentPublicInstance } from 'vue'
|
|
2
|
+
import { useEventListener, useRafFn } from '@vueuse/core'
|
|
3
|
+
import { injectHoverCardRootContext } from 'reka-ui'
|
|
4
|
+
import { useOverlayPortal } from '../../../lib/overlay-portal'
|
|
5
|
+
import { useOverlayPositionerClass } from '../../../lib/overlay-positioner'
|
|
6
|
+
import { inPointerCorridor } from '../../../lib/pointer-corridor'
|
|
7
|
+
|
|
8
|
+
interface HoverCardAnchorOptions {
|
|
9
|
+
anchor?: HTMLElement | null
|
|
10
|
+
external: boolean
|
|
11
|
+
closeDelay: number
|
|
12
|
+
positionerClass?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useHoverCardAnchor(props: HoverCardAnchorOptions) {
|
|
16
|
+
const root = injectHoverCardRootContext()
|
|
17
|
+
const original = { onOpen: root.onOpen, onClose: root.onClose, onDismiss: root.onDismiss }
|
|
18
|
+
const anchor = computed(() => (props.external ? (props.anchor ?? undefined) : undefined))
|
|
19
|
+
const { content, present } = useOverlayPortal(root.open)
|
|
20
|
+
useOverlayPositionerClass(content, () => props.positionerClass)
|
|
21
|
+
const panel = computed(() =>
|
|
22
|
+
props.external ? (content.value?.$el as HTMLElement | undefined) : undefined,
|
|
23
|
+
)
|
|
24
|
+
const liveAnchor = shallowRef<HTMLElement>()
|
|
25
|
+
const retained = shallowRef<{
|
|
26
|
+
getBoundingClientRect: () => DOMRect
|
|
27
|
+
contextElement: HTMLElement
|
|
28
|
+
}>()
|
|
29
|
+
const reference = computed(() =>
|
|
30
|
+
props.external ? (liveAnchor.value ?? retained.value) : undefined,
|
|
31
|
+
)
|
|
32
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
33
|
+
let tracking = false
|
|
34
|
+
|
|
35
|
+
function cancel() {
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
timer = undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dismiss() {
|
|
41
|
+
cancel()
|
|
42
|
+
tracking = false
|
|
43
|
+
root.onOpenChange(false)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function close() {
|
|
47
|
+
if (
|
|
48
|
+
timer !== undefined ||
|
|
49
|
+
!root.open.value ||
|
|
50
|
+
root.hasSelectionRef.value ||
|
|
51
|
+
root.isPointerDownOnContentRef.value
|
|
52
|
+
)
|
|
53
|
+
return
|
|
54
|
+
timer = setTimeout(dismiss, props.closeDelay)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function keep() {
|
|
58
|
+
tracking = false
|
|
59
|
+
cancel()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function track(event: Event) {
|
|
63
|
+
const pointer = event as PointerEvent
|
|
64
|
+
if (!root.open.value || !tracking || pointer.pointerType === 'touch') return
|
|
65
|
+
const target = pointer.target as Node | null
|
|
66
|
+
if (target && (anchor.value?.contains(target) || panel.value?.contains(target))) return keep()
|
|
67
|
+
const a = anchor.value?.getBoundingClientRect()
|
|
68
|
+
const b = panel.value?.getBoundingClientRect()
|
|
69
|
+
if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
|
|
70
|
+
else close()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function leave(event: Event) {
|
|
74
|
+
const pointer = event as PointerEvent
|
|
75
|
+
if (pointer.pointerType === 'touch' || !root.open.value) return
|
|
76
|
+
tracking = true
|
|
77
|
+
const a = anchor.value?.getBoundingClientRect()
|
|
78
|
+
const b = panel.value?.getBoundingClientRect()
|
|
79
|
+
if (a && b && inPointerCorridor({ x: pointer.clientX, y: pointer.clientY }, a, b)) cancel()
|
|
80
|
+
else close()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function measure() {
|
|
84
|
+
const element = anchor.value
|
|
85
|
+
if (!element?.isConnected) {
|
|
86
|
+
liveAnchor.value = undefined
|
|
87
|
+
if (root.open.value) dismiss()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
liveAnchor.value = element
|
|
91
|
+
const rect = element.getBoundingClientRect()
|
|
92
|
+
const previous = retained.value?.getBoundingClientRect()
|
|
93
|
+
if (
|
|
94
|
+
!previous ||
|
|
95
|
+
retained.value?.contextElement !== element ||
|
|
96
|
+
rect.x !== previous.x ||
|
|
97
|
+
rect.y !== previous.y ||
|
|
98
|
+
rect.width !== previous.width ||
|
|
99
|
+
rect.height !== previous.height
|
|
100
|
+
)
|
|
101
|
+
retained.value = { getBoundingClientRect: () => rect, contextElement: element }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { pause, resume } = useRafFn(measure, { immediate: false })
|
|
105
|
+
root.onOpen = () => (props.external ? keep() : original.onOpen())
|
|
106
|
+
root.onClose = () => (props.external ? close() : original.onClose())
|
|
107
|
+
root.onDismiss = () => (props.external ? dismiss() : original.onDismiss())
|
|
108
|
+
|
|
109
|
+
watch(
|
|
110
|
+
[anchor, root.open, present],
|
|
111
|
+
([element, , mounted], previous) => {
|
|
112
|
+
cancel()
|
|
113
|
+
tracking = false
|
|
114
|
+
pause()
|
|
115
|
+
if (!props.external || !mounted) {
|
|
116
|
+
liveAnchor.value = undefined
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (element !== previous?.[0]) {
|
|
120
|
+
root.hasSelectionRef.value = false
|
|
121
|
+
root.isPointerDownOnContentRef.value = false
|
|
122
|
+
}
|
|
123
|
+
measure()
|
|
124
|
+
resume()
|
|
125
|
+
},
|
|
126
|
+
{ immediate: true, flush: 'post' },
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
useEventListener(anchor, 'pointerenter', keep)
|
|
130
|
+
useEventListener(anchor, 'pointerleave', leave)
|
|
131
|
+
useEventListener(anchor, 'focusin', keep)
|
|
132
|
+
useEventListener(anchor, 'focusout', event => {
|
|
133
|
+
const target = (event as FocusEvent).relatedTarget as Node | null
|
|
134
|
+
if (!target || (!anchor.value?.contains(target) && !panel.value?.contains(target))) close()
|
|
135
|
+
})
|
|
136
|
+
useEventListener(panel, 'pointerenter', keep)
|
|
137
|
+
useEventListener(panel, 'pointerleave', leave)
|
|
138
|
+
useEventListener(() => anchor.value?.ownerDocument, 'pointermove', track)
|
|
139
|
+
useEventListener(
|
|
140
|
+
() => anchor.value?.ownerDocument,
|
|
141
|
+
'scroll',
|
|
142
|
+
event => {
|
|
143
|
+
if (root.open.value && (event.target as Node | null)?.contains(anchor.value ?? null))
|
|
144
|
+
dismiss()
|
|
145
|
+
},
|
|
146
|
+
{ capture: true, passive: true },
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
onScopeDispose(() => {
|
|
150
|
+
cancel()
|
|
151
|
+
Object.assign(root, original)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
function contentRef(value: Element | ComponentPublicInstance | null) {
|
|
155
|
+
content.value = value as ComponentPublicInstance | null
|
|
156
|
+
if (!value && !root.open.value) retained.value = undefined
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { reference, contentRef, present }
|
|
160
|
+
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
skeleton?: boolean
|
|
26
26
|
eager?: boolean
|
|
27
27
|
preview?: boolean | string
|
|
28
|
+
previewSize?: LightboxItem['previewSize']
|
|
28
29
|
draggable?: boolean
|
|
29
30
|
class?: string
|
|
30
31
|
style?: StyleValue
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
typeof props.preview === 'string' ? props.preview : (props.src ?? ''),
|
|
65
66
|
'preview',
|
|
66
67
|
),
|
|
68
|
+
previewSize: props.previewSize,
|
|
67
69
|
alt: props.alt,
|
|
68
70
|
fit: props.fit,
|
|
69
71
|
source: () => imageEl.value,
|
|
@@ -10,7 +10,15 @@ export function useImageGroupState() {
|
|
|
10
10
|
function byDom(a: LightboxItem, b: LightboxItem): number {
|
|
11
11
|
const first = a.source?.()
|
|
12
12
|
const second = b.source?.()
|
|
13
|
-
if (
|
|
13
|
+
if (
|
|
14
|
+
!first ||
|
|
15
|
+
!second ||
|
|
16
|
+
first === second ||
|
|
17
|
+
!('compareDocumentPosition' in first) ||
|
|
18
|
+
!('compareDocumentPosition' in second)
|
|
19
|
+
) {
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
14
22
|
return first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1
|
|
15
23
|
}
|
|
16
24
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { computed, onMounted, shallowRef, watch } from 'vue'
|
|
3
3
|
import { DialogRoot, DialogPortal, DialogContent, DialogTitle, useBodyScrollLock } from 'reka-ui'
|
|
4
4
|
import { Motion } from 'motion-v'
|
|
5
|
-
import { useEventListener } from '@vueuse/core'
|
|
6
5
|
import { cn } from '../../lib/cn'
|
|
7
6
|
import VisuallyHidden from '../visually-hidden/VisuallyHidden.vue'
|
|
8
7
|
import LightboxStrip from './LightboxStrip.vue'
|
|
@@ -17,6 +16,8 @@
|
|
|
17
16
|
import { useLightboxPhase } from './composables/useLightboxPhase'
|
|
18
17
|
import { useLightboxHint } from './composables/useLightboxHint'
|
|
19
18
|
import { useLightboxSlides } from './composables/useLightboxSlides'
|
|
19
|
+
import { lightboxStage } from './lightbox.variants'
|
|
20
|
+
import { useLightboxLayout } from './composables/useLightboxLayout'
|
|
20
21
|
import { useLightboxRotation } from './composables/useLightboxRotation'
|
|
21
22
|
import { clampIndex } from './utils/paging'
|
|
22
23
|
import { saveImage } from './utils/download'
|
|
@@ -57,9 +58,10 @@
|
|
|
57
58
|
loop: () => !!props.loop,
|
|
58
59
|
})
|
|
59
60
|
|
|
60
|
-
function measure() {
|
|
61
|
+
function measure(force = false) {
|
|
62
|
+
const width = frames.stage.value.width
|
|
61
63
|
frames.measure(stageEl.value)
|
|
62
|
-
paging.sync()
|
|
64
|
+
if (force || width !== frames.stage.value.width) paging.sync()
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
const phase = useLightboxPhase({
|
|
@@ -70,9 +72,9 @@
|
|
|
70
72
|
pose: pose.fromSource,
|
|
71
73
|
prepare: () => {
|
|
72
74
|
rotation.clear()
|
|
73
|
-
frames.read()
|
|
75
|
+
return frames.read(() => current.value)
|
|
74
76
|
},
|
|
75
|
-
layout: measure,
|
|
77
|
+
layout: () => measure(true),
|
|
76
78
|
})
|
|
77
79
|
|
|
78
80
|
const close = phase.close
|
|
@@ -94,6 +96,16 @@
|
|
|
94
96
|
() => mounted.value,
|
|
95
97
|
)
|
|
96
98
|
|
|
99
|
+
const layout = useLightboxLayout({
|
|
100
|
+
stage: () => stageEl.value,
|
|
101
|
+
current: () => current.value,
|
|
102
|
+
frames,
|
|
103
|
+
large,
|
|
104
|
+
phase,
|
|
105
|
+
zoom,
|
|
106
|
+
measure,
|
|
107
|
+
})
|
|
108
|
+
|
|
97
109
|
const hint = useLightboxHint({ motion, frames, paging, current: () => current.value })
|
|
98
110
|
|
|
99
111
|
const looping = computed(() => !!props.loop && count.value > 1)
|
|
@@ -137,10 +149,6 @@
|
|
|
137
149
|
if (open.value) void phase.show()
|
|
138
150
|
})
|
|
139
151
|
|
|
140
|
-
useEventListener('resize', () => {
|
|
141
|
-
if (mounted.value) measure()
|
|
142
|
-
})
|
|
143
|
-
|
|
144
152
|
function onEscape(event: Event) {
|
|
145
153
|
event.preventDefault()
|
|
146
154
|
void close()
|
|
@@ -173,7 +181,7 @@
|
|
|
173
181
|
const currentClass = computed(() =>
|
|
174
182
|
cn(
|
|
175
183
|
'absolute will-change-transform',
|
|
176
|
-
!zoom.zoomed.value && 'cursor-zoom-in',
|
|
184
|
+
!zoom.zoomed.value && (zoom.canToggle.value ? 'cursor-zoom-in' : 'cursor-default'),
|
|
177
185
|
zoom.zoomed.value && (gesture.mode.value === 'pan' ? 'cursor-grabbing' : 'cursor-grab'),
|
|
178
186
|
),
|
|
179
187
|
)
|
|
@@ -191,12 +199,7 @@
|
|
|
191
199
|
<div
|
|
192
200
|
ref="stageEl"
|
|
193
201
|
:data-hn-phase="phase.current()"
|
|
194
|
-
:class="
|
|
195
|
-
cn(
|
|
196
|
-
'dark fixed inset-0 z-(--hn-z-overlay) touch-none outline-none select-none',
|
|
197
|
-
props.class,
|
|
198
|
-
)
|
|
199
|
-
"
|
|
202
|
+
:class="cn(lightboxStage(), props.class)"
|
|
200
203
|
@pointerdown="gesture.onPointerdown"
|
|
201
204
|
@pointermove="gesture.onPointermove"
|
|
202
205
|
@pointerup="gesture.onPointerup"
|
|
@@ -220,7 +223,7 @@
|
|
|
220
223
|
:transition="motion.transition('base')"
|
|
221
224
|
:large-src="large.src.value"
|
|
222
225
|
:large-ready="large.ready.value"
|
|
223
|
-
@learn="
|
|
226
|
+
@learn="layout.learn"
|
|
224
227
|
/>
|
|
225
228
|
<DialogTitle as-child>
|
|
226
229
|
<VisuallyHidden>{{ current?.alt }}</VisuallyHidden>
|
|
@@ -229,6 +232,8 @@
|
|
|
229
232
|
:items="props.items"
|
|
230
233
|
:index="index"
|
|
231
234
|
:zoomed="zoom.zoomed.value"
|
|
235
|
+
:can-zoom-in="zoom.canZoomIn.value"
|
|
236
|
+
:at-original="zoom.atOriginal.value"
|
|
232
237
|
:waiting="large.waiting.value"
|
|
233
238
|
:loop="looping"
|
|
234
239
|
:hint="hint"
|
|
@@ -239,6 +244,7 @@
|
|
|
239
244
|
@zoom-in="zoom.step(1)"
|
|
240
245
|
@zoom-out="zoom.step(-1)"
|
|
241
246
|
@reset="zoom.reset()"
|
|
247
|
+
@original="zoom.original()"
|
|
242
248
|
@rotate="rotate"
|
|
243
249
|
@download="download"
|
|
244
250
|
/>
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
items: LightboxItem[]
|
|
17
17
|
index: number
|
|
18
18
|
zoomed: boolean
|
|
19
|
+
canZoomIn: boolean
|
|
20
|
+
atOriginal: boolean
|
|
19
21
|
waiting: boolean
|
|
20
22
|
loop: boolean
|
|
21
23
|
presence: MotionValue<number>
|
|
@@ -29,6 +31,7 @@
|
|
|
29
31
|
zoomIn: []
|
|
30
32
|
zoomOut: []
|
|
31
33
|
reset: []
|
|
34
|
+
original: []
|
|
32
35
|
rotate: []
|
|
33
36
|
download: []
|
|
34
37
|
}>()
|
|
@@ -88,7 +91,7 @@
|
|
|
88
91
|
<ChevronRight />
|
|
89
92
|
</IconButton>
|
|
90
93
|
</Motion>
|
|
91
|
-
<Motion class="absolute end-4 top-4" :style="{ opacity: props.presence }">
|
|
94
|
+
<Motion data-hn-close class="absolute end-4 top-4" :style="{ opacity: props.presence }">
|
|
92
95
|
<DialogClose as-child>
|
|
93
96
|
<CloseButton size="md" />
|
|
94
97
|
</DialogClose>
|
|
@@ -102,10 +105,13 @@
|
|
|
102
105
|
:items="props.items"
|
|
103
106
|
:index="props.index"
|
|
104
107
|
:zoomed="props.zoomed"
|
|
108
|
+
:can-zoom-in="props.canZoomIn"
|
|
109
|
+
:at-original="props.atOriginal"
|
|
105
110
|
@select="emit('select', $event)"
|
|
106
111
|
@zoom-in="emit('zoomIn')"
|
|
107
112
|
@zoom-out="emit('zoomOut')"
|
|
108
113
|
@reset="emit('reset')"
|
|
114
|
+
@original="emit('original')"
|
|
109
115
|
@rotate="emit('rotate')"
|
|
110
116
|
@download="emit('download')"
|
|
111
117
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { Download, RotateCw, Shrink, ZoomIn, ZoomOut } from '@lucide/vue'
|
|
2
|
+
import { Download, RotateCw, Scan, Shrink, ZoomIn, ZoomOut } from '@lucide/vue'
|
|
3
3
|
import { useUiLocale } from '../../locale'
|
|
4
4
|
import IconButton from '../icon-button/IconButton.vue'
|
|
5
5
|
import LightboxThumbs from './LightboxThumbs.vue'
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
items: LightboxItem[]
|
|
12
12
|
index: number
|
|
13
13
|
zoomed: boolean
|
|
14
|
+
canZoomIn: boolean
|
|
15
|
+
atOriginal: boolean
|
|
14
16
|
}>()
|
|
15
17
|
|
|
16
18
|
const emit = defineEmits<{
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
zoomIn: []
|
|
19
21
|
zoomOut: []
|
|
20
22
|
reset: []
|
|
23
|
+
original: []
|
|
21
24
|
rotate: []
|
|
22
25
|
download: []
|
|
23
26
|
}>()
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
</script>
|
|
27
30
|
|
|
28
31
|
<template>
|
|
29
|
-
<div class="flex flex-col items-center gap-1 pb-3">
|
|
32
|
+
<div class="flex flex-col items-center gap-1 pb-[max(--spacing(3),env(safe-area-inset-bottom))]">
|
|
30
33
|
<LightboxThumbs
|
|
31
34
|
v-if="props.items.length > 1"
|
|
32
35
|
:items="props.items"
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
<div class="flex items-center gap-1">
|
|
37
40
|
<IconButton
|
|
38
41
|
:label="t.lightbox.zoomOut"
|
|
42
|
+
:disabled="!props.zoomed"
|
|
39
43
|
variant="soft"
|
|
40
44
|
pill
|
|
41
45
|
class="hidden pointer-fine:inline-flex"
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
</IconButton>
|
|
46
50
|
<IconButton
|
|
47
51
|
:label="t.lightbox.zoomIn"
|
|
52
|
+
:disabled="!props.canZoomIn"
|
|
48
53
|
variant="soft"
|
|
49
54
|
pill
|
|
50
55
|
class="hidden pointer-fine:inline-flex"
|
|
@@ -52,6 +57,15 @@
|
|
|
52
57
|
>
|
|
53
58
|
<ZoomIn />
|
|
54
59
|
</IconButton>
|
|
60
|
+
<IconButton
|
|
61
|
+
:label="t.lightbox.actualSize"
|
|
62
|
+
variant="soft"
|
|
63
|
+
pill
|
|
64
|
+
:disabled="props.atOriginal"
|
|
65
|
+
@click="emit('original')"
|
|
66
|
+
>
|
|
67
|
+
<Scan />
|
|
68
|
+
</IconButton>
|
|
55
69
|
<IconButton
|
|
56
70
|
:label="t.lightbox.resetZoom"
|
|
57
71
|
variant="soft"
|
|
@@ -1,38 +1,134 @@
|
|
|
1
|
-
import { reactive, shallowRef } from 'vue'
|
|
1
|
+
import { onScopeDispose, reactive, shallowRef } from 'vue'
|
|
2
2
|
import type { LightboxItem } from '../types'
|
|
3
3
|
import { fitRect, type Rect } from '../utils/pose'
|
|
4
4
|
import type { Size } from '../utils/zoom'
|
|
5
5
|
|
|
6
|
+
function previewSizeOf(item: LightboxItem | undefined): Size | undefined {
|
|
7
|
+
const size = item?.previewSize
|
|
8
|
+
if (
|
|
9
|
+
size &&
|
|
10
|
+
Number.isFinite(size.width) &&
|
|
11
|
+
size.width > 0 &&
|
|
12
|
+
Number.isFinite(size.height) &&
|
|
13
|
+
size.height > 0
|
|
14
|
+
)
|
|
15
|
+
return size
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export function useLightboxFrames(items: () => LightboxItem[]) {
|
|
7
19
|
const stage = shallowRef<Size>({ width: 0, height: 0 })
|
|
8
|
-
const naturals = reactive(new Map<string, Size>())
|
|
20
|
+
const naturals = reactive(new Map<string, Size & { src: string }>())
|
|
21
|
+
const previews = reactive(new Map<string, Size & { src: string; preview: string }>())
|
|
22
|
+
const area = shallowRef<Rect>({ x: 0, y: 0, width: 0, height: 0 })
|
|
23
|
+
let loader: HTMLImageElement | undefined
|
|
9
24
|
|
|
10
25
|
function measure(el: HTMLElement | undefined) {
|
|
11
|
-
|
|
26
|
+
if (!el) return
|
|
27
|
+
const width = el.clientWidth
|
|
28
|
+
const height = el.clientHeight
|
|
29
|
+
const style = getComputedStyle(el)
|
|
30
|
+
const rect = el.getBoundingClientRect()
|
|
31
|
+
const chrome = el.querySelector('[data-hn-chrome]')?.getBoundingClientRect()
|
|
32
|
+
const close = el.querySelector('[data-hn-close]')?.getBoundingClientRect()
|
|
33
|
+
const x = Math.min(
|
|
34
|
+
width / 2,
|
|
35
|
+
Math.max(parseFloat(style.paddingLeft), parseFloat(style.paddingRight)),
|
|
36
|
+
)
|
|
37
|
+
const y = Math.min(
|
|
38
|
+
height / 2,
|
|
39
|
+
Math.max(
|
|
40
|
+
parseFloat(style.paddingTop) + (close ? close.bottom - rect.top : 0),
|
|
41
|
+
parseFloat(style.paddingBottom) + (chrome ? rect.bottom - chrome.top : 0),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
stage.value = { width, height }
|
|
45
|
+
area.value = { x, y, width: width - x * 2, height: height - y * 2 }
|
|
12
46
|
}
|
|
13
47
|
|
|
14
|
-
function read() {
|
|
48
|
+
function read(current?: () => LightboxItem | undefined): Promise<void> | undefined {
|
|
49
|
+
loader?.removeAttribute('src')
|
|
50
|
+
loader = undefined
|
|
15
51
|
for (const item of items()) {
|
|
16
52
|
const el = item.source?.()
|
|
17
|
-
if (
|
|
18
|
-
|
|
53
|
+
if (
|
|
54
|
+
el &&
|
|
55
|
+
'naturalWidth' in el &&
|
|
56
|
+
el.naturalWidth &&
|
|
57
|
+
el.naturalHeight &&
|
|
58
|
+
!el.srcset &&
|
|
59
|
+
(el.currentSrc || el.src) === new URL(item.src, el.ownerDocument.baseURI).href
|
|
60
|
+
) {
|
|
61
|
+
naturals.set(item.id, { src: item.src, width: el.naturalWidth, height: el.naturalHeight })
|
|
19
62
|
}
|
|
20
63
|
}
|
|
64
|
+
const item = current?.()
|
|
65
|
+
if (!item || previewSizeOf(item) || naturalOf(item)) return
|
|
66
|
+
const img = new Image()
|
|
67
|
+
loader = img
|
|
68
|
+
const { id, src } = item
|
|
69
|
+
img.src = src
|
|
70
|
+
return img
|
|
71
|
+
.decode()
|
|
72
|
+
.catch(() => {})
|
|
73
|
+
.then(() => {
|
|
74
|
+
if (loader !== img) return
|
|
75
|
+
learn(id, img)
|
|
76
|
+
loader = undefined
|
|
77
|
+
const latest = current?.()
|
|
78
|
+
if (latest?.id !== id || latest?.src !== src) return read(current)
|
|
79
|
+
})
|
|
21
80
|
}
|
|
22
81
|
|
|
23
82
|
function learn(id: string, img: HTMLImageElement) {
|
|
24
|
-
|
|
25
|
-
|
|
83
|
+
const item = items().find(item => item.id === id)
|
|
84
|
+
if (
|
|
85
|
+
!item ||
|
|
86
|
+
naturalOf(item) ||
|
|
87
|
+
!img.naturalWidth ||
|
|
88
|
+
!img.naturalHeight ||
|
|
89
|
+
img.src !== new URL(item.src, img.ownerDocument.baseURI).href
|
|
90
|
+
) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
naturals.set(id, { src: item.src, width: img.naturalWidth, height: img.naturalHeight })
|
|
26
94
|
}
|
|
27
95
|
|
|
28
96
|
function naturalOf(item: LightboxItem | undefined): Size | undefined {
|
|
29
|
-
|
|
97
|
+
if (!item) return
|
|
98
|
+
const natural = naturals.get(item.id)
|
|
99
|
+
return natural?.src === item.src ? natural : undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function learnPreview(item: LightboxItem | undefined, img: HTMLImageElement | undefined) {
|
|
103
|
+
if (!item?.preview || !img?.naturalWidth || !img.naturalHeight) return
|
|
104
|
+
if (img.src !== new URL(item.preview, img.ownerDocument.baseURI).href) return
|
|
105
|
+
const previous = previews.get(item.id)
|
|
106
|
+
if (previous?.src === item.src && previous.preview === item.preview) return
|
|
107
|
+
previews.set(item.id, {
|
|
108
|
+
src: item.src,
|
|
109
|
+
preview: item.preview,
|
|
110
|
+
width: img.naturalWidth,
|
|
111
|
+
height: img.naturalHeight,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function displayOf(item: LightboxItem | undefined): Size | undefined {
|
|
116
|
+
if (!item) return
|
|
117
|
+
const size = previewSizeOf(item)
|
|
118
|
+
if (size) return size
|
|
119
|
+
const preview = previews.get(item.id)
|
|
120
|
+
return preview?.src === item.src && preview.preview === item.preview ? preview : naturalOf(item)
|
|
30
121
|
}
|
|
31
122
|
|
|
32
123
|
function frameOf(item: LightboxItem | undefined): Rect {
|
|
33
|
-
const natural =
|
|
34
|
-
return natural ? fitRect(natural,
|
|
124
|
+
const natural = displayOf(item)
|
|
125
|
+
return natural ? fitRect(natural, area.value) : area.value
|
|
35
126
|
}
|
|
36
127
|
|
|
37
|
-
|
|
128
|
+
onScopeDispose(() => {
|
|
129
|
+
loader?.removeAttribute('src')
|
|
130
|
+
loader = undefined
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return { stage, area, measure, read, learn, learnPreview, naturalOf, displayOf, frameOf }
|
|
38
134
|
}
|
|
@@ -6,6 +6,7 @@ export const LARGE_HINT_DELAY_MS = 800
|
|
|
6
6
|
export function useLightboxLarge(item: () => LightboxItem | undefined, active: () => boolean) {
|
|
7
7
|
const src = shallowRef<string>()
|
|
8
8
|
const ready = shallowRef(false)
|
|
9
|
+
const image = shallowRef<HTMLImageElement>()
|
|
9
10
|
const waiting = shallowRef(false)
|
|
10
11
|
let loader: HTMLImageElement | null = null
|
|
11
12
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -17,6 +18,7 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
|
|
|
17
18
|
timer = null
|
|
18
19
|
src.value = undefined
|
|
19
20
|
ready.value = false
|
|
21
|
+
image.value = undefined
|
|
20
22
|
waiting.value = false
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -36,6 +38,7 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
|
|
|
36
38
|
if (timer) clearTimeout(timer)
|
|
37
39
|
timer = null
|
|
38
40
|
waiting.value = false
|
|
41
|
+
image.value = img
|
|
39
42
|
ready.value = true
|
|
40
43
|
}
|
|
41
44
|
const decode = typeof img.decode === 'function' ? img.decode() : Promise.resolve()
|
|
@@ -58,5 +61,5 @@ export function useLightboxLarge(item: () => LightboxItem | undefined, active: (
|
|
|
58
61
|
|
|
59
62
|
onBeforeUnmount(reset)
|
|
60
63
|
|
|
61
|
-
return { src, ready, waiting }
|
|
64
|
+
return { src, ready, waiting, image }
|
|
62
65
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { watch } from 'vue'
|
|
2
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
3
|
+
import type { LightboxItem } from '../types'
|
|
4
|
+
import type { useLightboxFrames } from './useLightboxFrames'
|
|
5
|
+
import type { useLightboxLarge } from './useLightboxLarge'
|
|
6
|
+
import type { useLightboxPhase } from './useLightboxPhase'
|
|
7
|
+
import type { useLightboxZoom } from './useLightboxZoom'
|
|
8
|
+
|
|
9
|
+
export function useLightboxLayout(options: {
|
|
10
|
+
stage: () => HTMLElement | undefined
|
|
11
|
+
current: () => LightboxItem | undefined
|
|
12
|
+
frames: ReturnType<typeof useLightboxFrames>
|
|
13
|
+
large: ReturnType<typeof useLightboxLarge>
|
|
14
|
+
phase: ReturnType<typeof useLightboxPhase>
|
|
15
|
+
zoom: ReturnType<typeof useLightboxZoom>
|
|
16
|
+
measure: () => void
|
|
17
|
+
}) {
|
|
18
|
+
function sync() {
|
|
19
|
+
if (options.phase.current() !== 'open') return
|
|
20
|
+
options.zoom.reflow(() => {
|
|
21
|
+
options.measure()
|
|
22
|
+
options.frames.learnPreview(options.current(), options.large.image.value)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function learn(id: string, img: HTMLImageElement) {
|
|
27
|
+
const update = () => options.frames.learn(id, img)
|
|
28
|
+
if (options.phase.current() === 'open' && options.current()?.id === id)
|
|
29
|
+
options.zoom.reflow(update)
|
|
30
|
+
else update()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
watch(() => [options.phase.current(), options.current(), options.large.image.value], sync, {
|
|
34
|
+
flush: 'post',
|
|
35
|
+
})
|
|
36
|
+
useResizeObserver(() => {
|
|
37
|
+
const stage = options.stage()
|
|
38
|
+
if (!stage) return []
|
|
39
|
+
const chrome = stage.querySelector<HTMLElement>('[data-hn-chrome]')
|
|
40
|
+
return chrome ? [stage, chrome] : [stage]
|
|
41
|
+
}, sync)
|
|
42
|
+
|
|
43
|
+
return { learn }
|
|
44
|
+
}
|