index.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <template>
  2. <div
  3. class="editable-element-text"
  4. :class="{ 'lock': elementInfo.lock }"
  5. :style="{
  6. top: elementInfo.top + 'px',
  7. left: elementInfo.left + 'px',
  8. width: elementInfo.width + 'px',
  9. height: elementInfo.height + 'px',
  10. }"
  11. >
  12. <div
  13. class="rotate-wrapper"
  14. :style="{ transform: `rotate(${elementInfo.rotate}deg)` }"
  15. >
  16. <div
  17. class="element-content"
  18. ref="elementRef"
  19. :style="{
  20. width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
  21. height: elementInfo.vertical ? elementInfo.height + 'px' : elementInfo.height + 'px',
  22. backgroundColor: elementInfo.fill,
  23. opacity: elementInfo.opacity,
  24. textShadow: shadowStyle,
  25. lineHeight: elementInfo.lineHeight,
  26. letterSpacing: (elementInfo.wordSpace || 0) + 'px',
  27. color: elementInfo.defaultColor,
  28. fontFamily: elementInfo.defaultFontName,
  29. writingMode: elementInfo.vertical ? 'vertical-rl' : 'horizontal-tb',
  30. display: 'flex',
  31. alignItems: 'center',
  32. overflow: hidden,
  33. }"
  34. v-contextmenu="contextmenus"
  35. @mousedown="$event => handleSelectElement($event)"
  36. @touchstart="$event => handleSelectElement($event)"
  37. >
  38. <ElementOutline
  39. :width="elementInfo.width"
  40. :height="elementInfo.height"
  41. :outline="elementInfo.outline"
  42. />
  43. <ProsemirrorEditor
  44. class="text"
  45. :elementId="elementInfo.id"
  46. :defaultColor="elementInfo.defaultColor"
  47. :defaultFontName="elementInfo.defaultFontName"
  48. :editable="!elementInfo.lock"
  49. :value="elementInfo.content"
  50. :style="{
  51. '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
  52. }"
  53. @update="({ value, ignore }) => updateContent(value, ignore)"
  54. @mousedown="$event => handleSelectElement($event, false)"
  55. />
  56. <!-- 当字号过大且行高较小时,会出现文字高度溢出的情况,导致拖拽区域无法被选中,因此添加了以下节点避免该情况 -->
  57. <div class="drag-handler top"></div>
  58. <div class="drag-handler bottom"></div>
  59. </div>
  60. </div>
  61. </div>
  62. </template>
  63. <script lang="ts" setup>
  64. import { computed, onMounted, onUnmounted, ref, watch, useTemplateRef } from 'vue'
  65. import { storeToRefs } from 'pinia'
  66. import { debounce } from 'lodash'
  67. import { useMainStore, useSlidesStore } from '@/store'
  68. import type { PPTTextElement } from '@/types/slides'
  69. import type { ContextmenuItem } from '@/components/Contextmenu/types'
  70. import useElementShadow from '@/views/components/element/hooks/useElementShadow'
  71. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  72. import ElementOutline from '@/views/components/element/ElementOutline.vue'
  73. import ProsemirrorEditor from '@/views/components/element/ProsemirrorEditor.vue'
  74. const props = defineProps<{
  75. elementInfo: PPTTextElement
  76. selectElement: (e: MouseEvent | TouchEvent, element: PPTTextElement, canMove?: boolean) => void
  77. contextmenus: () => ContextmenuItem[] | null
  78. }>()
  79. const mainStore = useMainStore()
  80. const slidesStore = useSlidesStore()
  81. const { handleElementId, isScaling } = storeToRefs(mainStore)
  82. const { addHistorySnapshot } = useHistorySnapshot()
  83. const elementRef = useTemplateRef<HTMLElement>('elementRef')
  84. const shadow = computed(() => props.elementInfo.shadow)
  85. const { shadowStyle } = useElementShadow(shadow)
  86. const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
  87. if (props.elementInfo.lock) return
  88. e.stopPropagation()
  89. props.selectElement(e, props.elementInfo, canMove)
  90. }
  91. // 监听文本元素的尺寸变化,当高度变化时,更新高度到vuex
  92. // 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
  93. const realHeightCache = ref(-1)
  94. const realWidthCache = ref(-1)
  95. watch(isScaling, () => {
  96. if (handleElementId.value !== props.elementInfo.id) return
  97. if (!isScaling.value) {
  98. if (!props.elementInfo.vertical && realHeightCache.value !== -1) {
  99. slidesStore.updateElement({
  100. id: props.elementInfo.id,
  101. props: { height: realHeightCache.value },
  102. })
  103. realHeightCache.value = -1
  104. }
  105. if (props.elementInfo.vertical && realWidthCache.value !== -1) {
  106. slidesStore.updateElement({
  107. id: props.elementInfo.id,
  108. props: { width: realWidthCache.value },
  109. })
  110. realWidthCache.value = -1
  111. }
  112. }
  113. })
  114. const updateTextElementHeight = (entries: ResizeObserverEntry[]) => {
  115. const contentRect = entries[0].contentRect
  116. if (!elementRef.value) return
  117. const realHeight = contentRect.height + 20
  118. const realWidth = contentRect.width + 20
  119. if (!props.elementInfo.vertical && props.elementInfo.height !== realHeight) {
  120. if (!isScaling.value) {
  121. slidesStore.updateElement({
  122. id: props.elementInfo.id,
  123. props: { height: realHeight },
  124. })
  125. }
  126. else realHeightCache.value = realHeight
  127. }
  128. if (props.elementInfo.vertical && props.elementInfo.width !== realWidth) {
  129. if (!isScaling.value) {
  130. slidesStore.updateElement({
  131. id: props.elementInfo.id,
  132. props: { width: realWidth },
  133. })
  134. }
  135. else realWidthCache.value = realWidth
  136. }
  137. }
  138. const resizeObserver = new ResizeObserver(updateTextElementHeight)
  139. onMounted(() => {
  140. if (elementRef.value) resizeObserver.observe(elementRef.value)
  141. })
  142. onUnmounted(() => {
  143. if (elementRef.value) resizeObserver.unobserve(elementRef.value)
  144. })
  145. const updateContent = (content: string, ignore = false) => {
  146. slidesStore.updateElement({
  147. id: props.elementInfo.id,
  148. props: { content },
  149. })
  150. if (!ignore) addHistorySnapshot()
  151. }
  152. const checkEmptyText = debounce(function() {
  153. const pureText = props.elementInfo.content.replace(/<[^>]+>/g, '')
  154. if (!pureText) slidesStore.deleteElement(props.elementInfo.id)
  155. }, 300, { trailing: true })
  156. const isHandleElement = computed(() => handleElementId.value === props.elementInfo.id)
  157. watch(isHandleElement, () => {
  158. if (!isHandleElement.value) checkEmptyText()
  159. })
  160. </script>
  161. <style lang="scss" scoped>
  162. .editable-element-text {
  163. position: absolute;
  164. &.lock .element-content {
  165. cursor: default;
  166. }
  167. }
  168. .rotate-wrapper {
  169. width: 100%;
  170. height: 100%;
  171. }
  172. .element-content {
  173. position: relative;
  174. padding: 10px;
  175. line-height: 1.5;
  176. word-break: break-word;
  177. cursor: move;
  178. .text {
  179. position: relative;
  180. }
  181. ::v-deep(a) {
  182. cursor: text;
  183. }
  184. }
  185. .drag-handler {
  186. height: 10px;
  187. position: absolute;
  188. left: 0;
  189. right: 0;
  190. &.top {
  191. top: 0;
  192. }
  193. &.bottom {
  194. bottom: 0;
  195. }
  196. }
  197. </style>