@bagelink/vue 1.16.0 → 1.17.6
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/assetTransport.d.ts +28 -0
- package/dist/assetTransport.d.ts.map +1 -0
- package/dist/assetTransport.test.d.ts +2 -0
- package/dist/assetTransport.test.d.ts.map +1 -0
- package/dist/assets.d.ts +65 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.test.d.ts +2 -0
- package/dist/assets.test.d.ts.map +1 -0
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +7 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
- package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
- package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
- package/dist/components/lightbox/index.d.ts.map +1 -1
- package/dist/components/lightbox/lightbox.types.d.ts +7 -0
- package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/useImageSrc.d.ts +8 -4
- package/dist/composables/useImageSrc.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +13 -6
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +6 -0
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +24167 -23417
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +5 -1
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +75 -0
- package/src/assetTransport.ts +98 -0
- package/src/assets.test.ts +82 -0
- package/src/assets.ts +162 -0
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/Image.vue +19 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +1 -1
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
- package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
- package/src/components/index.ts +3 -1
- package/src/components/lightbox/AssetPreview.vue +16 -0
- package/src/components/lightbox/Lightbox.vue +8 -3
- package/src/components/lightbox/LightboxImage.vue +4 -3
- package/src/components/lightbox/index.ts +29 -4
- package/src/components/lightbox/lightbox.types.ts +5 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/useImageSrc.ts +47 -37
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/form-flow.ts +31 -7
- package/src/form-flow/schema-fields.ts +28 -1
- package/src/index.ts +3 -0
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +5 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { IconType, Option } from '../../../index'
|
|
3
|
-
import type { AlignedPlacement } from '../../Dropdown.
|
|
3
|
+
import type { AlignedPlacement } from '../../Dropdown.types'
|
|
4
4
|
import { Btn, Card, Skeleton, Dropdown, Icon, TextInput, useSearch, useI18n, resolveI18n } from '../../../index'
|
|
5
5
|
import { computed, onMounted, ref, useId, watch } from 'vue'
|
|
6
6
|
import { getOptionIcon, getOptionLabel, getOptionSubtitle, getOptionValue } from '../../../utils/options'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onScopeDispose, ref } from 'vue'
|
|
3
|
+
import { Btn, Image, Card, Loading } from '../../../../index'
|
|
4
|
+
import { useAssetUpload, type AssetReference } from '../../../../assets'
|
|
5
|
+
import { useFileUpload } from './useFileUpload'
|
|
6
|
+
import { useFileDropzone } from '../../../file-drop/useFileDropzone'
|
|
7
|
+
import type { UploadInputProps } from '../../../../types/BagelForm'
|
|
8
|
+
|
|
9
|
+
const props = defineProps<UploadInputProps>()
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
'update:modelValue': [value: AssetReference | AssetReference[] | undefined]
|
|
12
|
+
addFileStart: []
|
|
13
|
+
}>()
|
|
14
|
+
const { upload, uploading, cancel } = useAssetUpload()
|
|
15
|
+
const failure = ref('')
|
|
16
|
+
const values = computed<AssetReference[]>(() => [props.modelValue].flat().filter(
|
|
17
|
+
(value): value is AssetReference => !!value && typeof value === 'object' && 'asset_id' in value,
|
|
18
|
+
))
|
|
19
|
+
const disabled = computed(() => props.disabled || props.readonly || uploading.value)
|
|
20
|
+
function output(items: AssetReference[]) {
|
|
21
|
+
emit('update:modelValue', props.multiple ? items : items[0])
|
|
22
|
+
}
|
|
23
|
+
const { browse, addFile, fileQueue } = useFileUpload({
|
|
24
|
+
multiple: props.multiple, accept: props.accept, capture: props.capture,
|
|
25
|
+
onFileQueued: async queued => {
|
|
26
|
+
if (disabled.value) {
|
|
27
|
+
for (const item of queued) if (item.url) URL.revokeObjectURL(item.url)
|
|
28
|
+
fileQueue.value = []
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
failure.value = ''
|
|
32
|
+
emit('addFileStart')
|
|
33
|
+
const next = props.multiple ? [...values.value] : []
|
|
34
|
+
try {
|
|
35
|
+
for (const item of props.multiple ? queued : queued.slice(0, 1)) {
|
|
36
|
+
next.push(await upload(item.file, { visibility: props.assetVisibility }))
|
|
37
|
+
output([...next])
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) failure.value = 'Upload failed. Please try again.'
|
|
41
|
+
} finally {
|
|
42
|
+
for (const item of queued) if (item.url) URL.revokeObjectURL(item.url)
|
|
43
|
+
fileQueue.value = []
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
const { handlers } = useFileDropzone({ disabled, onDrop: files => addFile(files) })
|
|
48
|
+
onScopeDispose(() => {
|
|
49
|
+
for (const item of fileQueue.value) if (item.url) URL.revokeObjectURL(item.url)
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<Card outline class="flex column gap-1 p-1" v-on="handlers">
|
|
55
|
+
<label v-if="label">{{ label }}</label>
|
|
56
|
+
<div v-for="(value, index) in values" :key="value.asset_id" class="flex gap-1 align-items-center">
|
|
57
|
+
<Image v-if="accept?.startsWith('image/')" :src="value" width="80" height="80" />
|
|
58
|
+
<span class="ellipsis-1">{{ value.asset_id }}</span>
|
|
59
|
+
<Btn v-if="!readonly" flat icon="close" :disabled="disabled" aria-label="Remove attachment"
|
|
60
|
+
@click="output(values.filter((_, i) => i !== index))" />
|
|
61
|
+
</div>
|
|
62
|
+
<Loading v-if="uploading" />
|
|
63
|
+
<Btn v-if="!readonly" :disabled="disabled" icon="upload" :value="btnPlaceholder || 'Upload file'" @click="browse()" />
|
|
64
|
+
<Btn v-if="uploading" flat value="Cancel" @click="cancel" />
|
|
65
|
+
<p v-if="failure || error" role="alert">{{ failure || error }}</p>
|
|
66
|
+
</Card>
|
|
67
|
+
</template>
|
|
@@ -5,6 +5,7 @@ import { watch, computed, ref, useSlots } from 'vue'
|
|
|
5
5
|
import { useFileDropzone } from '../../../file-drop/useFileDropzone'
|
|
6
6
|
import { useFileUpload } from './useFileUpload'
|
|
7
7
|
import { useBagelInputShell } from '../bagelInputShell'
|
|
8
|
+
import AssetUploadInput from './AssetUploadInput.vue'
|
|
8
9
|
|
|
9
10
|
const props = withDefaults(defineProps<UploadInputProps>(), {
|
|
10
11
|
height: '215px',
|
|
@@ -32,7 +33,7 @@ const {
|
|
|
32
33
|
pk,
|
|
33
34
|
} = useFileUpload({
|
|
34
35
|
disabled: props.disabled,
|
|
35
|
-
dirPath: props.dirPath,
|
|
36
|
+
dirPath: props.assetMode ? undefined : props.dirPath,
|
|
36
37
|
multiple: props.multiple,
|
|
37
38
|
accept: props.accept,
|
|
38
39
|
capture: props.capture,
|
|
@@ -121,7 +122,10 @@ function fileName(pathKey: string) {
|
|
|
121
122
|
</script>
|
|
122
123
|
|
|
123
124
|
<template>
|
|
125
|
+
<AssetUploadInput v-if="assetMode" v-bind="props"
|
|
126
|
+
@update:model-value="emit('update:modelValue', $event)" @add-file-start="emit('addFileStart')" />
|
|
124
127
|
<div
|
|
128
|
+
v-else
|
|
125
129
|
class="bagel-input" :class="[shellClass, { 'has-error': !!error, 'has-value': hasValue }]"
|
|
126
130
|
:style="shellStyle" :data-field="name || id"
|
|
127
131
|
>
|
package/src/components/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { default as DragOver } from './DragOver.vue'
|
|
|
19
19
|
export { useFileDropzone } from './file-drop/useFileDropzone'
|
|
20
20
|
export type { UseFileDropzoneOptions } from './file-drop/useFileDropzone'
|
|
21
21
|
export { default as Dropdown } from './Dropdown.vue'
|
|
22
|
+
export type { AlignedPlacement, AnchorPoint, DropdownItem } from './Dropdown.types'
|
|
23
|
+
export { default as ContextMenu } from './ContextMenu.vue'
|
|
22
24
|
export { default as EmptyState } from './EmptyState.vue'
|
|
23
25
|
export { default as FieldSetVue } from './FieldSetVue.vue'
|
|
24
26
|
export { default as KanbanBoard } from './kanban/KanbanBoard.vue'
|
|
@@ -30,7 +32,7 @@ export { default as FlipBook } from './FlipBook.vue'
|
|
|
30
32
|
export { default as FloatingPanel } from './FloatingPanel.vue'
|
|
31
33
|
export * from './form'
|
|
32
34
|
export { default as Icon } from './Icon/Icon.vue'
|
|
33
|
-
export {
|
|
35
|
+
export { FONT_AWESOME_BRANDS_ICONS, MATERIAL_ICONS } from './Icon/constants'
|
|
34
36
|
export { default as IframeVue } from './IframeVue.vue'
|
|
35
37
|
export { default as Image } from './Image.vue'
|
|
36
38
|
export * from './layout'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAssetDelivery, type AssetReference } from '../../assets'
|
|
3
|
+
import { Btn, Image } from '../../index'
|
|
4
|
+
const props = defineProps<{ asset: AssetReference, type: string, name?: string }>()
|
|
5
|
+
const { url, error, onError } = useAssetDelivery(() => props.asset)
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div class="flex column gap-1">
|
|
10
|
+
<Image v-if="type === 'image'" :src="asset" />
|
|
11
|
+
<video v-else-if="type === 'video' && url" :src="url" controls @error="onError" />
|
|
12
|
+
<iframe v-else-if="type === 'pdf' && url" :src="url" :title="name || 'PDF preview'" referrerpolicy="no-referrer" />
|
|
13
|
+
<Btn v-if="url" :href="url" target="_blank" rel="noopener noreferrer" value="Open / download" />
|
|
14
|
+
<p v-if="error" role="alert">File preview unavailable.</p>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
@@ -4,6 +4,7 @@ import type { LightboxItem } from './lightbox.types'
|
|
|
4
4
|
import { BglVideo, Btn, Icon, Zoomer, Image, normalizeURL, Swiper, downloadFile, useEscapeKey } from '../../index'
|
|
5
5
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
6
6
|
import LightboxImage from './LightboxImage.vue'
|
|
7
|
+
import AssetPreview from './AssetPreview.vue'
|
|
7
8
|
|
|
8
9
|
const isOpen = ref(false)
|
|
9
10
|
const dialogRef = ref<HTMLDialogElement | null>(null)
|
|
@@ -29,7 +30,10 @@ function open(item: LightboxItem, groupItems?: LightboxItem[]) {
|
|
|
29
30
|
&& groupItem.src === item.src
|
|
30
31
|
const hasPathMatch = item.pathKey !== undefined && item.pathKey !== null && item.pathKey !== ''
|
|
31
32
|
&& groupItem.pathKey === item.pathKey
|
|
32
|
-
|
|
33
|
+
const hasAssetMatch = item.asset && groupItem.asset
|
|
34
|
+
&& item.asset.asset_id === groupItem.asset.asset_id
|
|
35
|
+
&& item.asset.version_id === groupItem.asset.version_id
|
|
36
|
+
return hasSrcMatch || hasPathMatch || hasAssetMatch
|
|
33
37
|
})
|
|
34
38
|
if (currentIndex.value === -1) currentIndex.value = 0
|
|
35
39
|
zoom.value = 1
|
|
@@ -98,9 +102,10 @@ defineExpose({ open, close })
|
|
|
98
102
|
<Zoomer v-if="item.type === 'image'" v-model:zoom="zoom" :disabled="!item?.enableZoom"
|
|
99
103
|
:mouse-wheel-to-zoom="false" :double-click-to-zoom="true" :max-scale="5" :min-scale="1"
|
|
100
104
|
:aspect-ratio="0" :limit-translation="true" @click.stop>
|
|
101
|
-
<LightboxImage :src="item?.src" />
|
|
105
|
+
<LightboxImage :src="item?.asset ?? item?.src ?? item?.pathKey" />
|
|
102
106
|
</Zoomer>
|
|
103
107
|
|
|
108
|
+
<AssetPreview v-else-if="item.asset" :asset="item.asset" :type="item.type" :name="item.name" />
|
|
104
109
|
<BglVideo v-else-if="item?.type === 'video' && item?.src" :src="item?.src" autoplay controls
|
|
105
110
|
class="lightbox-video" />
|
|
106
111
|
|
|
@@ -143,7 +148,7 @@ defineExpose({ open, close })
|
|
|
143
148
|
|
|
144
149
|
<div v-if="group && group.length > 1" class="lightbox-thumbnails" @click.stop>
|
|
145
150
|
<template v-for="(item, index) in group" :key="index">
|
|
146
|
-
<Image v-if="item.type === 'image'" class="thumbnail" :src="item.src" alt=""
|
|
151
|
+
<Image v-if="item.type === 'image'" class="thumbnail" :src="item.asset ?? item.src ?? item.pathKey" alt=""
|
|
147
152
|
:class="{ active: currentIndex === index }" @click.stop="selectItem(index)" />
|
|
148
153
|
<Icon v-else class="thumbnail thumbnail-icon" icon="description"
|
|
149
154
|
:class="{ active: currentIndex === index }" @click.stop="selectItem(index)" />
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// so the natural dimensions drive layout — <Image>'s framed mode renders an
|
|
5
5
|
// absolute-fill img that collapses the zoomer to 0×0.
|
|
6
6
|
import { useImageSrc } from '../../index'
|
|
7
|
+
import type { AssetReference } from '../../assets'
|
|
7
8
|
|
|
8
|
-
const props = defineProps<{ src?: string }>()
|
|
9
|
-
const { imageSrc } = useImageSrc(() => props.src)
|
|
9
|
+
const props = defineProps<{ src?: string | AssetReference }>()
|
|
10
|
+
const { imageSrc, onError } = useImageSrc(() => props.src)
|
|
10
11
|
</script>
|
|
11
12
|
|
|
12
13
|
<template>
|
|
13
|
-
<img :draggable="false" :src="imageSrc" alt="Preview" class="lightbox-image">
|
|
14
|
+
<img :draggable="false" :src="imageSrc" alt="Preview" class="lightbox-image" @error="onError">
|
|
14
15
|
</template>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { App, Directive, DirectiveBinding, Plugin } from 'vue'
|
|
2
|
+
import { ASSET_CLIENT_KEY, type AssetClient } from '../../assets'
|
|
2
3
|
|
|
3
4
|
import type { LightboxItem } from './lightbox.types'
|
|
4
5
|
import { IMAGE_FORMATS_REGEXP, pathKeyToURL, VIDEO_FORMATS_REGEXP } from '../../index'
|
|
5
|
-
import { createApp } from 'vue'
|
|
6
|
+
import { createApp, inject } from 'vue'
|
|
6
7
|
import Lightbox from './Lightbox.vue'
|
|
7
8
|
|
|
8
9
|
type OpenFunction = (item: LightboxItem, groupItems?: LightboxItem[]) => void
|
|
@@ -36,7 +37,9 @@ const lightboxDirective: Directive = {
|
|
|
36
37
|
function groupHandler(item: LightboxItem) {
|
|
37
38
|
if (item.group) {
|
|
38
39
|
if (!groups[item.group]) { groups[item.group] = [] }
|
|
39
|
-
const currentIndex = groups[item.group].findIndex(i =>
|
|
40
|
+
const currentIndex = groups[item.group].findIndex(i => item.asset
|
|
41
|
+
? i.asset?.asset_id === item.asset.asset_id && i.asset?.version_id === item.asset.version_id
|
|
42
|
+
: i.src === item.src)
|
|
40
43
|
if (currentIndex === -1) { groups[item.group].push(item) }
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -44,7 +47,7 @@ function groupHandler(item: LightboxItem) {
|
|
|
44
47
|
function openClickHandler(e: MouseEvent, el: HTMLElement, binding: DirectiveBinding) {
|
|
45
48
|
e.stopPropagation()
|
|
46
49
|
const item = bindingToItem(binding, el)
|
|
47
|
-
const lightboxInstance = getLightboxInstance()
|
|
50
|
+
const lightboxInstance = getLightboxInstance(binding)
|
|
48
51
|
if (!lightboxInstance || !lightboxInstance.open) { return }
|
|
49
52
|
const open = lightboxInstance.open as OpenFunction
|
|
50
53
|
const group = item && item.group ? groups[item.group] : undefined
|
|
@@ -52,6 +55,9 @@ function openClickHandler(e: MouseEvent, el: HTMLElement, binding: DirectiveBind
|
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
function bindingToItem(binding: DirectiveBinding, el: HTMLElement): LightboxItem {
|
|
58
|
+
if (binding.value?.asset) {
|
|
59
|
+
return { ...binding.value, src: undefined, pathKey: undefined, type: binding.value.type || 'image', name: binding.value.name || '' }
|
|
60
|
+
}
|
|
55
61
|
let { group, src, type, name, thumbnail, enableZoom, openFile, download, pathKey } = binding.value || {}
|
|
56
62
|
src = pathKeyToURL(src || pathKey) || binding.value || el.getAttribute('src') || ''
|
|
57
63
|
type = type || determineFileType(src)
|
|
@@ -82,10 +88,25 @@ function determineFileType(url: any): string {
|
|
|
82
88
|
return extension ?? 'unknown'
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
const instances = new WeakMap<App, InstanceType<typeof Lightbox>>()
|
|
85
92
|
let lightboxInstance: InstanceType<typeof Lightbox> | undefined
|
|
86
93
|
|
|
87
|
-
function getLightboxInstance(): InstanceType<typeof Lightbox> | undefined {
|
|
94
|
+
function getLightboxInstance(binding: DirectiveBinding): InstanceType<typeof Lightbox> | undefined {
|
|
88
95
|
if (typeof document === 'undefined') { return undefined }
|
|
96
|
+
const owner = binding.instance?.$?.appContext.app
|
|
97
|
+
if (owner) {
|
|
98
|
+
const existing = instances.get(owner)
|
|
99
|
+
if (existing) return existing
|
|
100
|
+
const element = document.createElement('div')
|
|
101
|
+
document.body.prepend(element)
|
|
102
|
+
const child = createApp(Lightbox)
|
|
103
|
+
const client = owner.runWithContext(() => injectAssetClient())
|
|
104
|
+
if (client) child.provide(ASSET_CLIENT_KEY, client)
|
|
105
|
+
const instance = child.mount(element) as InstanceType<typeof Lightbox>
|
|
106
|
+
instances.set(owner, instance)
|
|
107
|
+
owner.onUnmount(() => { child.unmount(); element.remove(); instances.delete(owner) })
|
|
108
|
+
return instance
|
|
109
|
+
}
|
|
89
110
|
if (!lightboxInstance) {
|
|
90
111
|
const lightboxEl = document.createElement('div')
|
|
91
112
|
document.body.prepend(lightboxEl)
|
|
@@ -95,6 +116,10 @@ function getLightboxInstance(): InstanceType<typeof Lightbox> | undefined {
|
|
|
95
116
|
return lightboxInstance
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
function injectAssetClient(): AssetClient | undefined {
|
|
120
|
+
return inject(ASSET_CLIENT_KEY, undefined)
|
|
121
|
+
}
|
|
122
|
+
|
|
98
123
|
const lightboxPlugin: Plugin = {
|
|
99
124
|
install(app: App) {
|
|
100
125
|
app.directive('lightbox', lightboxDirective)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AssetReference } from '../../assets'
|
|
2
|
+
|
|
3
|
+
export type XORSrcPath = { src: string, pathKey?: never, asset?: never }
|
|
4
|
+
| { src?: never, pathKey: string, asset?: never }
|
|
5
|
+
| { src?: never, pathKey?: never, asset: AssetReference }
|
|
2
6
|
|
|
3
7
|
export type LightboxItem = XORSrcPath & {
|
|
4
8
|
type: string
|
package/src/composables/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { useImageSrc } from './useImageSrc'
|
|
|
16
16
|
export { useLocalStore } from './useLocalStore'
|
|
17
17
|
export { usePolling } from './usePolling'
|
|
18
18
|
export { createPointerSession, usePointerSession } from './usePointerSession'
|
|
19
|
-
export type { PointerSessionOptions } from './usePointerSession'
|
|
19
|
+
export type { PointerSessionMovePayload, PointerSessionOptions } from './usePointerSession'
|
|
20
20
|
export { resizeRect, snapGridDelta } from './resizeGeometry'
|
|
21
21
|
export type { ResizeRect } from './resizeGeometry'
|
|
22
22
|
export { usePrefersReducedMotion } from './usePrefersReducedMotion'
|
|
@@ -29,18 +29,14 @@ export function useBoundedRectDrag() {
|
|
|
29
29
|
if (!Number.isFinite(scale) || scale <= 0) return false
|
|
30
30
|
const initial = { ...options.rect }
|
|
31
31
|
const bounds = { ...options.bounds }
|
|
32
|
-
const startX = event.clientX
|
|
33
|
-
const startY = event.clientY
|
|
34
32
|
let moved = false
|
|
35
33
|
let previewed = false
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!moved && Math.hypot(clientDx, clientDy) < (options.threshold ?? 0)) return
|
|
40
|
-
if (!moved && clientDx === 0 && clientDy === 0) return
|
|
34
|
+
function applyMove(dxClient: number, dyClient: number) {
|
|
35
|
+
if (!moved && Math.hypot(dxClient, dyClient) < (options.threshold ?? 0)) return
|
|
36
|
+
if (!moved && dxClient === 0 && dyClient === 0) return
|
|
41
37
|
moved = true
|
|
42
|
-
const dx =
|
|
43
|
-
const dy =
|
|
38
|
+
const dx = dxClient / scale
|
|
39
|
+
const dy = dyClient / scale
|
|
44
40
|
const rect = options.mode === 'resize'
|
|
45
41
|
? resizeRect(initial, { x: dx, y: dy }, 'se', bounds, { width: options.minWidth ?? 0, height: options.minHeight ?? 0 })
|
|
46
42
|
: {
|
|
@@ -52,9 +48,10 @@ export function useBoundedRectDrag() {
|
|
|
52
48
|
options.onUpdate(rect)
|
|
53
49
|
}
|
|
54
50
|
const started = session.start(event, {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
threshold: options.threshold,
|
|
52
|
+
onMove: (_next, { dx, dy }) => applyMove(dx, dy),
|
|
53
|
+
onEnd: (_next, { dx, dy }) => {
|
|
54
|
+
try { applyMove(dx, dy); options.onCommit?.(moved) } finally { active.value = false }
|
|
58
55
|
},
|
|
59
56
|
onCancel: () => {
|
|
60
57
|
active.value = false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MaybeRefOrGetter } from 'vue'
|
|
2
2
|
import { appendScript, awaitGlobal, normalizeURL, pathKeyToURL } from '../index'
|
|
3
|
-
import { ref, toValue, watch } from 'vue'
|
|
3
|
+
import { computed, onScopeDispose, ref, toValue, watch } from 'vue'
|
|
4
|
+
import { useAssetDelivery, type AssetDeliveryRequest } from '../assets'
|
|
4
5
|
|
|
5
6
|
declare global {
|
|
6
7
|
interface Window {
|
|
@@ -8,74 +9,76 @@ declare global {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return URL.createObjectURL(await cachedResponse.blob())
|
|
18
|
-
}
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.warn('Cache access error:', error)
|
|
21
|
-
}
|
|
22
|
-
return undefined
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function cacheImage(url: string, blob: Blob) {
|
|
26
|
-
if (!('caches' in window)) { return }
|
|
27
|
-
try {
|
|
28
|
-
const imgCache = await window.caches.open('img-cache')
|
|
29
|
-
await imgCache.put(url, new Response(blob))
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.warn('Cache write error:', error)
|
|
32
|
-
}
|
|
12
|
+
let legacyCacheCleanup: Promise<boolean> | undefined
|
|
13
|
+
/** Remove only the retired sensitive image cache, once per browser session. */
|
|
14
|
+
export function clearLegacyImageCache() {
|
|
15
|
+
if (typeof window === 'undefined' || !('caches' in window)) return
|
|
16
|
+
legacyCacheCleanup ??= window.caches.delete('img-cache').catch(() => false)
|
|
17
|
+
return legacyCacheCleanup
|
|
33
18
|
}
|
|
34
19
|
|
|
35
|
-
async function convertHeicImage(url: string): Promise<string> {
|
|
20
|
+
async function convertHeicImage(url: string, signal: AbortSignal): Promise<string> {
|
|
36
21
|
await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
|
|
37
22
|
const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
|
|
38
|
-
const response = await fetch(normalizeURL(url))
|
|
23
|
+
const response = await fetch(normalizeURL(url), { signal, cache: 'no-store' })
|
|
24
|
+
if (!response.ok) throw new Error('Image request failed')
|
|
39
25
|
const blob = await response.blob()
|
|
40
26
|
const convertedBlob = await heic2any({ blob })
|
|
41
|
-
await cacheImage(url, convertedBlob)
|
|
42
27
|
return URL.createObjectURL(convertedBlob)
|
|
43
28
|
}
|
|
44
29
|
|
|
45
30
|
/**
|
|
46
31
|
* Resolve an image source for display: turns a pathKey/URL into a usable URL via
|
|
47
32
|
* `pathKeyToURL`, and transparently converts `.heic` files to a displayable blob
|
|
48
|
-
* (
|
|
33
|
+
* (in memory only) since browsers can't render HEIC in <img>. Shared by <Image>
|
|
49
34
|
* and the lightbox so both get the same resolution + HEIC handling.
|
|
50
35
|
*
|
|
51
36
|
* @param source ref/getter of the raw src or pathKey
|
|
52
37
|
* @returns `{ imageSrc, loadingError }` reactive refs
|
|
53
38
|
*/
|
|
54
|
-
export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
|
|
39
|
+
export function useImageSrc(source: MaybeRefOrGetter<string | AssetDeliveryRequest | undefined>) {
|
|
40
|
+
void clearLegacyImageCache()
|
|
41
|
+
const asset = useAssetDelivery(() => {
|
|
42
|
+
const value = toValue(source)
|
|
43
|
+
return typeof value === 'object' ? { ...value, representation: value.representation ?? 'preview' } : undefined
|
|
44
|
+
})
|
|
55
45
|
const imageSrc = ref<string | undefined>(undefined)
|
|
56
46
|
const loadingError = ref(false)
|
|
47
|
+
let controller: AbortController | undefined
|
|
48
|
+
let generation = 0
|
|
49
|
+
let objectUrl: string | undefined
|
|
50
|
+
function cleanup() {
|
|
51
|
+
controller?.abort()
|
|
52
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
|
53
|
+
objectUrl = undefined
|
|
54
|
+
}
|
|
57
55
|
|
|
58
56
|
async function load() {
|
|
57
|
+
const current = ++generation
|
|
58
|
+
cleanup()
|
|
59
59
|
loadingError.value = false
|
|
60
|
-
|
|
60
|
+
imageSrc.value = undefined
|
|
61
|
+
const value = toValue(source)
|
|
62
|
+
if (typeof value === 'object') return
|
|
63
|
+
const url = pathKeyToURL(value)
|
|
61
64
|
if (url === undefined || url === '') {
|
|
62
65
|
imageSrc.value = undefined
|
|
63
66
|
return
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
try {
|
|
70
|
+
controller = new AbortController()
|
|
67
71
|
const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
|
|
68
72
|
if (ext === 'heic') {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
imageSrc.value = await convertHeicImage(url)
|
|
73
|
+
const converted = await convertHeicImage(url, controller.signal)
|
|
74
|
+
if (current !== generation) { URL.revokeObjectURL(converted); return }
|
|
75
|
+
objectUrl = converted
|
|
76
|
+
imageSrc.value = converted
|
|
75
77
|
} else {
|
|
76
78
|
imageSrc.value = url
|
|
77
79
|
}
|
|
78
80
|
} catch (error) {
|
|
81
|
+
if (current !== generation) return
|
|
79
82
|
console.error('Image loading error:', error)
|
|
80
83
|
loadingError.value = true
|
|
81
84
|
imageSrc.value = undefined
|
|
@@ -83,6 +86,13 @@ export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
|
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
watch(() => toValue(source), load, { immediate: true })
|
|
86
|
-
|
|
87
|
-
return {
|
|
89
|
+
onScopeDispose(() => { generation++; cleanup() })
|
|
90
|
+
return {
|
|
91
|
+
imageSrc: computed(() => typeof toValue(source) === 'object' ? asset.url.value : imageSrc.value),
|
|
92
|
+
loadingError: computed(() => typeof toValue(source) === 'object' ? asset.error.value : loadingError.value),
|
|
93
|
+
onError: () => {
|
|
94
|
+
if (typeof toValue(source) === 'object') asset.onError()
|
|
95
|
+
else loadingError.value = true
|
|
96
|
+
},
|
|
97
|
+
}
|
|
88
98
|
}
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { getCurrentScope, onScopeDispose } from 'vue'
|
|
2
2
|
|
|
3
|
+
export interface PointerSessionMovePayload {
|
|
4
|
+
dx: number
|
|
5
|
+
dy: number
|
|
6
|
+
distance: number
|
|
7
|
+
exceededThreshold: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export interface PointerSessionOptions {
|
|
4
11
|
cursor?: string
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
/** Optional distance in client pixels before `exceededThreshold` becomes true. */
|
|
13
|
+
threshold?: number
|
|
14
|
+
/** Override the target element or document when starting from a delegated listener. */
|
|
15
|
+
target?: Element | Document | null
|
|
16
|
+
onThreshold?: (event: PointerEvent, payload: PointerSessionMovePayload) => void
|
|
17
|
+
onMove: (event: PointerEvent, payload: PointerSessionMovePayload) => void
|
|
18
|
+
/** Commit receives the release coordinates and movement summary, even when no move event preceded it. */
|
|
19
|
+
onEnd?: (event: PointerEvent, payload: PointerSessionMovePayload) => void
|
|
8
20
|
onCancel?: () => void
|
|
9
21
|
}
|
|
10
22
|
|
|
@@ -14,17 +26,46 @@ export function createPointerSession() {
|
|
|
14
26
|
function cancel() { finish?.() }
|
|
15
27
|
function start(event: PointerEvent, options: PointerSessionOptions): boolean {
|
|
16
28
|
if (finish || event.button !== 0 || event.isPrimary === false) return false
|
|
17
|
-
const
|
|
18
|
-
const doc =
|
|
29
|
+
const rawTarget = (options.target ?? event.currentTarget ?? event.target) as Element | Document | null
|
|
30
|
+
const doc = rawTarget && 'ownerDocument' in rawTarget
|
|
31
|
+
? (rawTarget.ownerDocument ?? (rawTarget as unknown as Document))
|
|
32
|
+
: (rawTarget as Document | null)
|
|
33
|
+
const targetEl = rawTarget && 'ownerDocument' in rawTarget && rawTarget.ownerDocument
|
|
34
|
+
? (rawTarget as Element)
|
|
35
|
+
: null
|
|
19
36
|
const view = doc?.defaultView
|
|
20
37
|
if (!view) return false
|
|
38
|
+
|
|
21
39
|
const id = event.pointerId
|
|
40
|
+
const startX = event.clientX
|
|
41
|
+
const startY = event.clientY
|
|
42
|
+
const threshold = options.threshold ?? 0
|
|
43
|
+
let exceededThreshold = threshold <= 0
|
|
44
|
+
|
|
22
45
|
const style = doc.body?.style
|
|
23
46
|
const cursor = style?.cursor ?? ''
|
|
24
47
|
const selection = style?.userSelect ?? ''
|
|
48
|
+
|
|
49
|
+
function buildPayload(next: PointerEvent): PointerSessionMovePayload {
|
|
50
|
+
const dx = (next.clientX ?? startX) - startX
|
|
51
|
+
const dy = (next.clientY ?? startY) - startY
|
|
52
|
+
const distance = Math.hypot(dx, dy)
|
|
53
|
+
if (!exceededThreshold && distance > threshold) {
|
|
54
|
+
exceededThreshold = true
|
|
55
|
+
options.onThreshold?.(next, { dx, dy, distance, exceededThreshold: true })
|
|
56
|
+
}
|
|
57
|
+
return { dx, dy, distance, exceededThreshold }
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
function move(next: PointerEvent) {
|
|
26
61
|
if (next.pointerId !== id) return
|
|
27
|
-
try {
|
|
62
|
+
try {
|
|
63
|
+
const payload = buildPayload(next)
|
|
64
|
+
options.onMove(next, payload)
|
|
65
|
+
} catch (error) {
|
|
66
|
+
cancel()
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
28
69
|
}
|
|
29
70
|
function up(next: PointerEvent) { if (next.pointerId === id) finish?.(next) }
|
|
30
71
|
function interrupted(next: PointerEvent) { if (next.pointerId === id) cancel() }
|
|
@@ -38,13 +79,17 @@ export function createPointerSession() {
|
|
|
38
79
|
view!.removeEventListener('pointercancel', interrupted)
|
|
39
80
|
view!.removeEventListener('blur', cancel)
|
|
40
81
|
view!.removeEventListener('keydown', keydown)
|
|
41
|
-
|
|
82
|
+
targetEl?.removeEventListener('lostpointercapture', interrupted as EventListener)
|
|
42
83
|
try {
|
|
43
|
-
if (
|
|
84
|
+
if (targetEl?.hasPointerCapture?.(id)) targetEl.releasePointerCapture(id)
|
|
44
85
|
} catch { /* Detached targets may already have lost capture. */ }
|
|
45
86
|
if (style) { style.cursor = cursor; style.userSelect = selection }
|
|
46
|
-
if (next)
|
|
47
|
-
|
|
87
|
+
if (next) {
|
|
88
|
+
const payload = buildPayload(next)
|
|
89
|
+
options.onEnd?.(next, payload)
|
|
90
|
+
} else {
|
|
91
|
+
options.onCancel?.()
|
|
92
|
+
}
|
|
48
93
|
}
|
|
49
94
|
event.preventDefault()
|
|
50
95
|
if (style) {
|
|
@@ -56,8 +101,8 @@ export function createPointerSession() {
|
|
|
56
101
|
view.addEventListener('pointercancel', interrupted)
|
|
57
102
|
view.addEventListener('blur', cancel)
|
|
58
103
|
view.addEventListener('keydown', keydown)
|
|
59
|
-
|
|
60
|
-
try {
|
|
104
|
+
targetEl?.addEventListener('lostpointercapture', interrupted as EventListener)
|
|
105
|
+
try { targetEl?.setPointerCapture?.(id) } catch { /* Window listeners remain a capture fallback. */ }
|
|
61
106
|
return true
|
|
62
107
|
}
|
|
63
108
|
return { start, cancel }
|
|
@@ -47,8 +47,8 @@ function initResize(el: HTMLElement, binding: { value?: ResizeOptions }) {
|
|
|
47
47
|
if (options.maxHeight !== undefined) target.setAttribute('aria-valuemax', String(options.maxHeight))
|
|
48
48
|
target.setAttribute('aria-valuenow', String(el.offsetHeight))
|
|
49
49
|
|
|
50
|
-
function update(clientY: number,
|
|
51
|
-
const event = { clientY, deltaY
|
|
50
|
+
function update(clientY: number, deltaY: number, initialHeight: number): ResizeEvent {
|
|
51
|
+
const event = { clientY, deltaY, initialHeight }
|
|
52
52
|
const proposed = initialHeight + event.deltaY
|
|
53
53
|
const snapped = options.gridSize && options.gridSize > 0 ? Math.round(proposed / options.gridSize) * options.gridSize : proposed
|
|
54
54
|
const height = Math.min(options.maxHeight ?? Infinity, Math.max(options.minHeight ?? 0, snapped))
|
|
@@ -61,16 +61,15 @@ function initResize(el: HTMLElement, binding: { value?: ResizeOptions }) {
|
|
|
61
61
|
function handleStart(event: PointerEvent) {
|
|
62
62
|
if (event.button !== 0 || event.isPrimary === false) return
|
|
63
63
|
pointer.cancel()
|
|
64
|
-
const startY = event.clientY
|
|
65
64
|
const initialHeight = el.offsetHeight
|
|
66
65
|
const originalHeight = el.style.height
|
|
67
66
|
const originalValue = target.getAttribute('aria-valuenow')
|
|
68
67
|
options.onStart?.(event)
|
|
69
68
|
pointer.start(event, {
|
|
70
69
|
cursor: 'ns-resize',
|
|
71
|
-
onMove: e => update(e.clientY,
|
|
72
|
-
onEnd(e) {
|
|
73
|
-
const result = update(e.clientY,
|
|
70
|
+
onMove: (e, { dy }) => update(e.clientY, dy, initialHeight),
|
|
71
|
+
onEnd(e, { dy }) {
|
|
72
|
+
const result = update(e.clientY, dy, initialHeight)
|
|
74
73
|
options.onEnd?.(result)
|
|
75
74
|
},
|
|
76
75
|
onCancel() {
|
|
@@ -92,7 +91,7 @@ function initResize(el: HTMLElement, binding: { value?: ResizeOptions }) {
|
|
|
92
91
|
: event.key === 'End' ? options.maxHeight!
|
|
93
92
|
: initialHeight + (event.key === 'ArrowUp' ? -step : step)
|
|
94
93
|
options.onStart?.(event)
|
|
95
|
-
const result = update(height - initialHeight,
|
|
94
|
+
const result = update(height - initialHeight, height - initialHeight, initialHeight)
|
|
96
95
|
options.onEnd?.(result)
|
|
97
96
|
}
|
|
98
97
|
|