| <template> | |
| <div | |
| class="editable-element-text" | |
| :class="{ 'lock': elementInfo.lock }" | |
| :style="{ | |
| top: elementInfo.top + 'px', | |
| left: elementInfo.left + 'px', | |
| width: elementInfo.width + 'px', | |
| height: elementInfo.height + 'px', | |
| }" | |
| > | |
| <div | |
| class="rotate-wrapper" | |
| :style="{ transform: `rotate(${elementInfo.rotate}deg)` }" | |
| > | |
| <div | |
| class="element-content" | |
| ref="elementRef" | |
| :style="{ | |
| width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px', | |
| height: elementInfo.vertical ? elementInfo.height + 'px' : 'auto', | |
| backgroundColor: elementInfo.fill, | |
| opacity: elementInfo.opacity, | |
| textShadow: shadowStyle, | |
| lineHeight: elementInfo.lineHeight, | |
| letterSpacing: (elementInfo.wordSpace || 0) + 'px', | |
| color: elementInfo.defaultColor, | |
| fontFamily: elementInfo.defaultFontName, | |
| writingMode: elementInfo.vertical ? 'vertical-rl' : 'horizontal-tb', | |
| }" | |
| v-contextmenu="contextmenus" | |
| @mousedown="$event => handleSelectElement($event)" | |
| @touchstart="$event => handleSelectElement($event)" | |
| > | |
| <ElementOutline | |
| :width="elementInfo.width" | |
| :height="elementInfo.height" | |
| :outline="elementInfo.outline" | |
| /> | |
| <ProsemirrorEditor | |
| class="text" | |
| :elementId="elementInfo.id" | |
| :defaultColor="elementInfo.defaultColor" | |
| :defaultFontName="elementInfo.defaultFontName" | |
| :editable="!elementInfo.lock" | |
| :value="elementInfo.content" | |
| :style="{ | |
| '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`, | |
| }" | |
| @update="({ value, ignore }) => updateContent(value, ignore)" | |
| @mousedown="$event => handleSelectElement($event, false)" | |
| /> | |
| <!-- 当字号过大且行高较小时,会出现文字高度溢出的情况,导致拖拽区域无法被选中,因此添加了以下节点避免该情况 --> | |
| <div class="drag-handler top"></div> | |
| <div class="drag-handler bottom"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script lang="ts" setup> | |
| import { computed, onMounted, onUnmounted, ref, watch } from 'vue' | |
| import { storeToRefs } from 'pinia' | |
| import { debounce } from 'lodash' | |
| import { useMainStore, useSlidesStore } from '@/store' | |
| import type { PPTTextElement } from '@/types/slides' | |
| import type { ContextmenuItem } from '@/components/Contextmenu/types' | |
| import useElementShadow from '@/views/components/element/hooks/useElementShadow' | |
| import useHistorySnapshot from '@/hooks/useHistorySnapshot' | |
| import ElementOutline from '@/views/components/element/ElementOutline.vue' | |
| import ProsemirrorEditor from '@/views/components/element/ProsemirrorEditor.vue' | |
| const props = defineProps<{ | |
| elementInfo: PPTTextElement | |
| selectElement: (e: MouseEvent | TouchEvent, element: PPTTextElement, canMove?: boolean) => void | |
| contextmenus: () => ContextmenuItem[] | null | |
| }>() | |
| const mainStore = useMainStore() | |
| const slidesStore = useSlidesStore() | |
| const { handleElementId, isScaling } = storeToRefs(mainStore) | |
| const { addHistorySnapshot } = useHistorySnapshot() | |
| const elementRef = ref<HTMLElement>() | |
| const shadow = computed(() => props.elementInfo.shadow) | |
| const { shadowStyle } = useElementShadow(shadow) | |
| const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => { | |
| if (props.elementInfo.lock) return | |
| e.stopPropagation() | |
| props.selectElement(e, props.elementInfo, canMove) | |
| } | |
| // 监听文本元素的尺寸变化,当高度变化时,更新高度到vuex | |
| // 如果高度变化时正处在缩放操作中,则等待缩放操作结束后再更新 | |
| const realHeightCache = ref(-1) | |
| const realWidthCache = ref(-1) | |
| watch(isScaling, () => { | |
| if (handleElementId.value !== props.elementInfo.id) return | |
| if (!isScaling.value) { | |
| if (!props.elementInfo.vertical && realHeightCache.value !== -1) { | |
| slidesStore.updateElement({ | |
| id: props.elementInfo.id, | |
| props: { height: realHeightCache.value }, | |
| }) | |
| realHeightCache.value = -1 | |
| } | |
| if (props.elementInfo.vertical && realWidthCache.value !== -1) { | |
| slidesStore.updateElement({ | |
| id: props.elementInfo.id, | |
| props: { width: realWidthCache.value }, | |
| }) | |
| realWidthCache.value = -1 | |
| } | |
| } | |
| }) | |
| const updateTextElementHeight = (entries: ResizeObserverEntry[]) => { | |
| const contentRect = entries[0].contentRect | |
| if (!elementRef.value) return | |
| const realHeight = contentRect.height + 20 | |
| const realWidth = contentRect.width + 20 | |
| if (!props.elementInfo.vertical && props.elementInfo.height !== realHeight) { | |
| if (!isScaling.value) { | |
| slidesStore.updateElement({ | |
| id: props.elementInfo.id, | |
| props: { height: realHeight }, | |
| }) | |
| } | |
| else realHeightCache.value = realHeight | |
| } | |
| if (props.elementInfo.vertical && props.elementInfo.width !== realWidth) { | |
| if (!isScaling.value) { | |
| slidesStore.updateElement({ | |
| id: props.elementInfo.id, | |
| props: { width: realWidth }, | |
| }) | |
| } | |
| else realWidthCache.value = realWidth | |
| } | |
| } | |
| const resizeObserver = new ResizeObserver(updateTextElementHeight) | |
| onMounted(() => { | |
| if (elementRef.value) resizeObserver.observe(elementRef.value) | |
| }) | |
| onUnmounted(() => { | |
| if (elementRef.value) resizeObserver.unobserve(elementRef.value) | |
| }) | |
| const updateContent = (content: string, ignore = false) => { | |
| slidesStore.updateElement({ | |
| id: props.elementInfo.id, | |
| props: { content }, | |
| }) | |
| if (!ignore) addHistorySnapshot() | |
| } | |
| const checkEmptyText = debounce(function() { | |
| const pureText = props.elementInfo.content.replace(/<[^>]+>/g, '') | |
| if (!pureText) slidesStore.deleteElement(props.elementInfo.id) | |
| }, 300, { trailing: true }) | |
| const isHandleElement = computed(() => handleElementId.value === props.elementInfo.id) | |
| watch(isHandleElement, () => { | |
| if (!isHandleElement.value) checkEmptyText() | |
| }) | |
| </script> | |
| <style lang="scss" scoped> | |
| .editable-element-text { | |
| position: absolute; | |
| &.lock .element-content { | |
| cursor: default; | |
| } | |
| } | |
| .rotate-wrapper { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .element-content { | |
| position: relative; | |
| padding: 10px; | |
| line-height: 1.5; | |
| word-break: break-word; | |
| cursor: move; | |
| .text { | |
| position: relative; | |
| } | |
| ::v-deep(a) { | |
| cursor: text; | |
| } | |
| } | |
| .drag-handler { | |
| height: 10px; | |
| position: absolute; | |
| left: 0; | |
| right: 0; | |
| &.top { | |
| top: 0; | |
| } | |
| &.bottom { | |
| bottom: 0; | |
| } | |
| } | |
| </style> | |