| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- <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' : elementInfo.height + 'px',
- 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"
- />
- <div class="shape-text" :style="elementInfo.style" :class="[elementInfo.align, { 'editable': editable || elementInfo.content }]">
- <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>
- <!-- 当字号过大且行高较小时,会出现文字高度溢出的情况,导致拖拽区域无法被选中,因此添加了以下节点避免该情况 -->
- <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, useTemplateRef } 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 = useTemplateRef<HTMLElement>('elementRef')
- 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;
- 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";
- .text {
- position: relative;
- }
- ::v-deep(a) {
- cursor: text;
- }
- }
- .drag-handler {
- height: 10px;
- position: absolute;
- left: 0;
- right: 0;
- &.top {
- top: 0;
- }
- &.bottom {
- bottom: 0;
- }
- }
- .shape-text {
- width:100%;
- height:100%;
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- display: flex;
- flex-direction: column;
- word-break: break-word;
- pointer-events: none;
- white-space: normal;
- &.editable {
- pointer-events: all;
- }
- &.top {
- justify-content: flex-start;
- }
- &.middle {
- justify-content: center;
- left: 50%;
- top: 50%;
- -webkit-transform: translate(-50%,-50%);
- transform: translate(-50%,-50%);
- }
- &.bottom {
- justify-content: flex-end;
- }
- }
- </style>
|