@volverjs/ui-vue 0.0.10-beta.12 → 0.0.10-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/auto-imports.d.ts +1 -0
- package/dist/components/VvInputFile/VvInputFile.es.js +1529 -0
- package/dist/components/VvInputFile/VvInputFile.umd.js +1 -0
- package/dist/components/VvInputFile/VvInputFile.vue.d.ts +141 -0
- package/dist/components/VvInputFile/index.d.ts +52 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.es.js +633 -310
- package/dist/components/index.umd.js +1 -1
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.es.js +77 -1
- package/dist/composables/index.umd.js +1 -1
- package/dist/composables/useBlurhash.d.ts +7 -0
- package/dist/icons.es.js +3 -3
- package/dist/icons.umd.js +1 -1
- package/dist/stories/AlertGroup/AlertGroupWithComposable.stories.d.ts +1 -1
- package/dist/stories/Blurhash/BlurhashComposable.stories.d.ts +4 -0
- package/dist/stories/InputFile/InputFile.settings.d.ts +56 -0
- package/dist/stories/InputFile/InputFile.stories.d.ts +12 -0
- package/dist/stories/InputFile/InputFileModifiers.stories.d.ts +9 -0
- package/dist/stories/InputFile/InputFileSlots.stories.d.ts +6 -0
- package/dist/types/blurhash.d.ts +12 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/input-file.d.ts +14 -0
- package/dist/workers/blurhash.d.ts +1 -0
- package/package.json +13 -1
- package/src/assets/icons/detailed.json +1 -1
- package/src/assets/icons/normal.json +1 -1
- package/src/assets/icons/simple.json +1 -1
- package/src/components/VvInputFile/VvInputFile.vue +274 -0
- package/src/components/VvInputFile/index.ts +36 -0
- package/src/components/index.ts +1 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useBlurhash.ts +76 -0
- package/src/stories/AlertGroup/AlertGroupWithComposable.stories.ts +2 -2
- package/src/stories/Blurhash/BlurhashComposable.stories.ts +195 -0
- package/src/stories/InputFile/InputFile.settings.ts +36 -0
- package/src/stories/InputFile/InputFile.stories.ts +90 -0
- package/src/stories/InputFile/InputFileModifiers.stories.ts +51 -0
- package/src/stories/InputFile/InputFileSlots.stories.ts +25 -0
- package/src/types/blurhash.ts +21 -0
- package/src/types/index.ts +2 -0
- package/src/types/input-file.ts +16 -0
- package/src/workers/blurhash.ts +9 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default {
|
|
3
|
+
name: 'VvInputFile',
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { useVModel } from '@vueuse/core'
|
|
9
|
+
import type { UploadedFile } from '../../types'
|
|
10
|
+
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
11
|
+
import VvButton from '../VvButton/VvButton.vue'
|
|
12
|
+
import VvIcon from '../VvIcon/VvIcon.vue'
|
|
13
|
+
import HintSlotFactory from '../common/HintSlot'
|
|
14
|
+
import { VvInputFileProps, type VvInputFileEvents } from '.'
|
|
15
|
+
|
|
16
|
+
// props, emit, slots and attrs
|
|
17
|
+
const props = defineProps(VvInputFileProps)
|
|
18
|
+
const emit = defineEmits<VvInputFileEvents>()
|
|
19
|
+
const slots = useSlots()
|
|
20
|
+
|
|
21
|
+
// props merged with volver defaults (now only for labels)
|
|
22
|
+
const propsDefaults = useDefaults<typeof VvInputFileProps>(
|
|
23
|
+
'VvInputFile',
|
|
24
|
+
VvInputFileProps,
|
|
25
|
+
props,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const { modifiers, id } = toRefs(props)
|
|
29
|
+
|
|
30
|
+
const hasId = useUniqueId(id)
|
|
31
|
+
const hasHintId = computed(() => `${hasId.value}-hint`)
|
|
32
|
+
|
|
33
|
+
// styles
|
|
34
|
+
const bemCssClasses = useModifiers(
|
|
35
|
+
'vv-input-file',
|
|
36
|
+
modifiers,
|
|
37
|
+
computed(() => ({
|
|
38
|
+
dragging: isDragging.value,
|
|
39
|
+
loading: props.loading,
|
|
40
|
+
valid: props.valid === true,
|
|
41
|
+
invalid: props.invalid === true,
|
|
42
|
+
'icon-before': !!props.iconLeft,
|
|
43
|
+
'icon-after': !!props.iconRight,
|
|
44
|
+
})),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
HintSlot,
|
|
49
|
+
hasHintLabelOrSlot,
|
|
50
|
+
hasInvalidLabelOrSlot,
|
|
51
|
+
hintSlotScope,
|
|
52
|
+
} = HintSlotFactory(propsDefaults, slots)
|
|
53
|
+
|
|
54
|
+
const localModelValue = useVModel(props, 'modelValue', emit)
|
|
55
|
+
const files = computed(() => {
|
|
56
|
+
if (
|
|
57
|
+
!localModelValue.value ||
|
|
58
|
+
(!Array.isArray(localModelValue.value) &&
|
|
59
|
+
!(localModelValue.value as File)?.name)
|
|
60
|
+
) {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
return Array.isArray(localModelValue.value)
|
|
64
|
+
? localModelValue.value
|
|
65
|
+
: [localModelValue.value]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const hasMax = computed(() => {
|
|
69
|
+
return typeof props.max === 'string' ? parseInt(props.max) : props.max
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const hasDropArea = computed(() => {
|
|
73
|
+
return modifiers?.value?.includes('drop-area')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const isMultiple = computed(() => {
|
|
77
|
+
if (!props.multiple) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
if (!hasMax.value) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
return hasMax.value - files.value.length > 1
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const isDragging = ref(false)
|
|
87
|
+
|
|
88
|
+
const inputEl = ref<HTMLInputElement>()
|
|
89
|
+
const onDragenter = () => {
|
|
90
|
+
isDragging.value = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const onDragleave = () => {
|
|
94
|
+
isDragging.value = false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const onDrop = (event: DragEvent) => {
|
|
98
|
+
if (!event.dataTransfer?.files) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
isDragging.value = false
|
|
102
|
+
addFiles(event.dataTransfer?.files)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const onChange = () => {
|
|
106
|
+
if (!inputEl.value?.files) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
addFiles(inputEl.value.files)
|
|
110
|
+
inputEl.value.value = ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const addFiles = (uploadedFiles: FileList) => {
|
|
114
|
+
if (!props.multiple) {
|
|
115
|
+
if (Array.isArray(localModelValue.value)) {
|
|
116
|
+
localModelValue.value = [...uploadedFiles]
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
localModelValue.value = uploadedFiles[0]
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
let toReturn: (File | UploadedFile)[] = []
|
|
123
|
+
if (!Array.isArray(localModelValue.value) && localModelValue.value) {
|
|
124
|
+
toReturn = [localModelValue.value]
|
|
125
|
+
} else {
|
|
126
|
+
toReturn =
|
|
127
|
+
localModelValue.value && Array.isArray(localModelValue.value)
|
|
128
|
+
? [...localModelValue.value]
|
|
129
|
+
: toReturn
|
|
130
|
+
}
|
|
131
|
+
for (const file of uploadedFiles) {
|
|
132
|
+
if (hasMax.value && toReturn.length > hasMax.value) {
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
toReturn.push(file)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
localModelValue.value = toReturn
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const onClick = () => {
|
|
142
|
+
if (!inputEl.value) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
inputEl.value.click()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const onClickRemoveFile = (index: number) => {
|
|
149
|
+
if (!Array.isArray(localModelValue.value)) {
|
|
150
|
+
localModelValue.value = undefined
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const toReturn = [...localModelValue.value]
|
|
154
|
+
toReturn.splice(index, 1)
|
|
155
|
+
localModelValue.value = toReturn
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const previewSrc = computed(() => {
|
|
159
|
+
if (files.value.length === 0) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if (files.value[0] instanceof File) {
|
|
163
|
+
return URL.createObjectURL(files.value[0])
|
|
164
|
+
}
|
|
165
|
+
return files.value[0].url
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
onBeforeUnmount(() => {
|
|
169
|
+
if (previewSrc.value) {
|
|
170
|
+
URL.revokeObjectURL(previewSrc.value)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const sizeInKiB = (size?: number) => {
|
|
175
|
+
if (!size) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
return Math.floor(size / 1024)
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<template>
|
|
183
|
+
<div :class="bemCssClasses">
|
|
184
|
+
<label v-if="label" :for="hasId">
|
|
185
|
+
{{ label }}
|
|
186
|
+
</label>
|
|
187
|
+
<div
|
|
188
|
+
v-if="hasDropArea"
|
|
189
|
+
class="vv-input-file__drop-area"
|
|
190
|
+
@dragenter.prevent.stop="onDragenter"
|
|
191
|
+
@dragleave.prevent.stop="onDragleave"
|
|
192
|
+
@drop.prevent.stop="onDrop"
|
|
193
|
+
@dragover.prevent.stop
|
|
194
|
+
@click.stop="onClick"
|
|
195
|
+
>
|
|
196
|
+
<slot name="drop-area">
|
|
197
|
+
<VvButton
|
|
198
|
+
modifiers="action"
|
|
199
|
+
aria-label="upload"
|
|
200
|
+
:label="!previewSrc ? labelButton : undefined"
|
|
201
|
+
:class="{
|
|
202
|
+
'absolute top-8 right-8': previewSrc,
|
|
203
|
+
}"
|
|
204
|
+
:icon="!previewSrc ? 'image' : 'edit'"
|
|
205
|
+
class="z-1"
|
|
206
|
+
@click.stop="onClick"
|
|
207
|
+
/>
|
|
208
|
+
<picture class="vv-input-file__preview">
|
|
209
|
+
<img
|
|
210
|
+
v-if="previewSrc"
|
|
211
|
+
:src="previewSrc"
|
|
212
|
+
:alt="files[0].name"
|
|
213
|
+
/>
|
|
214
|
+
</picture>
|
|
215
|
+
</slot>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="vv-input-file__wrapper">
|
|
218
|
+
<VvIcon v-if="iconLeft" :name="iconLeft" />
|
|
219
|
+
<input
|
|
220
|
+
:id="hasId"
|
|
221
|
+
ref="inputEl"
|
|
222
|
+
:placeholder="placeholder"
|
|
223
|
+
:aria-describedby="hasHintLabelOrSlot ? hasHintId : undefined"
|
|
224
|
+
:aria-invalid="invalid"
|
|
225
|
+
:aria-errormessage="
|
|
226
|
+
hasInvalidLabelOrSlot ? hasHintId : undefined
|
|
227
|
+
"
|
|
228
|
+
:multiple="isMultiple"
|
|
229
|
+
:accept="accept"
|
|
230
|
+
type="file"
|
|
231
|
+
:name="name"
|
|
232
|
+
@change="onChange"
|
|
233
|
+
/>
|
|
234
|
+
<VvIcon v-if="iconRight" :name="iconRight" />
|
|
235
|
+
</div>
|
|
236
|
+
<ul class="vv-input-file__list">
|
|
237
|
+
<li
|
|
238
|
+
v-for="(file, index) in files"
|
|
239
|
+
:key="index"
|
|
240
|
+
class="vv-input-file__item"
|
|
241
|
+
>
|
|
242
|
+
<VvIcon
|
|
243
|
+
class="vv-input-file__item-icon"
|
|
244
|
+
name="akar-icons:file"
|
|
245
|
+
/>
|
|
246
|
+
<div class="vv-input-file__item-name">{{ file.name }}</div>
|
|
247
|
+
<small class="vv-input-file__item-info">
|
|
248
|
+
{{ sizeInKiB(file.size) }} KB
|
|
249
|
+
</small>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
class="vv-input-file__item-remove"
|
|
253
|
+
title="Remove"
|
|
254
|
+
aria-label="remove-file"
|
|
255
|
+
@click.stop="onClickRemoveFile(index)"
|
|
256
|
+
/>
|
|
257
|
+
</li>
|
|
258
|
+
</ul>
|
|
259
|
+
<HintSlot :id="hasHintId" class="vv-input-file__hint">
|
|
260
|
+
<template v-if="$slots.hint" #hint>
|
|
261
|
+
<slot name="hint" v-bind="hintSlotScope" />
|
|
262
|
+
</template>
|
|
263
|
+
<template v-if="$slots.loading" #loading>
|
|
264
|
+
<slot name="loading" v-bind="hintSlotScope" />
|
|
265
|
+
</template>
|
|
266
|
+
<template v-if="$slots.valid" #valid>
|
|
267
|
+
<slot name="valid" v-bind="hintSlotScope" />
|
|
268
|
+
</template>
|
|
269
|
+
<template v-if="$slots.invalid" #invalid>
|
|
270
|
+
<slot name="invalid" v-bind="hintSlotScope" />
|
|
271
|
+
</template>
|
|
272
|
+
</HintSlot>
|
|
273
|
+
</div>
|
|
274
|
+
</template>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { UploadedFile } from '@/types'
|
|
2
|
+
import {
|
|
3
|
+
ModifiersProps,
|
|
4
|
+
ValidProps,
|
|
5
|
+
InvalidProps,
|
|
6
|
+
HintProps,
|
|
7
|
+
LabelProps,
|
|
8
|
+
LoadingProps,
|
|
9
|
+
} from '../../props'
|
|
10
|
+
|
|
11
|
+
export type VvInputFileEvents = {
|
|
12
|
+
'update:modelValue': [File | undefined]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const VvInputFileProps = {
|
|
16
|
+
...ModifiersProps,
|
|
17
|
+
...ValidProps,
|
|
18
|
+
...InvalidProps,
|
|
19
|
+
...HintProps,
|
|
20
|
+
...LabelProps,
|
|
21
|
+
...LoadingProps,
|
|
22
|
+
name: { type: String },
|
|
23
|
+
id: { type: String },
|
|
24
|
+
modelValue: {
|
|
25
|
+
type: Object as PropType<File | (File | UploadedFile)[] | UploadedFile>,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
max: [Number, String],
|
|
29
|
+
labelButton: { type: String, default: 'Image' },
|
|
30
|
+
loading: Boolean,
|
|
31
|
+
accept: String,
|
|
32
|
+
placeholder: String,
|
|
33
|
+
multiple: Boolean,
|
|
34
|
+
iconLeft: String,
|
|
35
|
+
iconRight: String,
|
|
36
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -31,3 +31,4 @@ export { default as VvSelect } from './VvSelect/VvSelect.vue'
|
|
|
31
31
|
export { default as VvTab } from './VvTab/VvTab.vue'
|
|
32
32
|
export { default as VvTextarea } from './VvTextarea/VvTextarea.vue'
|
|
33
33
|
export { default as VvTooltip } from './VvTooltip/VvTooltip.vue'
|
|
34
|
+
export { default as VvInputFile } from './VvInputFile/VvInputFile.vue'
|
package/src/composables/index.ts
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { wrap } from 'comlink'
|
|
2
|
+
import pica from 'pica'
|
|
3
|
+
import type { BlurhashWorkerType } from '@/types/blurhash'
|
|
4
|
+
import BlurhashWorker from '@/workers/blurhash?worker&inline'
|
|
5
|
+
|
|
6
|
+
const remoteFunction = wrap<BlurhashWorkerType>(new BlurhashWorker())
|
|
7
|
+
|
|
8
|
+
function loadImage(src: string): Promise<CanvasImageSource> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const img = new Image()
|
|
11
|
+
img.onload = () => resolve(img)
|
|
12
|
+
img.onerror = (...args) => reject(args)
|
|
13
|
+
img.src = src
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getWidthHeightFromMaxSize = (
|
|
18
|
+
width: number,
|
|
19
|
+
height: number,
|
|
20
|
+
maxSize: number,
|
|
21
|
+
) => {
|
|
22
|
+
if (width > height) {
|
|
23
|
+
return {
|
|
24
|
+
width: maxSize,
|
|
25
|
+
height: Math.round(maxSize * (height / width)),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
width: Math.round(maxSize * (width / height)),
|
|
30
|
+
height: maxSize,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resizeImage = async (
|
|
35
|
+
image: ImageBitmap | HTMLImageElement | HTMLCanvasElement | File | Blob,
|
|
36
|
+
width: number,
|
|
37
|
+
height: number,
|
|
38
|
+
) => {
|
|
39
|
+
const resizer = new pica()
|
|
40
|
+
const canvas = document.createElement('canvas')
|
|
41
|
+
canvas.width = width
|
|
42
|
+
canvas.height = height
|
|
43
|
+
const result = await resizer.resize(image, canvas)
|
|
44
|
+
return result.getContext('2d')?.getImageData(0, 0, width, height).data
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const useBlurhash = () => {
|
|
48
|
+
async function encode(file: File) {
|
|
49
|
+
const imageUrl = URL.createObjectURL(file)
|
|
50
|
+
const image = await loadImage(imageUrl)
|
|
51
|
+
if ('width' in image && 'height' in image) {
|
|
52
|
+
const { width: newWidth, height: newHeight } =
|
|
53
|
+
getWidthHeightFromMaxSize(
|
|
54
|
+
image.width as number,
|
|
55
|
+
image.height as number,
|
|
56
|
+
32,
|
|
57
|
+
)
|
|
58
|
+
const imageData = await resizeImage(
|
|
59
|
+
image as ImageBitmap,
|
|
60
|
+
newWidth,
|
|
61
|
+
newHeight,
|
|
62
|
+
)
|
|
63
|
+
if (imageData) {
|
|
64
|
+
return remoteFunction.encode(
|
|
65
|
+
imageData,
|
|
66
|
+
newWidth,
|
|
67
|
+
newHeight,
|
|
68
|
+
4,
|
|
69
|
+
4,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { encode, decode: remoteFunction.decode, loadImage }
|
|
76
|
+
}
|
|
@@ -6,7 +6,7 @@ import { Default as DefaultStory, type Story } from './AlertGroup.stories'
|
|
|
6
6
|
import { useAlert } from '@/composables/alert/useAlert'
|
|
7
7
|
|
|
8
8
|
const meta: Meta<typeof VvAlertGroup> = {
|
|
9
|
-
title: '
|
|
9
|
+
title: 'Composables/useAlert',
|
|
10
10
|
component: VvAlertGroup,
|
|
11
11
|
args: defaultArgs,
|
|
12
12
|
argTypes,
|
|
@@ -15,7 +15,7 @@ const meta: Meta<typeof VvAlertGroup> = {
|
|
|
15
15
|
|
|
16
16
|
export default meta
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const Default: Story = {
|
|
19
19
|
...DefaultStory,
|
|
20
20
|
parameters: {
|
|
21
21
|
docs: {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import VvInputFile from '@/components/VvInputFile/VvInputFile.vue'
|
|
4
|
+
import { useBlurhash } from '@/composables/useBlurhash'
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Composables/useBlurhash',
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default meta
|
|
12
|
+
|
|
13
|
+
export const Default: StoryObj = {
|
|
14
|
+
render: (args) => ({
|
|
15
|
+
components: { VvInputFile },
|
|
16
|
+
setup() {
|
|
17
|
+
const isLoading = ref(false)
|
|
18
|
+
const { encode, decode, loadImage } = useBlurhash()
|
|
19
|
+
const file = ref({})
|
|
20
|
+
const canvas = ref()
|
|
21
|
+
const isImgLoaded = ref(false)
|
|
22
|
+
const blurhash = ref('')
|
|
23
|
+
const imageUrl = ref('')
|
|
24
|
+
const image = ref()
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
args,
|
|
28
|
+
isLoading,
|
|
29
|
+
canvas,
|
|
30
|
+
encode,
|
|
31
|
+
decode,
|
|
32
|
+
file,
|
|
33
|
+
blurhash,
|
|
34
|
+
isImgLoaded,
|
|
35
|
+
loadImage,
|
|
36
|
+
image,
|
|
37
|
+
imageUrl,
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
watch: {
|
|
41
|
+
file: {
|
|
42
|
+
immediate: true,
|
|
43
|
+
async handler(newValue) {
|
|
44
|
+
if (newValue?.size) {
|
|
45
|
+
this.imageUrl = URL.createObjectURL(newValue)
|
|
46
|
+
this.image = await this.loadImage(this.imageUrl)
|
|
47
|
+
this.blurhash = await this.encode(newValue)
|
|
48
|
+
} else {
|
|
49
|
+
this.image = null
|
|
50
|
+
this.imageUrl = ''
|
|
51
|
+
this.blurhash = ''
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
blurhash: {
|
|
56
|
+
async handler(newValue) {
|
|
57
|
+
if (this.image) {
|
|
58
|
+
const blurhashDecoded = await this.decode(
|
|
59
|
+
newValue,
|
|
60
|
+
this.image.width,
|
|
61
|
+
this.image.height,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (this.canvas) {
|
|
65
|
+
this.canvas.width = this.image.width
|
|
66
|
+
this.canvas.height = this.image.height
|
|
67
|
+
const ctx = this.canvas.getContext('2d')
|
|
68
|
+
const imageData = ctx.createImageData(
|
|
69
|
+
this.canvas.width,
|
|
70
|
+
this.canvas.height,
|
|
71
|
+
)
|
|
72
|
+
imageData.data.set(blurhashDecoded)
|
|
73
|
+
ctx.putImageData(imageData, 0, 0)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
template: /* html */ `
|
|
80
|
+
<div class="w-full grid gap-md grid-cols-3 h-150" :class="{ 'vv-skeleton': isLoading }">
|
|
81
|
+
<div class="w-150 h-150 col-span-1">
|
|
82
|
+
<div class="text-20 font-semibold mb-md">Upload image</div>
|
|
83
|
+
<vv-input-file v-model="file" name="input-file" modifiers="drop-area square hidden" accept=".gif,.jpg,.jpeg,.png,image/gif,image/jpeg,image/png" />
|
|
84
|
+
</div>
|
|
85
|
+
<div v-show="blurhash" class="h-150 col-span-2">
|
|
86
|
+
<picture class="flex gap-md justify-center">
|
|
87
|
+
<div>
|
|
88
|
+
<div class="text-20 font-semibold mb-md">Blurhash</div>
|
|
89
|
+
<canvas
|
|
90
|
+
ref="canvas"
|
|
91
|
+
class="w-150 h-150 block object-cover" />
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<div class="text-20 font-semibold mb-md">Image</div>
|
|
95
|
+
<img
|
|
96
|
+
v-if="image"
|
|
97
|
+
class="w-150 h-150 block object-cover"
|
|
98
|
+
:class="{ 'vv-skeleton__item': isLoading }"
|
|
99
|
+
:src="imageUrl"
|
|
100
|
+
alt="image"
|
|
101
|
+
:width="image.width"
|
|
102
|
+
:height="image.height" />
|
|
103
|
+
</div>
|
|
104
|
+
</picture>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
`,
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
parameters: {
|
|
111
|
+
docs: {
|
|
112
|
+
source: {
|
|
113
|
+
type: 'code',
|
|
114
|
+
language: 'html',
|
|
115
|
+
code: /* html */ `
|
|
116
|
+
<div class="w-full grid gap-md grid-cols-3 h-150" :class="{ 'vv-skeleton': isLoading }">
|
|
117
|
+
<div class="w-150 h-150 col-span-1">
|
|
118
|
+
<div class="text-20 font-semibold mb-md">Upload image</div>
|
|
119
|
+
<vv-input-file v-model="file" name="input-file" modifiers="drop-area square hidden" accept=".gif,.jpg,.jpeg,.png,image/gif,image/jpeg,image/png" />
|
|
120
|
+
</div>
|
|
121
|
+
<div v-show="blurhash" class="h-150 col-span-2">
|
|
122
|
+
<picture class="flex gap-md justify-center">
|
|
123
|
+
<div>
|
|
124
|
+
<div class="text-20 font-semibold mb-md">Blurhash</div>
|
|
125
|
+
<canvas
|
|
126
|
+
ref="canvas"
|
|
127
|
+
class="w-150 h-150 block object-cover" />
|
|
128
|
+
</div>
|
|
129
|
+
<div>
|
|
130
|
+
<div class="text-20 font-semibold mb-md">Image</div>
|
|
131
|
+
<img
|
|
132
|
+
v-if="image"
|
|
133
|
+
class="w-150 h-150 block object-cover"
|
|
134
|
+
:class="{ 'vv-skeleton__item': isLoading }"
|
|
135
|
+
:src="imageUrl"
|
|
136
|
+
alt="image"
|
|
137
|
+
:width="image.width"
|
|
138
|
+
:height="image.height" />
|
|
139
|
+
</div>
|
|
140
|
+
</picture>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<script setup lang='ts'>
|
|
145
|
+
import { useBlurhash } from '@volverjs/ui-vue/composables'
|
|
146
|
+
|
|
147
|
+
const { encode, decode, loadImage } = useBlurhash()
|
|
148
|
+
|
|
149
|
+
const isLoading = ref(false)
|
|
150
|
+
const file = ref({})
|
|
151
|
+
const canvas = ref()
|
|
152
|
+
const isImgLoaded = ref(false)
|
|
153
|
+
const blurhash = ref('')
|
|
154
|
+
const imageUrl = ref('')
|
|
155
|
+
const image = ref()
|
|
156
|
+
|
|
157
|
+
watch(file, async (newValue) => {
|
|
158
|
+
if (newValue?.size) {
|
|
159
|
+
this.imageUrl = URL.createObjectURL(newValue)
|
|
160
|
+
this.image = await this.loadImage(this.imageUrl)
|
|
161
|
+
this.blurhash = await this.encode(newValue)
|
|
162
|
+
} else {
|
|
163
|
+
this.image = null
|
|
164
|
+
this.imageUrl = ''
|
|
165
|
+
this.blurhash = ''
|
|
166
|
+
}
|
|
167
|
+
}, { immediate: true })
|
|
168
|
+
|
|
169
|
+
watch(blurhash, async (newValue) => {
|
|
170
|
+
if (this.image) {
|
|
171
|
+
const blurhashDecoded = await this.decode(
|
|
172
|
+
newValue,
|
|
173
|
+
this.image.width,
|
|
174
|
+
this.image.height,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (this.canvas) {
|
|
178
|
+
this.canvas.width = this.image.width
|
|
179
|
+
this.canvas.height = this.image.height
|
|
180
|
+
const ctx = this.canvas.getContext('2d')
|
|
181
|
+
const imageData = ctx.createImageData(
|
|
182
|
+
this.canvas.width,
|
|
183
|
+
this.canvas.height,
|
|
184
|
+
)
|
|
185
|
+
imageData.data.set(blurhashDecoded)
|
|
186
|
+
ctx.putImageData(imageData, 0, 0)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
</script>
|
|
191
|
+
`,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HintArgTypes, ModifiersArgTypes } from '@/stories/argTypes'
|
|
2
|
+
import { VvInputFileProps } from '@/components/VvInputFile'
|
|
3
|
+
|
|
4
|
+
export const defaultArgs = {
|
|
5
|
+
...propsToObject(VvInputFileProps),
|
|
6
|
+
name: 'vv-input-file',
|
|
7
|
+
label: 'Upload file',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const argTypes = {
|
|
11
|
+
...HintArgTypes,
|
|
12
|
+
modifiers: {
|
|
13
|
+
...ModifiersArgTypes.modifiers,
|
|
14
|
+
options: ['drop-area', 'hidden', 'square', 'circle'],
|
|
15
|
+
},
|
|
16
|
+
'drop-area': {
|
|
17
|
+
description: 'Drop area slot',
|
|
18
|
+
control: {
|
|
19
|
+
type: 'text',
|
|
20
|
+
},
|
|
21
|
+
table: {
|
|
22
|
+
category: 'Slots',
|
|
23
|
+
type: {
|
|
24
|
+
summary: 'html',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'update:model-value': {
|
|
29
|
+
table: {
|
|
30
|
+
category: 'Events',
|
|
31
|
+
type: {
|
|
32
|
+
summary: 'File | File[]',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|