FieldImageUploadInput.vue 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { useDropZone } from '@vueuse/core'
  4. import { useTemplateRef, computed, toRef } from 'vue'
  5. import useValue from '#shared/components/Form/composables/useValue.ts'
  6. import type { FormFieldContext } from '#shared/components/Form/types/field.ts'
  7. import { i18n } from '#shared/i18n.ts'
  8. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  9. import CommonDivider from '#desktop/components/CommonDivider/CommonDivider.vue'
  10. export interface Props {
  11. context: FormFieldContext<{
  12. placeholderImagePath?: string
  13. }>
  14. }
  15. const props = defineProps<Props>()
  16. const contextReactive = toRef(props, 'context')
  17. const { localValue } = useValue(contextReactive)
  18. const imageUpload = computed<string>({
  19. get() {
  20. return localValue.value || ''
  21. },
  22. set(value) {
  23. localValue.value = value
  24. },
  25. })
  26. const imageUploadOrPlaceholder = computed<string>(() => {
  27. if (props.context.placeholderImagePath && !imageUpload.value) {
  28. return props.context.placeholderImagePath || ''
  29. }
  30. return imageUpload.value
  31. })
  32. const MAX_IMAGE_SIZE_IN_MB = 8
  33. const imageUploadInput = useTemplateRef('image-upload')
  34. const reset = () => {
  35. imageUpload.value = ''
  36. const input = imageUploadInput.value
  37. if (!input) return
  38. input.value = ''
  39. input.files = null
  40. }
  41. const loadImages = async (files: FileList | File[] | null) => {
  42. Array.from(files || []).forEach((file) => {
  43. const reader = new FileReader()
  44. reader.onload = (e) => {
  45. if (!e.target || !e.target.result) return
  46. imageUpload.value = e.target.result as string
  47. }
  48. if (file.size && file.size > 1024 * 1024 * MAX_IMAGE_SIZE_IN_MB) {
  49. props.context.node.setErrors(
  50. i18n.t(
  51. 'File too big, max. %s MB allowed.',
  52. MAX_IMAGE_SIZE_IN_MB.toString(),
  53. ),
  54. )
  55. return
  56. }
  57. reader.readAsDataURL(file)
  58. })
  59. }
  60. const onFileChanged = async ($event: Event) => {
  61. const input = $event.target as HTMLInputElement
  62. const { files } = input
  63. if (files) await loadImages(files)
  64. }
  65. const dropZoneElement = useTemplateRef('drop-zone')
  66. const { isOverDropZone } = useDropZone(dropZoneElement, {
  67. onDrop: loadImages,
  68. dataTypes: (types) => types.every((type) => type.startsWith('image/')),
  69. })
  70. </script>
  71. <template>
  72. <div
  73. ref="drop-zone"
  74. class="flex w-full flex-col items-center gap-2 p-2"
  75. :class="context.classes.input"
  76. >
  77. <div
  78. v-if="isOverDropZone"
  79. class="w-full rounded text-center outline-dashed outline-1 outline-blue-800"
  80. >
  81. <CommonLabel
  82. class="py-2 text-blue-800 dark:text-blue-800"
  83. prefix-icon="upload"
  84. >
  85. {{ $t('Drop image file here') }}
  86. </CommonLabel>
  87. </div>
  88. <template v-else>
  89. <template v-if="imageUploadOrPlaceholder">
  90. <div
  91. class="grid w-full grid-cols-[20px_auto_20px] items-center justify-items-center gap-2.5 p-2.5"
  92. >
  93. <img
  94. class="col-start-2 max-h-32"
  95. :src="imageUploadOrPlaceholder"
  96. :alt="$t('Image preview')"
  97. />
  98. <CommonButton
  99. v-if="imageUpload"
  100. variant="remove"
  101. size="small"
  102. icon="x-lg"
  103. :aria-label="$t('Remove image')"
  104. @click="!context.disabled && reset()"
  105. />
  106. </div>
  107. <CommonDivider padding />
  108. </template>
  109. <CommonButton
  110. variant="secondary"
  111. size="medium"
  112. prefix-icon="image"
  113. :disabled="context.disabled"
  114. @click="!context.disabled && imageUploadInput?.click()"
  115. @blur="context.handlers.blur"
  116. >{{ $t('Upload image') }}</CommonButton
  117. >
  118. </template>
  119. <input
  120. :id="context.id"
  121. ref="image-upload"
  122. data-test-id="imageUploadInput"
  123. type="file"
  124. :name="context.node.name"
  125. :disabled="context.disabled"
  126. class="hidden"
  127. :class="context.classes.input"
  128. tabindex="-1"
  129. aria-hidden="true"
  130. :aria-describedby="context.describedBy"
  131. accept="image/*"
  132. v-bind="context.attrs"
  133. @change="!context.disabled && onFileChanged($event)"
  134. />
  135. </div>
  136. </template>