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
399abc0859
commit
8ff70da477
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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;">
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="减小段落缩进">
|
||||||
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
|
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '-1')"><IconIndentLeft /></Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="增大缩进">
|
<Popover trigger="click" v-model:open="indentLeftPanelVisible">
|
||||||
|
<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 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>
|
<Button style="flex: 1;" @click="emitRichTextCommand('indent', '+1')"><IconIndentRight /></Button>
|
||||||
</Tooltip>
|
</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>
|
</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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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)"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user