feat: 支持段落首行缩进

This commit is contained in:
pipipi-pikachu 2023-07-30 14:27:58 +08:00
parent 399abc0859
commit 8ff70da477
8 changed files with 106 additions and 54 deletions

View File

@ -17,7 +17,6 @@
} }
p { p {
text-indent: var(--textIndent);
margin-top: var(--paragraphSpace); margin-top: var(--paragraphSpace);
} }
p:first-child { p:first-child {
@ -61,25 +60,28 @@
} }
[data-indent='1'] { [data-indent='1'] {
padding-left: 48px; padding-left: 24px;
} }
[data-indent='2'] { [data-indent='2'] {
padding-left: 96px; padding-left: 48px;
} }
[data-indent='3'] { [data-indent='3'] {
padding-left: 144px; padding-left: 72px;
} }
[data-indent='4'] { [data-indent='4'] {
padding-left: 192px; padding-left: 96px;
} }
[data-indent='5'] { [data-indent='5'] {
padding-left: 240px; padding-left: 120px;
} }
[data-indent='6'] { [data-indent='6'] {
padding-left: 288px; padding-left: 144px;
} }
[data-indent='7'] { [data-indent='7'] {
padding-left: 336px; padding-left: 168px;
}
[data-indent='8'] {
padding-left: 192px;
} }
} }

View File

@ -140,8 +140,6 @@ interface PPTBaseElement {
* *
* shadow?: 阴影 * shadow?: 阴影
* *
* textIndent?: 段落首行缩进
*
* paragraphSpace?: 段间距 5px * paragraphSpace?: 段间距 5px
* *
* vertical?: 竖向文本 * vertical?: 竖向文本
@ -157,7 +155,6 @@ export interface PPTTextElement extends PPTBaseElement {
wordSpace?: number wordSpace?: number
opacity?: number opacity?: number
shadow?: PPTElementShadow shadow?: PPTElementShadow
textIndent?: number
paragraphSpace?: number paragraphSpace?: number
vertical?: boolean vertical?: boolean
} }

View File

