PersonalSettingAvatarCropImageFlyout.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { ref } from 'vue'
  4. import { Cropper, type CropperResult } from 'vue-advanced-cropper'
  5. import 'vue-advanced-cropper/dist/style.css'
  6. import CommonAvatar from '#shared/components/CommonAvatar/CommonAvatar.vue'
  7. import type { ImageFileData } from '#shared/utils/files.ts'
  8. import CommonFlyout from '#desktop/components/CommonFlyout/CommonFlyout.vue'
  9. interface Props {
  10. image?: ImageFileData
  11. }
  12. const props = defineProps<Props>()
  13. defineEmits<{
  14. 'image-cropped': [ImageFileData | undefined]
  15. }>()
  16. const croppedImage = ref<ImageFileData>()
  17. const imageCropped = (crop: CropperResult) => {
  18. if (!crop.canvas) return
  19. croppedImage.value = {
  20. content: crop.canvas.toDataURL('image/png'),
  21. name: 'avatar.png',
  22. type: 'image/png',
  23. }
  24. }
  25. const discardImage = () => {
  26. croppedImage.value = undefined
  27. }
  28. </script>
  29. <template>
  30. <CommonFlyout
  31. :header-title="__('Crop Image')"
  32. :footer-action-options="{
  33. actionLabel: __('Save'),
  34. actionButton: { variant: 'submit' },
  35. }"
  36. header-icon="image"
  37. name="avatar-file-upload"
  38. @action="$emit('image-cropped', croppedImage)"
  39. @close="discardImage"
  40. >
  41. <div class="flex flex-col gap-3">
  42. <div v-if="croppedImage" class="flex flex-row items-center gap-1">
  43. <CommonAvatar :image="croppedImage.content" size="normal" />
  44. <CommonLabel>{{ $t('Avatar Preview') }}</CommonLabel>
  45. </div>
  46. <Cropper
  47. :src="props.image?.content"
  48. :stencil-props="{
  49. aspectRatio: 1,
  50. class: 'cropper-stencil',
  51. previewClass: 'cropper-stencil__preview',
  52. draggingClass: 'cropper-stencil--dragging',
  53. handlersClasses: {
  54. default: 'cropper-handler',
  55. eastNorth: 'cropper-handler--east-north',
  56. westNorth: 'cropper-handler--west-north',
  57. eastSouth: 'cropper-handler--east-south',
  58. westSouth: 'cropper-handler--west-south',
  59. },
  60. }"
  61. :transitions="false"
  62. class="cropper !max-h-[340px] !max-w-[476px]"
  63. background-class="cropper-background"
  64. image-class="cropper__image"
  65. @change="imageCropped"
  66. />
  67. </div>
  68. </CommonFlyout>
  69. </template>
  70. <style scoped>
  71. :deep(.cropper-background) {
  72. background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
  73. }
  74. :deep(.cropper) {
  75. &__image {
  76. opacity: 1;
  77. }
  78. }
  79. :deep(.cropper-stencil) {
  80. &__preview {
  81. &::after,
  82. &::before {
  83. content: '';
  84. opacity: 0;
  85. transition: opacity 0.25s;
  86. position: absolute;
  87. pointer-events: none;
  88. z-index: 1;
  89. }
  90. &::after {
  91. border-left: solid 1px white;
  92. border-right: solid 1px white;
  93. width: 33%;
  94. height: 100%;
  95. transform: translateX(-50%);
  96. left: 50%;
  97. top: 0;
  98. }
  99. &::before {
  100. border-top: solid 1px white;
  101. border-bottom: solid 1px white;
  102. height: 33%;
  103. width: 100%;
  104. transform: translateY(-50%);
  105. top: 50%;
  106. left: 0;
  107. }
  108. }
  109. &--dragging {
  110. :deep(.cropper-stencil__preview) {
  111. &::after,
  112. &::before {
  113. opacity: 0.4;
  114. }
  115. }
  116. }
  117. }
  118. :deep(.cropper-line) {
  119. border-color: rgba(white, 0.8);
  120. }
  121. :deep(.cropper-handler) {
  122. display: block;
  123. opacity: 0.7;
  124. position: relative;
  125. flex-shrink: 0;
  126. transition: opacity 0.5s;
  127. border: none;
  128. background: white;
  129. top: auto;
  130. left: auto;
  131. height: 4px;
  132. width: 4px;
  133. &--west-north,
  134. &--east-south,
  135. &--west-south,
  136. &--east-north {
  137. display: block;
  138. height: 16px;
  139. width: 16px;
  140. background: none;
  141. }
  142. &--west-north {
  143. border-left: solid 2px white;
  144. border-top: solid 2px white;
  145. top: 7px;
  146. left: 7px;
  147. }
  148. &--east-south {
  149. border-right: solid 2px white;
  150. border-bottom: solid 2px white;
  151. top: -7px;
  152. left: -7px;
  153. }
  154. &--west-south {
  155. border-left: solid 2px white;
  156. border-bottom: solid 2px white;
  157. top: -7px;
  158. left: 7px;
  159. }
  160. &--east-north {
  161. border-right: solid 2px white;
  162. border-top: solid 2px white;
  163. top: 7px;
  164. left: -7px;
  165. }
  166. &--hover {
  167. opacity: 1;
  168. }
  169. }
  170. </style>