feat: 添加竖向文本框

This commit is contained in:
pipipi-pikachu 2022-08-19 20:03:03 +08:00
parent e1cbd11146
commit d245de2a40
16 changed files with 167 additions and 28 deletions

14
src/components.d.ts vendored
View File

@ -1,11 +1,21 @@
import { icons } from '@/plugins/icon'
import { components } from '@/plugins/component'
import FileInput from '@/components/FileInput.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import FullscreenSpin from '@/components/FullscreenSpin.vue'
type Icon = typeof icons
type CustomComponent = typeof components
declare module 'vue' {
export interface GlobalComponents extends Icon, CustomComponent {
export interface GlobalComponents extends Icon {
FileInput: typeof FileInput
CheckboxButton: typeof CheckboxButton
CheckboxButtonGroup: typeof CheckboxButtonGroup
ColorPicker: typeof ColorPicker
FullscreenSpin: typeof FullscreenSpin
// antd 组件
InputNumber: typeof import('ant-design-vue')['InputNumber']

View File

@ -172,8 +172,16 @@ export default () => {
* @param position
* @param content
*/
const createTextElement = (position: CommonElementPosition, content = '') => {
interface CreateTextData {
content?: string
vertical?: boolean
}
const createTextElement = (position: CommonElementPosition, data?: CreateTextData) => {
const { left, top, width, height } = position
const content = data?.content || ''
const vertical = data?.vertical || false
const id = nanoid(10)
createElement({
type: 'text',
@ -186,6 +194,7 @@ export default () => {
rotate: 0,
defaultFontName: theme.value.fontName,
defaultColor: theme.value.fontColor,
vertical,
}, () => {
setTimeout(() => {
const editorRef: HTMLElement | null = document.querySelector(`#editable-element-${id} .ProseMirror`)

View File

@ -442,6 +442,7 @@ export default () => {
if (el.outline?.width) options.line = getOutlineOption(el.outline)
if (el.opacity !== undefined) options.transparency = (1 - el.opacity) * 100
if (el.paragraphSpace !== undefined) options.paraSpaceBefore = el.paragraphSpace * 0.75
if (el.vertical) options.vert = 'eaVert'
pptxSlide.addText(textProps, options)
}

View File

@ -22,7 +22,7 @@ export default () => {
top: 0,
width: 600,
height: 50,
}, text)
}, { content: text })
}
/**

View File

@ -6,7 +6,7 @@ import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import FullscreenSpin from '@/components/FullscreenSpin.vue'
export const components = {
const components = {
FileInput,
CheckboxButton,
CheckboxButtonGroup,

View File

@ -107,6 +107,8 @@ import {
Square,
Round,
Needle,
TextRotationNone,
TextRotationDown,
} from '@icon-park/vue-next'
export const icons = {
@ -215,6 +217,8 @@ export const icons = {
IconSquare: Square,
IconRound: Round,
IconNeedle: Needle,
IconTextRotationNone: TextRotationNone,
IconTextRotationDown: TextRotationDown,
}
export default {

View File

@ -80,6 +80,7 @@ export interface CreateElementSelectionData {
export interface CreatingTextElement {
type: 'text'
vertical?: boolean
}
export interface CreatingShapeElement {
type: 'shape'

View File

@ -139,6 +139,8 @@ interface PPTBaseElement {
* textIndent?: 段落首行缩进
*
* paragraphSpace?: 段间距 5px
*
* vertical?: 竖向文本
*/
export interface PPTTextElement extends PPTBaseElement {
type: 'text'
@ -153,6 +155,7 @@ export interface PPTTextElement extends PPTBaseElement {
shadow?: PPTElementShadow
textIndent?: number
paragraphSpace?: number
vertical?: boolean
}

View File

@ -10,7 +10,7 @@
<template v-if="handlerVisible">
<ResizeHandler
class="operate-resize-handler"
v-for="point in textElementResizeHandlers"
v-for="point in resizeHandlers"
:key="point.direction"
:type="point.direction"
:rotate="elementInfo.rotate"
@ -68,5 +68,6 @@ const { canvasScale } = storeToRefs(useMainStore())
const scaleWidth = computed(() => props.elementInfo.width * canvasScale.value)
const scaleHeight = computed(() => props.elementInfo.height * canvasScale.value)
const { textElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const { textElementResizeHandlers, verticalTextElementResizeHandlers, borderLines } = useCommonOperate(scaleWidth, scaleHeight)
const resizeHandlers = computed(() => props.elementInfo.vertical ? verticalTextElementResizeHandlers.value : textElementResizeHandlers.value)
</script>

View File

@ -23,6 +23,12 @@ export default (width: Ref<number>, height: Ref<number>) => {
{ direction: OperateResizeHandlers.RIGHT, style: {left: width.value + 'px', top: height.value / 2 + 'px'} },
]
})
const verticalTextElementResizeHandlers = computed(() => {
return [
{ direction: OperateResizeHandlers.TOP, style: {left: width.value / 2 + 'px'} },
{ direction: OperateResizeHandlers.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
]
})
// 元素选中边框线
const borderLines = computed(() => {
@ -37,6 +43,7 @@ export default (width: Ref<number>, height: Ref<number>) => {
return {
resizeHandlers,
textElementResizeHandlers,
verticalTextElementResizeHandlers,
borderLines,
}
}

View File

@ -31,7 +31,7 @@ export default (elementRef: Ref<HTMLElement | undefined>) => {
top: 0,
width: 600,
height: 50,
}, string)
}, { content: string })
})
}
}

View File

@ -75,15 +75,15 @@ export default (viewportRef: Ref<HTMLElement | undefined>) => {
const type = creatingElement.value.type
if (type === 'text') {
const position = formatCreateSelection(selectionData)
position && createTextElement(position)
position && createTextElement(position, { vertical: creatingElement.value.vertical })
}
else if (type === 'shape') {
const position = formatCreateSelection(selectionData)
position && createShapeElement(position, (creatingElement.value as CreatingShapeElement).data)
position && createShapeElement(position, creatingElement.value.data)
}
else if (type === 'line') {
const position = formatCreateSelectionForLine(selectionData)
position && createLineElement(position, (creatingElement.value as CreatingLineElement).data)
position && createLineElement(position, creatingElement.value.data)
}
mainStore.setCreatingElement(null)
}

View File

@ -11,7 +11,17 @@
<div class="add-element-handler">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入文字">
<IconFontSize class="handler-item" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
<div class="handler-item group-btn">
<IconFontSize class="icon" :class="{ 'active': creatingElement?.type === 'text' }" @click="drawText()" />
<Popover trigger="click" v-model:visible="textTypeSelectVisible">
<template #content>
<div class="text-type-item" @click="() => { drawText(); textTypeSelectVisible = false }"><IconTextRotationNone /> 横向文本框</div>
<div class="text-type-item" @click="() => { drawText(true); textTypeSelectVisible = false }"><IconTextRotationDown /> 竖向文本框</div>
</template>
<IconDown class="arrow" />
</Popover>
</div>
</Tooltip>
<FileInput @change="files => insertImageElement(files)">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="插入图片">
@ -166,11 +176,13 @@ const chartPoolVisible = ref(false)
const tableGeneratorVisible = ref(false)
const mediaInputVisible = ref(false)
const latexEditorVisible = ref(false)
const textTypeSelectVisible = ref(false)
//
const drawText = () => {
const drawText = (vertical = false) => {
mainStore.setCreatingElement({
type: 'text',
vertical,
})
}
@ -214,18 +226,69 @@ const drawLine = (line: LinePoolItem) => {
left: 50%;
transform: translate(-50%, -50%);
display: flex;
.handler-item {
width: 32px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
margin: 0 2px;
border-radius: $borderRadius;
&:not(.group-btn):hover {
background-color: #f1f1f1;
}
&.active {
color: $themeColor;
}
&.group-btn {
width: auto;
margin-right: 4px;
&:hover {
background-color: #f3f3f3;
}
.icon, .arrow {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.icon {
width: 26px;
padding: 0 2px;
&:hover {
background-color: #e9e9e9;
}
&.active {
color: $themeColor;
}
}
.arrow {
font-size: 12px;
&:hover {
background-color: #e9e9e9;
}
}
}
}
}
.handler-item {
margin: 0 10px;
font-size: 14px;
overflow: hidden;
cursor: pointer;
&.disable {
opacity: .5;
}
&.active {
color: $themeColor;
}
}
.right-handler {
display: flex;
@ -250,4 +313,16 @@ const drawLine = (line: LinePoolItem) => {
color: $themeColor;
}
}
.text-type-item {
padding: 5px 10px;
cursor: pointer;
&:hover {
background-color: #f1f1f1;
}
& + .text-type-item {
margin-top: 3px;
}
}
</style>

View File

@ -56,7 +56,7 @@ const insertTextElement = () => {
top: (VIEWPORT_SIZE * viewportRatio.value - height) / 2,
width,
height,
}, '<p>新添加文本</p>')
}, { content: '<p>新添加文本</p>' })
}
const insertImageElement = (files: FileList) => {

View File

@ -4,7 +4,8 @@
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
height: elementInfo.vertical ? elementInfo.height + 'px' : 'auto',
}"
>
<div
@ -14,6 +15,8 @@
<div
class="element-content"
:style="{
width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
height: elementInfo.vertical ? elementInfo.height + 'px' : 'auto',
backgroundColor: elementInfo.fill,
opacity: elementInfo.opacity,
textShadow: shadowStyle,
@ -21,6 +24,7 @@
letterSpacing: (elementInfo.wordSpace || 0) + 'px',
color: elementInfo.defaultColor,
fontFamily: elementInfo.defaultFontName,
writingMode: elementInfo.vertical ? 'vertical-rl' : 'horizontal-tb',
}"
>
<ElementOutline

