index.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. }"
  31. v-contextmenu="contextmenus"
  32. @mousedown="$event => handleSelectElement($event)"
  33. @touchstart="$event => handleSelectElement($event)"
  34. >
  35. <ElementOutline
  36. :width="elementInfo.width"
  37. :height="elementInfo.height"
  38. :outline="elementInfo.outline"
  39. />
  40. <div class="shape-text" :style="elementInfo.style" :class="[elementInfo.align, { 'editable': editable || elementInfo.content }]">
  41. <ProsemirrorEditor
  42. class="text"
  43. :elementId="elementInfo.id"
  44. :defaultColor="elementInfo.defaultColor"
  45. :defaultFontName="elementInfo.defaultFontName"
  46. :editable="!elementInfo.lock"
  47. :value="elementInfo.content"
  48. :style="{
  49. '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
  50. }"
  51. @update="({ value, ignore }) => updateContent(value, ignore)"
  52. @mousedown="$event => handleSelectElement($event, false)"
  53. />
  54. </div>
  55. <!-- 当字号过大且行高较小时,会出现文字高度溢出的情况,导致拖拽区域无法被选中,因此添加了以下节点避免该情况 -->
  56. <div class="drag-handler top"></div>
  57. <div class="drag-handler bottom"></div>
  58. </div>
  59. </div>
  60. </div>
  61. </template>
  62. <script lang="ts" setup>
  63. import { computed, onMounted, onUnmounted, ref, watch, useTemplateRef } from 'vue'
  64. import { storeToRefs } from 'pinia'
  65. import { debounce } from 'lodash'
  66. import { useMainStore, useSlidesStore } from '@/store'
  67. import type { PPTTextElement } from '@/types/slides'
  68. import type { ContextmenuItem } from '@/components/Contextmenu/types'
  69. import useElementShadow from '@/views/components/element/hooks/useElementShadow'
  70. import useHistorySnapshot from '@/hooks/useHistorySnapshot'
  71. import ElementOutline from '@/views/components/element/ElementOutline.vue'
  72. import ProsemirrorEditor from '@/views/components/element/ProsemirrorEditor.vue'
  73. const props = defineProps<{
  74. elementInfo: PPTTextElement
  75. selectElement: (e: MouseEvent | TouchEvent, element: PPTTextElement, canMove?: boolean) => void
  76. contextmenus: () => ContextmenuItem[] | null
  77. }>()
  78. const mainStore = useMainStore()
  79. const slidesStore = useSlidesStore()
  80. const { handleElementId, isScaling } = storeToRefs(mainStore)
  81. const { addHistorySnapshot } = useHistorySnapshot()
  82. const elementRef = useTemplateRef<HTMLElement>('elementRef')
  83. const shadow = computed(() => props.elementInfo.shadow)
  84. const { shadowStyle } = useElementShadow(shadow)
  85. const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
  86. if (props.elementInfo.lock) return
  87. e.stopPropagation()
  88. props.selectElement(e, props.elementInfo, canMove)
  89. }
  90. // 监听文本元素的尺寸变化,当高度变化时,更新高度到vuex
  91. // 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新
  92. const realHeightCache = ref(-1)
  93. const realWidthCache = ref(-1)
  94. watch(isScaling, () => {
  95. if (handleElementId.value !== props.elementInfo.id) return
  96. if (!isScaling.value) {
  97. if (!props.elementInfo.vertical && realHeightCache.value !== -1) {
  98. slidesStore.updateElement({
  99. id: props.elementInfo.id,
  100. props: { height: realHeightCache.value },
  101. })
  102. realHeightCache.value = -1
  103. }
  104. if (props.elementInfo.vertical && realWidthCache.value !== -1) {
  105. slidesStore.updateElement({
  106. id: props.elementInfo.id,
  107. props: { width: realWidthCache.value },
  108. })
  109. realWidthCache.value = -1
  110. }
  111. }
  112. })
  113. const updateTextElementHeight = (entries: ResizeObserverEntry[]) => {
  114. const contentRect = entries[0].contentRect
  115. if (!elementRef.value) return
  116. const realHeight = contentRect.height + 20
  117. const realWidth = contentRect.width + 20
  118. if (!props.elementInfo.vertical && props.elementInfo.height !== realHeight) {
  119. if (!isScaling.value) {
  120. slidesStore.updateElement({
  121. id: props.elementInfo.id,
  122. props: { height: realHeight },
  123. })
  124. }
  125. else realHeightCache.value = realHeight
  126. }
  127. if (props.elementInfo.vertical && props.elementInfo.width !== realWidth) {
  128. if (!isScaling.value) {
  129. slidesStore.updateElement({
  130. id: props.elementInfo.id,
  131. props: { width: realWidth },
  132. })
  133. }
  134. else realWidthCache.value = realWidth
  135. }
  136. }
  137. const resizeObserver = new ResizeObserver(updateTextElementHeight)
  138. onMounted(() => {
  139. if (elementRef.value) resizeObserver.observe(elementRef.value)
  140. })
  141. onUnmounted(() => {
  142. if (elementRef.value) resizeObserver.unobserve(elementRef.value)
  143. })
  144. const updateContent = (content: string, ignore = false) => {
  145. slidesStore.updateElement({
  146. id: props.elementInfo.id,
  147. props: { content },
  148. })
  149. if (!ignore) addHistorySnapshot()
  150. }
  151. const checkEmptyText = debounce(function() {
  152. const pureText = props.elementInfo.content.replace(/<[^>]+>/g, '')
  153. if (!pureText) slidesStore.deleteElement(props.elementInfo.id)
  154. }, 300, { trailing: true })
  155. const isHandleElement = computed(() => handleElementId.value === props.elementInfo.id)
  156. watch(isHandleElement, () => {
  157. if (!isHandleElement.value) checkEmptyText()
  158. })
  159. </script>
  160. <style lang="scss" scoped>
  161. .editable-element-text {
  162. position: absolute;
  163. &.lock .element-content {
  164. cursor: default;
  165. }
  166. }
  167. .rotate-wrapper {
  168. width: 100%;
  169. height: 100%;
  170. }
  171. .element-content {
  172. position: relative;
  173. padding: 10px;
  174. line-height: 1.5;
  175. word-break: break-word;
  176. cursor: move;
  177. font-family: Kaiti, "Kaiti SC", "Kaiti TC", Roboto, "Noto Sans SC", "Noto Sans TC", "Noto Sans KR", "Noto Sans JP", "Roboto", Roboto, "Noto Sans SC", "Noto Sans TC", "Noto Sans KR", "Noto Sans JP";
  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. .shape-text {
  198. width:100%;
  199. height:100%;
  200. position: absolute;
  201. top: 0;
  202. bottom: 0;
  203. left: 0;
  204. right: 0;
  205. display: flex;
  206. flex-direction: column;
  207. word-break: break-word;
  208. pointer-events: none;
  209. white-space: normal;
  210. &.editable {
  211. pointer-events: all;
  212. }
  213. &.top {
  214. justify-content: flex-start;
  215. }
  216. &.middle {
  217. justify-content: center;
  218. left: 50%;
  219. top: 50%;
  220. -webkit-transform: translate(-50%,-50%);
  221. transform: translate(-50%,-50%);
  222. }
  223. &.bottom {
  224. justify-content: flex-end;
  225. }
  226. }
  227. </style>