feat: 添加文字格式刷

This commit is contained in:
pipipi-pikachu 2022-09-17 17:51:54 +08:00
parent 5b8695e9c0
commit 10229ec8bc
12 changed files with 209 additions and 110 deletions

View 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,
}
}

View File

@ -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 {

View File

@ -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
},
},
})

View File

@ -91,3 +91,15 @@ export interface CreatingLineElement {
data: LinePoolItem
}
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'
}

View File

@ -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: '',

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View 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>

View File

@ -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>