ImageResizable.vue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3'
  4. import { computed, reactive, ref } from 'vue'
  5. import DraggableResizable from 'vue3-draggable-resizable'
  6. import { loadImageIntoBase64 } from '#shared/utils/files.ts'
  7. import log from '#shared/utils/log.ts'
  8. import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
  9. import testFlags from '#shared/utils/testFlags.ts'
  10. const props = defineProps(nodeViewProps)
  11. const initialHeight = props.node.attrs.height
  12. const initialWidth = props.node.attrs.width
  13. const needBase64Convert = (src: string) => {
  14. return !(src.startsWith('data:') || src.startsWith('cid:'))
  15. }
  16. const isResized = ref(false)
  17. const isResizing = ref(false)
  18. const imageLoaded = ref(false)
  19. const isDraggable = computed(() => props.node.attrs.isDraggable)
  20. const src = computed(() => props.node.attrs.src)
  21. if (needBase64Convert(src.value)) {
  22. loadImageIntoBase64(
  23. src.value,
  24. props.node.attrs.type,
  25. props.node.attrs.alt,
  26. ).then((base64) => {
  27. if (base64) {
  28. props.updateAttributes({ src: base64 })
  29. } else {
  30. log.error(`Could not load image ${src.value}`)
  31. props.deleteNode()
  32. }
  33. })
  34. }
  35. const dimensions = reactive({
  36. maxWidth: 0,
  37. maxHeight: 0,
  38. height: computed({
  39. get: () => Number(props.node.attrs.height) || 0,
  40. set: (height) => props.updateAttributes({ height }),
  41. }),
  42. width: computed({
  43. get: () => Number(props.node.attrs.width) || 0,
  44. set: (width) => props.updateAttributes({ width }),
  45. }),
  46. })
  47. const onLoadImage = (e: Event) => {
  48. if (
  49. imageLoaded.value ||
  50. needBase64Convert(src.value) ||
  51. props.editor.isDestroyed ||
  52. !props.editor.isEditable
  53. )
  54. return
  55. const img = e.target as HTMLImageElement
  56. const { naturalWidth, naturalHeight } = img
  57. dimensions.width = initialWidth !== '100%' ? initialWidth : naturalWidth
  58. dimensions.height = initialHeight !== 'auto' ? initialHeight : naturalWidth
  59. dimensions.maxHeight = naturalHeight
  60. dimensions.maxWidth = naturalWidth
  61. imageLoaded.value = true
  62. testFlags.set('editor.imageResized')
  63. }
  64. const stopResizing = ({ w, h }: { w: number; h: number }) => {
  65. dimensions.width = w
  66. dimensions.height = h
  67. isResized.value = true
  68. }
  69. const style = computed(() => {
  70. if (!imageLoaded.value || !isResized.value) return {}
  71. const { width, height } = dimensions
  72. return {
  73. width: `${width}px`,
  74. height: `${height}px`,
  75. maxWidth: '100%',
  76. }
  77. })
  78. // this is needed so "dragable resize" could calculate the maximum size
  79. const wrapperStyle = computed(() => {
  80. if (!isResizing.value) return {}
  81. const { maxWidth, maxHeight } = dimensions
  82. return { width: maxWidth, height: maxHeight }
  83. })
  84. </script>
  85. <template>
  86. <NodeViewWrapper as="div" class="inline-block" :style="wrapperStyle">
  87. <button
  88. v-if="!isResizing && src"
  89. class="inline-block"
  90. @click="isResizing = true"
  91. @keydown.space.prevent="isResizing = true"
  92. @keydown.enter.prevent="isResizing = true"
  93. >
  94. <img
  95. v-if="!isResizing && src"
  96. class="inline-block"
  97. :style="style"
  98. :src="src"
  99. :alt="node.attrs.alt"
  100. :width="node.attrs.width"
  101. :height="node.attrs.height"
  102. :title="node.attrs.title"
  103. :draggable="isDraggable"
  104. @load="onLoadImage"
  105. />
  106. </button>
  107. <DraggableResizable
  108. v-else-if="src"
  109. v-model:active="isResizing"
  110. :h="dimensions.height"
  111. :w="dimensions.width"
  112. :draggable="false"
  113. lock-aspect-ratio
  114. parent
  115. class="!relative !inline-block"
  116. @resize-end="stopResizing"
  117. >
  118. <img
  119. class="inline-block"
  120. :alt="$t('Resize frame')"
  121. :src="src"
  122. :draggable="isDraggable"
  123. />
  124. </DraggableResizable>
  125. </NodeViewWrapper>
  126. </template>