Skip to content

Commit f2e4a5b

Browse files
committed
fix: support image preview
1 parent 488e4b5 commit f2e4a5b

File tree

15 files changed

+233
-22
lines changed

15 files changed

+233
-22
lines changed

demo-element-plus/src/viewer/FTableViewer.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ const table = useTable({
7676
filterParams: {
7777
gender: '',
7878
},
79+
settings: {
80+
url: form.settings.url,
81+
modal: {
82+
title: 'Crear record',
83+
},
84+
},
7985
buttons: {
8086
edit: {
8187
// onClick(row) {

demo-quasar/src/plugins/fancy-crud.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineConfig } from '@fancy-crud/vue'
22
import axios from 'axios'
33

44
import { components, styles, toast } from '@fancy-crud/wrapper-quasar'
5-
import { valibotSafeParser as parser } from '@fancy-crud/plugin-rule-parsers'
5+
import { zodSafeParser as parser } from '@fancy-crud/plugin-rule-parsers'
66

77
axios.defaults.baseURL = 'http://localhost:9000/api/'
88

demo-quasar/src/viewer/FTableViewer.vue

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
<script lang='ts' setup>
1515
import { FieldType, useForm, useTable } from '@fancy-crud/vue'
16+
import { z } from 'zod';
1617
1718
const r = ref(1)
1819
@@ -26,13 +27,24 @@ const form = useForm({
2627
wrapper: {
2728
class: 'col-span-12',
2829
},
29-
parseModelValue: Number,
30+
3031
},
3132
gender: {
32-
type: FieldType.text,
33+
type: FieldType.select,
3334
label: 'Gender',
35+
rules: value => ({ value, rule: z.string().nonempty() }),
36+
multiple: true,
37+
exclude: true,
3438
wrapper: {
35-
class: 'col-span-12',
39+
class: 'col-span-6',
40+
},
41+
},
42+
image2: {
43+
type: FieldType.file,
44+
label: 'Imagen',
45+
preview: true,
46+
wrapper: {
47+
class: 'col-span-6',
3648
},
3749
},
3850
created_at: {
@@ -45,6 +57,14 @@ const form = useForm({
4557
class: 'col-span-12',
4658
},
4759
},
60+
is_active: {
61+
type: FieldType.checkbox,
62+
label: 'Is active',
63+
modelValue: false,
64+
wrapper: {
65+
class: 'col-span-12',
66+
},
67+
},
4868
},
4969
settings: {
5070
url: 'artists/',
@@ -55,17 +75,26 @@ const form = useForm({
5575
const table = useTable({
5676
form,
5777
columns: {
78+
name: {
79+
},
5880
gender: {
5981
format: (value: unknown) => value === 'm' ? 'Male' : 'Female',
6082
},
61-
created_at: {
62-
format: (value: unknown) => 'Lo que sea',
83+
image2: {
84+
label: 'Image',
6385
input: {
6486
isEnable: true,
87+
type: FieldType.image,
6588
},
6689
},
90+
created_at: {
91+
format: (value: unknown) => 'Lo que sea',
92+
},
6793
is_active: {
68-
input: { isEnable: true, type: FieldType.checkbox },
94+
input: {
95+
type: FieldType.checkbox,
96+
isEnable: true,
97+
},
6998
},
7099
actions: { value: 'actions', label: '', align: 'right' },
71100
},

demo-vuetify/src/viewer/FTableViewer.vue

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ const form = useForm({
3131
class: 'col-span-12',
3232
},
3333
},
34+
image2: {
35+
type: FieldType.image,
36+
label: 'Imagen',
37+
wrapper: {
38+
class: 'col-span-6',
39+
},
40+
},
3441
created_at: {
3542
type: FieldType.text,
3643
label: 'Created at',
@@ -46,14 +53,6 @@ const form = useForm({
4653
url: 'artists/',
4754
title: '{{ Crear artista | Actualizar artista }}',
4855
},
49-
buttons: {
50-
main: {
51-
onClick(a: number, b: number) {
52-
53-
},
54-
chubaca: '',
55-
},
56-
},
5756
})
5857
5958
const table = useTable({
@@ -65,6 +64,11 @@ const table = useTable({
6564
created_at: {
6665
format: (value: unknown) => 'Lo que sea',
6766
},
67+
image2: {
68+
input: {
69+
isEnable: true,
70+
},
71+
},
6872
actions: { value: 'actions', label: '' },
6973
},
7074
settings: {

packages/core/src/forms/axioma/typing/form.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface BaseRawField extends Record<string, any> {
2323
modelValue?: unknown
2424
fileUrl?: string | null
2525
multiple?: boolean
26+
preview?: boolean
2627
rules?: Rule
2728
recordValue?: (value: any) => unknown
2829
interceptOptions?: (options: any[]) => unknown[]

packages/plugin-rule-parsers/src/valibot.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { AnySchema } from 'valibot'
22
import { safeParse } from 'valibot'
33

44
export function valibotSafeParser(raw: { value: unknown; rule: AnySchema }) {
5+
if (typeof raw === 'string' || typeof raw === 'boolean')
6+
return raw
7+
58
const { value, rule } = raw
69
const result = safeParse(rule, value)
710

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<template>
2+
<a
3+
@mouseenter="togglePreview"
4+
ref="previewRef"
5+
target="_blank"
6+
:href="fileUrl"
7+
class="f-preview-trigger"
8+
:class="{ 'f-preview-trigger--disabled': !hasFile }"
9+
>
10+
<svg v-if="isImage" class="w-6 h-6 text-primary" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
11+
<path fill-rule="evenodd" d="M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7Zm.394 9.553a1 1 0 0 0-1.817.062l-2.5 6A1 1 0 0 0 8 19h8a1 1 0 0 0 .894-1.447l-2-4A1 1 0 0 0 13.2 13.4l-.53.706-1.276-2.553ZM13 9.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" clip-rule="evenodd" />
12+
</svg>
13+
<svg v-else class="w-6 h-6 text-primary" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
14+
<path fill-rule="evenodd" d="M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7Z" clip-rule="evenodd" />
15+
</svg>
16+
<span style="width: 100%" />
17+
<teleport to="body">
18+
<div
19+
@mouseleave="togglePreview"
20+
class="shadow-black f-preview shadow-2xl -translate-y-1/2"
21+
:class="displayPreview" :style="{ top: state.axis.y, left: state.axis.x }"
22+
>
23+
<a :href="fileUrl" target="_blank">
24+
<figure class="f-preview-img">
25+
<img :src="fileUrl" alt="">
26+
</figure>
27+
</a>
28+
</div>
29+
</teleport>
30+
</a>
31+
</template>
32+
33+
<script lang="ts" setup>
34+
import type { NormalizedField } from '@fancy-crud/core'
35+
36+
const props = defineProps<{
37+
formId: symbol
38+
field: NormalizedField
39+
}>()
40+
41+
const state = reactive({
42+
isHover: false,
43+
axis: {
44+
x: '0px',
45+
y: '0px',
46+
},
47+
})
48+
49+
const previewRef = ref<HTMLElement | null>(null)
50+
const fileUrl = computed(() => {
51+
return props.field.fileUrl || undefined
52+
})
53+
const displayPreview = computed(() => state.isHover ? 'f-preview--scale-100' : 'f-preview--scale-0')
54+
const hasFile = computed(() => {
55+
return !!fileUrl.value
56+
})
57+
const isImage = computed(() => checkIfFileIsImage(props.field.type, fileUrl.value))
58+
59+
onMounted(() => {
60+
setPreviewPosition()
61+
62+
window.addEventListener('resize', () => {
63+
setPreviewPosition()
64+
})
65+
})
66+
67+
function checkIfFileIsImage(fieldType: string, url?: string | null) {
68+
if (fieldType === 'image')
69+
return true
70+
71+
const IMAGE_EXTENSIONS = /\.(jpeg|jpg|gif|png|webp)$/
72+
const result = props.field.fileUrl?.match(IMAGE_EXTENSIONS) !== null
73+
return result
74+
}
75+
76+
function togglePreview() {
77+
state.isHover = !state.isHover
78+
}
79+
80+
function setPreviewPosition() {
81+
setTimeout(() => {
82+
if (!previewRef.value)
83+
return
84+
85+
const { top, left } = previewRef.value.getBoundingClientRect()
86+
state.axis.x = `${left}px`
87+
state.axis.y = `${top}px`
88+
}, 1000)
89+
}
90+
</script>
91+
92+
<style lang="sass">
93+
.f-preview
94+
position: absolute
95+
max-height: 250px
96+
max-width: 250px
97+
min-height: fit-content
98+
height: fit-content
99+
width: fit-content
100+
overflow: hidden
101+
box-shadow: 0 0 15px rgba(0, 0, 0, 0.15)
102+
transform-origin: top left
103+
z-index: 9999
104+
transition: transform 0.5s ease
105+
106+
.f-preview-img
107+
width: 100%
108+
height: 100%
109+
background-size: contain
110+
background-position: 0 0
111+
background-repeat: no-repeat
112+
background-color: white
113+
114+
.f-preview--scale-0
115+
@extends .f-preview
116+
transform: scale(0) translateY(-50%)
117+
118+
.f-preview--scale-100
119+
@extends .f-preview
120+
transform: scale(1) translateY(-50%)
121+
122+
.f-preview-trigger--disabled
123+
opacity: 0.3
124+
</style>

packages/vue/src/forms/components/FFormBody.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const defaultControls: Record<string, any> = {
5353
file: FFile,
5454
datepicker: FDatepicker,
5555
text: FText,
56+
image: FFile,
5657
...components,
5758
}
5859

packages/vue/src/forms/components/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import FCheckbox from './FCheckbox.vue'
88
import FText from './FText.vue'
99
import FColor from './FColor.vue'
1010
import FDatepicker from './FDatepicker.vue'
11+
import FPassword from './FPassword.vue'
1112
import FFile from './FFile.vue'
1213
import FRadio from './FRadio.vue'
1314
import FSelect from './FSelect.vue'
1415
import FTextarea from './FTextarea.vue'
16+
import FFileReveal from './FFileReveal.vue'
1517

1618
export {
1719
FForm,
@@ -27,4 +29,6 @@ export {
2729
FRadio,
2830
FSelect,
2931
FTextarea,
32+
FFileReveal,
33+
FPassword,
3034
}

packages/vue/src/forms/typing/form.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export enum FieldType {
4242
select = 'select',
4343
file = 'file',
4444
datepicker = 'datepicker',
45+
image = 'image',
4546
}
4647

4748
export interface RawTextField extends BaseRawField {

0 commit comments

Comments
 (0)