View File

@ -1,12 +1,12 @@
<template>
<div
class="editable-element-text"
ref="elementRef"
:class="{ 'lock': elementInfo.lock }"
:style="{
top: elementInfo.top + 'px',
left: elementInfo.left + 'px',
width: elementInfo.width + 'px',
width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
height: elementInfo.vertical ? elementInfo.height + 'px' : 'auto',
}"
>
<div
@ -15,7 +15,10 @@
>
<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,
@ -23,6 +26,7 @@
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)"
@ -104,26 +108,37 @@ const handleSelectElement = (e: MouseEvent | TouchEvent, canMove = true) => {
// vuex
//
const realHeightCache = ref(-1)
const realWidthCache = ref(-1)
watch(isScaling, () => {
if (handleElementId.value !== props.elementInfo.id) return
if (!isScaling.value && realHeightCache.value !== -1) {
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
const realHeight = contentRect.height + 20
const realWidth = contentRect.width + 20
if (props.elementInfo.height !== realHeight) {
if (!props.elementInfo.vertical && props.elementInfo.height !== realHeight) {
if (!isScaling.value) {
slidesStore.updateElement({
id: props.elementInfo.id,
@ -132,6 +147,15 @@ const updateTextElementHeight = (entries: ResizeObserverEntry[]) => {
}
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)