@ -3,30 +3,32 @@ import { type Transaction, TextSelection, AllSelection } from 'prosemirror-state
import type { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { isList } from './toggleList' import { isList } from './toggleList'
function setNodeIndentMarkup(tr: Transaction, pos: number, delta: number): Transaction { type IndentKey = 'indent' | 'textIndent'
function setNodeIndentMarkup(tr: Transaction, pos: number, delta: number, indentKey: IndentKey): Transaction {
if (!tr.doc) return tr if (!tr.doc) return tr
const node = tr.doc.nodeAt(pos) const node = tr.doc.nodeAt(pos)
if (!node) return tr if (!node) return tr
const minIndent = 0 const minIndent = 0
const maxIndent = 7 const maxIndent = 8
let indent = (node.attrs.indent || 0) + delta let indent = (node.attrs[indentKey] || 0) + delta
if (indent < minIndent) indent = minIndent if (indent < minIndent) indent = minIndent
if (indent > maxIndent) indent = maxIndent if (indent > maxIndent) indent = maxIndent
if (indent === node.attrs.indent) return tr if (indent === node.attrs[indentKey]) return tr
const nodeAttrs = { const nodeAttrs = {
...node.attrs, ...node.attrs,
indent, [indentKey]: indent,
} }
return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks) return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks)
} }
const setTextIndent = (tr: Transaction, schema: Schema, delta: number): Transaction => { const setIndent = (tr: Transaction, schema: Schema, delta: number, indentKey: IndentKey): Transaction => {
const { selection, doc } = tr const { selection, doc } = tr
if (!selection || !doc) return tr if (!selection || !doc) return tr
@ -38,7 +40,7 @@ const setTextIndent = (tr: Transaction, schema: Schema, delta: number): Transact
const nodeType = node.type const nodeType = node.type
if (nodeType.name === 'paragraph' || nodeType.name === 'blockquote') { if (nodeType.name === 'paragraph' || nodeType.name === 'blockquote') {
tr = setNodeIndentMarkup(tr, pos, delta) tr = setNodeIndentMarkup(tr, pos, delta, indentKey)
return false return false
} }
else if (isList(node, schema)) return false else if (isList(node, schema)) return false
@ -52,10 +54,29 @@ export const indentCommand = (view: EditorView, delta: number) => {
const { state } = view const { state } = view
const { schema, selection } = state const { schema, selection } = state
const tr = setTextIndent( const tr = setIndent(
state.tr.setSelection(selection), state.tr.setSelection(selection),
schema, schema,
delta, delta,
'indent',
)
if (tr.docChanged) {
view.dispatch(tr)
return true
}
return false
}
export const textIndentCommand = (view: EditorView, delta: number) => {
const { state } = view
const { schema, selection } = state
const tr = setIndent(
state.tr.setSelection(selection),
schema,
delta,
'textIndent',
) )
if (tr.docChanged) { if (tr.docChanged) {
view.dispatch(tr) view.dispatch(tr)

View File

@ -84,6 +84,9 @@ const paragraph: NodeSpec = {
indent: { indent: {
default: 0, default: 0,
}, },
textIndent: {
default: 0,
},
}, },
content: 'inline*', content: 'inline*',
group: 'block', group: 'block',
@ -91,14 +94,20 @@ const paragraph: NodeSpec = {
{ {
tag: 'p', tag: 'p',
getAttrs: dom => { getAttrs: dom => {
const { textAlign } = (dom as HTMLElement).style const { textAlign, textIndent } = (dom as HTMLElement).style
let align = (dom as HTMLElement).getAttribute('align') || textAlign || '' let align = (dom as HTMLElement).getAttribute('align') || textAlign || ''
align = /(left|right|center|justify)/.test(align) ? align : '' align = /(left|right|center|justify)/.test(align) ? align : ''
let textIndentLevel = 0
if (textIndent) {
textIndentLevel = Math.floor(parseInt(textIndent) / 24)
if (!textIndentLevel) textIndentLevel = 1
}
const indent = +((dom as HTMLElement).getAttribute('data-indent') || 0) const indent = +((dom as HTMLElement).getAttribute('data-indent') || 0)
return { align, indent } return { align, indent, textIndent: textIndentLevel }
} }
}, },
{ {
@ -111,9 +120,10 @@ const paragraph: NodeSpec = {
}, },
], ],
toDOM: (node: Node) => { toDOM: (node: Node) => {
const { align, indent } = node.attrs const { align, indent, textIndent } = node.attrs
let style = '' let style = ''
if (align && align !== 'left') style += `text-align: ${align};` if (align && align !== 'left') style += `text-align: ${align};`
if (textIndent) style += `text-indent: ${textIndent * 24}px;`
const attr: Attr = { style } const attr: Attr = { style }
if (indent) attr['data-indent'] = indent if (indent) attr['data-indent'] = indent

View File

@ -256,14 +256,35 @@
</ButtonGroup> </ButtonGroup>
</div> </div>
<ButtonGroup class="row"> <div class="row">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="减小缩进"> <ButtonGroup style="flex: 15;">
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button> <Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="减小段落缩进">
</Tooltip> <Button style="flex: 1;" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大缩进"> </Tooltip>
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button> <Popover trigger="click" v-model:open="indentLeftPanelVisible">
</Tooltip> <template #content>
</ButtonGroup> <div class="popover-list">
<span class="popover-item" @click="emitRichTextCommand('textIndent', '-1')">减小首行缩进</span>
</div>
</template>
<Button class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
<div style="flex: 1;"></div>
<ButtonGroup style="flex: 15;">
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大段落缩进">
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
</Tooltip>
<Popover trigger="click" v-model:open="indentRightPanelVisible">
<template #content>
<div class="popover-list">
<span class="popover-item" @click="emitRichTextCommand('textIndent', '+1')">增大首行缩进</span>
</div>
</template>
<Button class="popover-btn"><IconDown /></Button>
</Popover>
</ButtonGroup>
</div>
<Divider /> <Divider />
@ -288,13 +309,6 @@
<SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}px</SelectOption> <SelectOption v-for="item in wordSpaceOptions" :key="item" :value="item">{{item}}px</SelectOption>
</Select> </Select>
</div> </div>
<div class="row">
<div style="flex: 2;">首行缩进</div>
<Select style="flex: 3;" :value="textIndent" @change="value => updateTextIndent(value as number)">
<template #suffixIcon><IconIndentRight /></template>
<SelectOption v-for="item in textIndentOptions" :key="item" :value="item">{{item}}px</SelectOption>
</Select>
</div>
<div class="row"> <div class="row">
<div style="flex: 2;">文本框填充</div> <div style="flex: 2;">文本框填充</div>
<Popover trigger="click"> <Popover trigger="click">
@ -439,6 +453,8 @@ const updateElement = (props: Partial<PPTTextElement>) => {
const bulletListPanelVisible = ref(false) const bulletListPanelVisible = ref(false)
const orderedListPanelVisible = ref(false) const orderedListPanelVisible = ref(false)
const indentLeftPanelVisible = ref(false)
const indentRightPanelVisible = ref(false)
const bulletListStyleTypeOption = ref(['disc', 'circle', 'square']) const bulletListStyleTypeOption = ref(['disc', 'circle', 'square'])
const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek']) const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha', 'lower-greek'])
@ -446,7 +462,6 @@ const orderedListStyleTypeOption = ref(['decimal', 'lower-roman', 'upper-roman',
const fill = ref<string>('#000') const fill = ref<string>('#000')
const lineHeight = ref<number>() const lineHeight = ref<number>()
const wordSpace = ref<number>() const wordSpace = ref<number>()
const textIndent = ref<number>()
const paragraphSpace = ref<number>() const paragraphSpace = ref<number>()
watch(handleElement, () => { watch(handleElement, () => {
@ -455,7 +470,6 @@ watch(handleElement, () => {
fill.value = handleElement.value.fill || '#fff' fill.value = handleElement.value.fill || '#fff'
lineHeight.value = handleElement.value.lineHeight || 1.5 lineHeight.value = handleElement.value.lineHeight || 1.5
wordSpace.value = handleElement.value.wordSpace || 0 wordSpace.value = handleElement.value.wordSpace || 0
textIndent.value = handleElement.value.textIndent || 0
paragraphSpace.value = handleElement.value.paragraphSpace === undefined ? 5 : handleElement.value.paragraphSpace paragraphSpace.value = handleElement.value.paragraphSpace === undefined ? 5 : handleElement.value.paragraphSpace
}, { deep: true, immediate: true }) }, { deep: true, immediate: true })
@ -466,7 +480,6 @@ const fontSizeOptions = [
] ]
const lineHeightOptions = [0.9, 1.0, 1.15, 1.2, 1.4, 1.5, 1.8, 2.0, 2.5, 3.0] const lineHeightOptions = [0.9, 1.0, 1.15, 1.2, 1.4, 1.5, 1.8, 2.0, 2.5, 3.0]
const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 6, 8, 10] const wordSpaceOptions = [0, 1, 2, 3, 4, 5, 6, 8, 10]
const textIndentOptions = [0, 48, 96, 144, 192, 240, 288, 336]
const paragraphSpaceOptions = [0, 5, 10, 15, 20, 25, 30, 40, 50, 80] const paragraphSpaceOptions = [0, 5, 10, 15, 20, 25, 30, 40, 50, 80]
// //
@ -484,11 +497,6 @@ const updateWordSpace = (value: number) => {
updateElement({ wordSpace: value }) updateElement({ wordSpace: value })
} }
//
const updateTextIndent = (value: number) => {
updateElement({ textIndent: value })
}
// //
const updateFill = (value: string) => { const updateFill = (value: string) => {
updateElement({ fill: value }) updateElement({ fill: value })
@ -626,6 +634,21 @@ const updateLink = (link?: string) => {
background-color: #666; background-color: #666;
} }
} }
.popover-list {
display: flex;
flex-direction: column;
padding: 5px;
margin: -12px;
}
.popover-item {
padding: 9px 12px;
border-radius: 2px;
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
}
.popover-btn { .popover-btn {
padding: 0 3px; padding: 0 3px;
} }

View File

@ -18,7 +18,7 @@ import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
import { findNodesWithSameMark, getTextAttrs, autoSelectAll, addMark, markActive, getFontsize } from '@/utils/prosemirror/utils' import { findNodesWithSameMark, getTextAttrs, autoSelectAll, addMark, markActive, getFontsize } from '@/utils/prosemirror/utils'
import emitter, { EmitterEvents, type RichTextAction, type RichTextCommand } from '@/utils/emitter' import emitter, { EmitterEvents, type RichTextAction, type RichTextCommand } from '@/utils/emitter'
import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign' import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign'
import { indentCommand } from '@/utils/prosemirror/commands/setTextIndent' import { indentCommand, textIndentCommand } from '@/utils/prosemirror/commands/setTextIndent'
import { toggleList } from '@/utils/prosemirror/commands/toggleList' import { toggleList } from '@/utils/prosemirror/commands/toggleList'
import type { TextFormatPainterKeys } from '@/types/edit' import type { TextFormatPainterKeys } from '@/types/edit'
@ -175,6 +175,9 @@ const execCommand = ({ target, action }: RichTextCommand) => {
else if (item.command === 'indent' && item.value) { else if (item.command === 'indent' && item.value) {
indentCommand(editorView, +item.value) indentCommand(editorView, +item.value)
} }
else if (item.command === 'textIndent' && item.value) {
textIndentCommand(editorView, +item.value)
}
else if (item.command === 'bulletList') { else if (item.command === 'bulletList') {
const listStyleType = item.value || '' const listStyleType = item.value || ''
const { bullet_list: bulletList, list_item: listItem } = editorView.state.schema.nodes const { bullet_list: bulletList, list_item: listItem } = editorView.state.schema.nodes

View File

@ -34,7 +34,9 @@
/> />
<div <div
class="text ProseMirror-static" class="text ProseMirror-static"
:style="cssVar" :style="{
'--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
}"
v-html="elementInfo.content" v-html="elementInfo.content"
></div> ></div>
</div> </div>
@ -43,7 +45,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, type StyleValue } from 'vue' import { computed } from 'vue'
import type { PPTTextElement } from '@/types/slides' import type { PPTTextElement } from '@/types/slides'
import ElementOutline from '@/views/components/element/ElementOutline.vue' import ElementOutline from '@/views/components/element/ElementOutline.vue'
@ -55,11 +57,6 @@ const props = defineProps<{
const shadow = computed(() => props.elementInfo.shadow) const shadow = computed(() => props.elementInfo.shadow)
const { shadowStyle } = useElementShadow(shadow) const { shadowStyle } = useElementShadow(shadow)
const cssVar = computed(() => ({
'--textIndent': `${props.elementInfo.textIndent || 0}px`,
'--paragraphSpace': `${props.elementInfo.paragraphSpace === undefined ? 5 : props.elementInfo.paragraphSpace}px`,
} as StyleValue))
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -45,7 +45,6 @@
:editable="!elementInfo.lock" :editable="!elementInfo.lock"
:value="elementInfo.content" :value="elementInfo.content"
:style="{ :style="{
'--textIndent': `${elementInfo.textIndent || 0}px`,
'--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`, '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
}" }"
@update="value => updateContent(value)" @update="value => updateContent(value)"