mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加文字格式刷
This commit is contained in:
parent
5b8695e9c0
commit
10229ec8bc
28
src/hooks/useTextFormatPainter.ts
Normal file
28
src/hooks/useTextFormatPainter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMainStore } from '@/store'
|
||||
|
||||
export default () => {
|
||||
const mainStore = useMainStore()
|
||||
const { richTextAttrs, textFormatPainter } = storeToRefs(mainStore)
|
||||
|
||||
const toggleFormatPainter = () => {
|
||||
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||
else {
|
||||
mainStore.setTextFormatPainter({
|
||||
bold: richTextAttrs.value.bold,
|
||||
em: richTextAttrs.value.em,
|
||||
underline: richTextAttrs.value.underline,
|
||||
strikethrough: richTextAttrs.value.strikethrough,
|
||||
color: richTextAttrs.value.color,
|
||||
backcolor: richTextAttrs.value.backcolor,
|
||||
fontname: richTextAttrs.value.fontsize,
|
||||
fontsize: richTextAttrs.value.fontsize,
|
||||
align: richTextAttrs.value.align,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toggleFormatPainter,
|
||||
}
|
||||
}
|
@ -109,6 +109,7 @@ import {
|
||||
Needle,
|
||||
TextRotationNone,
|
||||
TextRotationDown,
|
||||
FormatBrush,
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
export const icons = {
|
||||
@ -219,6 +220,7 @@ export const icons = {
|
||||
IconNeedle: Needle,
|
||||
IconTextRotationNone: TextRotationNone,
|
||||
IconTextRotationDown: TextRotationDown,
|
||||
IconFormatBrush: FormatBrush,
|
||||
}
|
||||
|
||||
export default {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { customAlphabet } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { CreatingElement } from '@/types/edit'
|
||||
import { CreatingElement, TextFormatPainter } from '@/types/edit'
|
||||
import { ToolbarStates } from '@/types/toolbar'
|
||||
import { DialogForExportTypes } from '@/types/export'
|
||||
import { SYS_FONTS } from '@/configs/font'
|
||||
@ -31,6 +31,7 @@ export interface MainState {
|
||||
selectedSlidesIndex: number[]
|
||||
dialogForExport: DialogForExportTypes
|
||||
databaseId: string
|
||||
textFormatPainter: TextFormatPainter | null
|
||||
}
|
||||
|
||||
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')
|
||||
@ -59,6 +60,7 @@ export const useMainStore = defineStore('main', {
|
||||
selectedSlidesIndex: [], // 当前被选中的页面索引集合
|
||||
dialogForExport: '', // 导出面板
|
||||
databaseId, // 标识当前应用的indexedDB数据库ID
|
||||
textFormatPainter: null, // 文字格式刷
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@ -160,5 +162,9 @@ export const useMainStore = defineStore('main', {
|
||||
setDialogForExport(type: DialogForExportTypes) {
|
||||
this.dialogForExport = type
|
||||
},
|
||||
|
||||
setTextFormatPainter(textFormatPainter: TextFormatPainter | null) {
|
||||
this.textFormatPainter = textFormatPainter
|
||||
},
|
||||
},
|
||||
})
|
@ -90,4 +90,16 @@ export interface CreatingLineElement {
|
||||
type: 'line'
|
||||
data: LinePoolItem
|
||||
}
|
||||
export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
|
||||
export type CreatingElement = CreatingTextElement | CreatingShapeElement | CreatingLineElement
|
||||
|
||||
export interface TextFormatPainter {
|
||||
bold?: boolean
|
||||
em?: boolean
|
||||
underline?: boolean
|
||||
strikethrough?: boolean
|
||||
color?: string
|
||||
backcolor?: string
|
||||
fontsize?: string
|
||||
fontname?: string
|
||||
align?: 'left' | 'right' | 'center'
|
||||
}
|
@ -159,16 +159,18 @@ export const getAttrValueInSelection = (view: EditorView, attr: string) => {
|
||||
return value
|
||||
}
|
||||
|
||||
type Align = 'left' | 'right' | 'center'
|
||||
|
||||
interface DefaultAttrs {
|
||||
color?: string
|
||||
backcolor?: string
|
||||
fontsize?: string
|
||||
fontname?: string
|
||||
align?: string
|
||||
align?: Align
|
||||
}
|
||||
const _defaultAttrs: DefaultAttrs = {
|
||||
color: '#000',
|
||||
backcolor: '#000',
|
||||
backcolor: '',
|
||||
fontsize: '20px',
|
||||
fontname: '微软雅黑',
|
||||
align: 'left',
|
||||
@ -190,7 +192,7 @@ export const getTextAttrs = (view: EditorView, defaultAttrs: DefaultAttrs = {})
|
||||
const fontsize = getAttrValue(marks, 'fontsize', 'fontsize') || defaultAttrs.fontsize
|
||||
const fontname = getAttrValue(marks, 'fontname', 'fontname') || defaultAttrs.fontname
|
||||
const link = getAttrValue(marks, 'link', 'href') || ''
|
||||
const align = getAttrValueInSelection(view, 'align') || defaultAttrs.align
|
||||
const align = (getAttrValueInSelection(view, 'align') || defaultAttrs.align) as Align
|
||||
const isBulletList = isActiveOfParentNodeType('bullet_list', view.state)
|
||||
const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state)
|
||||
const isBlockquote = isActiveOfParentNodeType('blockquote', view.state)
|
||||
@ -232,7 +234,7 @@ export const defaultRichTextAttrs: TextAttrs = {
|
||||
subscript: false,
|
||||
code: false,
|
||||
color: '#000',
|
||||
backcolor: '#000',
|
||||
backcolor: '',
|
||||
fontsize: '20px',
|
||||
fontname: '微软雅黑',
|
||||
link: '',
|
||||
|
@ -93,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, provide, ref, watch, watchEffect } from 'vue'
|
||||
import { nextTick, onMounted, onUnmounted, provide, ref, watch, watchEffect } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
||||
@ -142,6 +142,7 @@ const {
|
||||
showRuler,
|
||||
creatingElement,
|
||||
canvasScale,
|
||||
textFormatPainter,
|
||||
} = storeToRefs(mainStore)
|
||||
const { currentSlide } = storeToRefs(useSlidesStore())
|
||||
const { ctrlKeyState, spaceKeyState } = storeToRefs(useKeyboardStore())
|
||||
@ -190,17 +191,23 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区
|
||||
// 点击画布的空白区域:清空焦点元素、设置画布焦点、清除文字选区、清空格式刷状态
|
||||
const handleClickBlankArea = (e: MouseEvent) => {
|
||||
mainStore.setActiveElementIdList([])
|
||||
if (activeElementIdList.value.length) mainStore.setActiveElementIdList([])
|
||||
|
||||
if (!spaceKeyState.value) updateMouseSelection(e)
|
||||
else dragViewport(e)
|
||||
|
||||
if (!editorAreaFocus.value) mainStore.setEditorareaFocus(true)
|
||||
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||
removeAllRanges()
|
||||
}
|
||||
|
||||
// 画布注销时清空格式刷状态
|
||||
onUnmounted(() => {
|
||||
if (textFormatPainter.value) mainStore.setTextFormatPainter(null)
|
||||
})
|
||||
|
||||
// 移除画布编辑区域焦点
|
||||
const removeEditorAreaFocus = () => {
|
||||
if (editorAreaFocus.value) mainStore.setEditorareaFocus(false)
|
||||
|
@ -87,10 +87,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||
<IconText />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Popover trigger="click">
|
||||
@ -101,10 +100,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||
<IconHighLight />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||
@ -151,6 +149,7 @@ import { WEB_FONTS } from '@/configs/font'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
import TextColorButton from '../common/TextColorButton.vue'
|
||||
|
||||
const slidesStore = useSlidesStore()
|
||||
const { richTextAttrs, availableFonts, activeElementList } = storeToRefs(useMainStore())
|
||||
@ -251,18 +250,6 @@ const updateFontStyle = (command: string, value: string) => {
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.text-color-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.text-color-block {
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.font-size-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -111,10 +111,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||
<IconText />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Popover trigger="click">
|
||||
@ -125,10 +124,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||
<IconHighLight />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||
@ -169,14 +167,33 @@
|
||||
@click="emitRichTextCommand('underline')"
|
||||
><IconTextUnderline /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="删除线">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="richTextAttrs.strikethrough"
|
||||
@click="emitRichTextCommand('strikethrough')"
|
||||
><IconStrikethrough /></CheckboxButton>
|
||||
</Tooltip>
|
||||
</CheckboxButtonGroup>
|
||||
|
||||
<CheckboxButtonGroup class="row">
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
@click="emitRichTextCommand('clear')"
|
||||
><IconFormat /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="格式刷">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="!!textFormatPainter"
|
||||
@click="toggleFormatPainter()"
|
||||
><IconFormatBrush /></CheckboxButton>
|
||||
</Tooltip>
|
||||
</CheckboxButtonGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
<RadioGroup
|
||||
class="row"
|
||||
button-style="solid"
|
||||
@ -230,16 +247,18 @@ import { PPTShapeElement, ShapeGradient, ShapeText } from '@/types/slides'
|
||||
import { WEB_FONTS } from '@/configs/font'
|
||||
import emitter, { EmitterEvents } from '@/utils/emitter'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
|
||||
|
||||
import ElementOpacity from '../common/ElementOpacity.vue'
|
||||
import ElementOutline from '../common/ElementOutline.vue'
|
||||
import ElementShadow from '../common/ElementShadow.vue'
|
||||
import ElementFlip from '../common/ElementFlip.vue'
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
import TextColorButton from '../common/TextColorButton.vue'
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const slidesStore = useSlidesStore()
|
||||
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(mainStore)
|
||||
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(mainStore)
|
||||
|
||||
const handleShapeElement = handleElement as Ref<PPTShapeElement>
|
||||
|
||||
@ -262,6 +281,7 @@ watch(handleElement, () => {
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
const { toggleFormatPainter } = useTextFormatPainter()
|
||||
|
||||
const updateElement = (props: Partial<PPTShapeElement>) => {
|
||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||
@ -323,18 +343,6 @@ const emitRichTextCommand = (command: string, value?: string) => {
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.text-color-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.text-color-block {
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.font-size-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -39,10 +39,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||
<Button class="text-color-btn" style="flex: 1;">
|
||||
<TextColorButton :color="textAttrs.color" style="flex: 1;">
|
||||
<IconText />
|
||||
<div class="text-color-block" :style="{ backgroundColor: textAttrs.color }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Popover trigger="click">
|
||||
@ -53,10 +52,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="单元格填充">
|
||||
<Button class="text-color-btn" style="flex: 1;">
|
||||
<TextColorButton :color="textAttrs.backcolor" style="flex: 1;">
|
||||
<IconFill />
|
||||
<div class="text-color-block" :style="{ backgroundColor: textAttrs.backcolor }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
@ -196,6 +194,7 @@ import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
||||
import ElementOutline from '../common/ElementOutline.vue'
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
import TextColorButton from '../common/TextColorButton.vue'
|
||||
|
||||
const slidesStore = useSlidesStore()
|
||||
const { handleElement, handleElementId, selectedTableCells: selectedCells, availableFonts } = storeToRefs(useMainStore())
|
||||
@ -211,7 +210,7 @@ const textAttrs = ref({
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
color: '#000',
|
||||
backcolor: '#000',
|
||||
backcolor: '',
|
||||
fontsize: '12px',
|
||||
fontname: '微软雅黑',
|
||||
align: 'left',
|
||||
@ -259,7 +258,7 @@ const updateTextAttrState = () => {
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
color: '#000',
|
||||
backcolor: '#000',
|
||||
backcolor: '',
|
||||
fontsize: '12px',
|
||||
fontname: '微软雅黑',
|
||||
align: 'left',
|
||||
@ -272,7 +271,7 @@ const updateTextAttrState = () => {
|
||||
underline: !!style.underline,
|
||||
strikethrough: !!style.strikethrough,
|
||||
color: style.color || '#000',
|
||||
backcolor: style.backcolor || '#000',
|
||||
backcolor: style.backcolor || '',
|
||||
fontsize: style.fontsize || '12px',
|
||||
fontname: style.fontname || '微软雅黑',
|
||||
align: style.align || 'left',
|
||||
@ -404,17 +403,6 @@ const setTableCol = (value: number) => {
|
||||
.switch-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
.text-color-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.text-color-block {
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.set-count {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -51,10 +51,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字颜色">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.color" style="flex: 3;">
|
||||
<IconText />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.color }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Popover trigger="click">
|
||||
@ -65,10 +64,9 @@
|
||||
/>
|
||||
</template>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="文字高亮">
|
||||
<Button class="text-color-btn" style="flex: 3;">
|
||||
<TextColorButton :color="richTextAttrs.backcolor" style="flex: 3;">
|
||||
<IconHighLight />
|
||||
<div class="text-color-block" :style="{ backgroundColor: richTextAttrs.backcolor }"></div>
|
||||
</Button>
|
||||
</TextColorButton>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大字号">
|
||||
@ -116,12 +114,6 @@
|
||||
@click="emitRichTextCommand('strikethrough')"
|
||||
><IconStrikethrough /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
@click="emitRichTextCommand('clear')"
|
||||
><IconFormat /></CheckboxButton>
|
||||
</Tooltip>
|
||||
</CheckboxButtonGroup>
|
||||
|
||||
<CheckboxButtonGroup class="row">
|
||||
@ -153,6 +145,22 @@
|
||||
@click="emitRichTextCommand('blockquote')"
|
||||
><IconQuote /></CheckboxButton>
|
||||
</Tooltip>
|
||||
</CheckboxButtonGroup>
|
||||
|
||||
<CheckboxButtonGroup class="row">
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
@click="emitRichTextCommand('clear')"
|
||||
><IconFormat /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="格式刷">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="!!textFormatPainter"
|
||||
@click="toggleFormatPainter()"
|
||||
><IconFormatBrush /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="超链接">
|
||||
<Popover placement="bottomRight" trigger="click" v-model:visible="linkPopoverVisible">
|
||||
<template #content>
|
||||
@ -278,12 +286,15 @@ import { PPTTextElement } from '@/types/slides'
|
||||
import emitter, { EmitterEvents, RichTextAction } from '@/utils/emitter'
|
||||
import { WEB_FONTS } from '@/configs/font'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
import useTextFormatPainter from '@/hooks/useTextFormatPainter'
|
||||
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
import ElementOpacity from '../common/ElementOpacity.vue'
|
||||
import ElementOutline from '../common/ElementOutline.vue'
|
||||
import ElementShadow from '../common/ElementShadow.vue'
|
||||
import ColorButton from '../common/ColorButton.vue'
|
||||
import TextColorButton from '../common/TextColorButton.vue'
|
||||
|
||||
// 注意,存在一个未知原因的BUG,如果文本加粗后文本框高度增加,画布的可视区域定位会出现错误
|
||||
// 因此在执行预置样式命令时,将加粗命令放在尽可能靠前的位置,避免字号增大后再加粗
|
||||
@ -360,10 +371,12 @@ const presetStyles = [
|
||||
},
|
||||
]
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const slidesStore = useSlidesStore()
|
||||
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore())
|
||||
const { handleElement, handleElementId, richTextAttrs, availableFonts, textFormatPainter } = storeToRefs(mainStore)
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
const { toggleFormatPainter } = useTextFormatPainter()
|
||||
|
||||
const updateElement = (props: Partial<PPTTextElement>) => {
|
||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||
@ -491,18 +504,6 @@ const updateLink = (link?: string) => {
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
.text-color-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.text-color-block {
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.font-size-btn {
|
||||
padding: 0;
|
||||
}
|
||||
|
38
src/views/Editor/Toolbar/common/TextColorButton.vue
Normal file
38
src/views/Editor/Toolbar/common/TextColorButton.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Button class="text-color-btn">
|
||||
<slot></slot>
|
||||
<div class="text-color-block">
|
||||
<div class="text-color-block-content" :style="{ backgroundColor: color }"></div>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-color-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
.text-color-block {
|
||||
width: 17px;
|
||||
height: 4px;
|
||||
margin-top: 1px;
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACdJREFUGFdjfPbs2X8GBgYGSUlJEMXAiCHw//9/sIrnz59DVKALAADNxxVfaiODNQAAAABJRU5ErkJggg==);
|
||||
|
||||
.text-color-block-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="prosemirror-editor"
|
||||
:class="{ 'format-painter': textFormatPainter }"
|
||||
ref="editorViewRef"
|
||||
></div>
|
||||
</template>
|
||||
@ -14,7 +15,7 @@ import { EditorView } from 'prosemirror-view'
|
||||
import { toggleMark, wrapIn } from 'prosemirror-commands'
|
||||
import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
|
||||
import { findNodesWithSameMark, getTextAttrs, autoSelectAll, addMark, markActive, getFontsize } from '@/utils/prosemirror/utils'
|
||||
import emitter, { EmitterEvents, RichTextCommand } from '@/utils/emitter'
|
||||
import emitter, { EmitterEvents, RichTextAction, RichTextCommand } from '@/utils/emitter'
|
||||
import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign'
|
||||
import { indentCommand } from '@/utils/prosemirror/commands/setTextIndent'
|
||||
import { toggleList } from '@/utils/prosemirror/commands/toggleList'
|
||||
@ -53,7 +54,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const mainStore = useMainStore()
|
||||
const { handleElementId } = storeToRefs(mainStore)
|
||||
const { handleElementId, textFormatPainter } = storeToRefs(mainStore)
|
||||
|
||||
const editorViewRef = ref<HTMLElement>()
|
||||
let editorView: EditorView
|
||||
@ -104,23 +105,6 @@ watch(() => props.editable, () => {
|
||||
editorView.setProps({ editable: () => props.editable })
|
||||
})
|
||||
|
||||
// Prosemirror编辑器的初始化和卸载
|
||||
onMounted(() => {
|
||||
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
||||
handleDOMEvents: {
|
||||
focus: handleFocus,
|
||||
blur: handleBlur,
|
||||
keydown: handleKeydown,
|
||||
click: handleClick,
|
||||
},
|
||||
editable: () => props.editable,
|
||||
})
|
||||
if (props.autoFocus) editorView.focus()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
editorView && editorView.destroy()
|
||||
})
|
||||
|
||||
// 暴露 focus 方法
|
||||
const focus = () => editorView.focus()
|
||||
defineExpose({ focus })
|
||||
@ -249,6 +233,38 @@ const execCommand = ({ target, action }: RichTextCommand) => {
|
||||
handleClick()
|
||||
}
|
||||
|
||||
// 鼠标抬起时,执行格式刷命令
|
||||
const handleMouseup = () => {
|
||||
if (!textFormatPainter.value) return
|
||||
|
||||
const actions: RichTextAction[] = [{ command: 'clear' }]
|
||||
for (const key of Object.keys(textFormatPainter.value)) {
|
||||
const command = key
|
||||
const value = textFormatPainter.value[key]
|
||||
if (value) actions.push({ command, value })
|
||||
}
|
||||
execCommand({ action: actions })
|
||||
mainStore.setTextFormatPainter(null)
|
||||
}
|
||||
|
||||
// Prosemirror编辑器的初始化和卸载
|
||||
onMounted(() => {
|
||||
editorView = initProsemirrorEditor((editorViewRef.value as Element), textContent.value, {
|
||||
handleDOMEvents: {
|
||||
focus: handleFocus,
|
||||
blur: handleBlur,
|
||||
keydown: handleKeydown,
|
||||
click: handleClick,
|
||||
mouseup: handleMouseup,
|
||||
},
|
||||
editable: () => props.editable,
|
||||
})
|
||||
if (props.autoFocus) editorView.focus()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
editorView && editorView.destroy()
|
||||
})
|
||||
|
||||
emitter.on(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
||||
onUnmounted(() => {
|
||||
emitter.off(EmitterEvents.RICH_TEXT_COMMAND, execCommand)
|
||||
@ -258,5 +274,9 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.prosemirror-editor {
|
||||
cursor: text;
|
||||
|
||||
&.format-painter {
|
||||
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAVCAYAAACzK0UYAAAABHNCSVQICAgIfAhkiAAAAVRJREFUSInt1DFuwjAYBeCXUrFavgBN9yB6AKR6Bi7AVLrlBpFYgAUpp2i37AysVDIXcCIuwJRMEEYk9LrQDlVQ7EiVOvSt/v1/tmUbeZ7TGMPL5WLgEJLzNE2ptabWmsfjkTeLjTGUUvJ8Pjsjo9GIUkpKKam1voncuTRumn/EKfd1BSQnAF4qhvyK2k1VD88YQ6UUiqJI2+12r2LiPI7j2Xa7rV9yRZbLpRWiAKhGwjW1x3XN828jD9PpVK3X60bAarWy20lZltjv940QwO4KPzbu7oCgLMu/g3Q6ncZI73Q6WSFhGDZGnrIss0LG4zGEEG4ISZUkiW8DDAYDCCEQBIEbAmAWx7GNgSiKAOB1OBzaIyQnSZIom/cRRRG63e7C87z3MAw/fu7Gy/OcRVEgCIK01Wp9/10k37Ism9TdLCHEFzC/zvMPh8Nmt9v5ANDv9/EJD8ykxYswZDkAAAAASUVORK5CYII=) 1 10, default !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user