mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 文本框富文本支持链接(#83)
This commit is contained in:
parent
6c5adddd32
commit
7be6f75d47
@ -87,6 +87,7 @@ import {
|
||||
VideoTwo,
|
||||
Formula,
|
||||
ElectronicPen,
|
||||
LinkOne,
|
||||
} from '@icon-park/vue-next'
|
||||
|
||||
export default {
|
||||
@ -163,6 +164,7 @@ export default {
|
||||
app.component('IconAlignTextTopOne', AlignTextTopOne)
|
||||
app.component('IconAlignTextBottomOne', AlignTextBottomOne)
|
||||
app.component('IconAlignTextMiddleOne', AlignTextMiddleOne)
|
||||
app.component('IconLinkOne', LinkOne)
|
||||
|
||||
// 箭头与符号
|
||||
app.component('IconDown', Down)
|
||||
|
@ -59,6 +59,8 @@ const forecolor: MarkSpec = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
inline: true,
|
||||
group: 'inline',
|
||||
parseDOM: [
|
||||
{
|
||||
style: 'color',
|
||||
@ -81,7 +83,7 @@ const backcolor: MarkSpec = {
|
||||
group: 'inline',
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'span[style*=background-color]',
|
||||
style: 'background-color',
|
||||
getAttrs: backcolor => backcolor ? { backcolor } : {}
|
||||
},
|
||||
],
|
||||
@ -135,6 +137,26 @@ const fontname: MarkSpec = {
|
||||
},
|
||||
}
|
||||
|
||||
const link: MarkSpec = {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: { default: null },
|
||||
target: { default: '_blank' },
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => {
|
||||
const href = (dom as HTMLElement).getAttribute('href')
|
||||
const title = (dom as HTMLElement).getAttribute('title')
|
||||
return { href, title }
|
||||
}
|
||||
},
|
||||
],
|
||||
toDOM: node => ['a', node.attrs, 0],
|
||||
}
|
||||
|
||||
export default {
|
||||
...marks,
|
||||
subscript,
|
||||
@ -145,4 +167,5 @@ export default {
|
||||
backcolor,
|
||||
fontsize,
|
||||
fontname,
|
||||
link,
|
||||
}
|
@ -1,7 +1,67 @@
|
||||
import { Node, NodeType, ResolvedPos, Mark } from 'prosemirror-model'
|
||||
import { Node, NodeType, ResolvedPos, Mark, MarkType } from 'prosemirror-model'
|
||||
import { EditorState, Selection } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
|
||||
export const findNodesWithSameMark = (doc: Node, from: number, to: number, markType: MarkType) => {
|
||||
let ii = from
|
||||
const finder = (mark: Mark) => mark.type === markType
|
||||
let firstMark = null
|
||||
let fromNode = null
|
||||
let toNode = null
|
||||
|
||||
while (ii <= to) {
|
||||
const node = doc.nodeAt(ii)
|
||||
if (!node || !node.marks) return null
|
||||
|
||||
const mark = node.marks.find(finder)
|
||||
if (!mark) return null
|
||||
|
||||
if (firstMark && mark !== firstMark) return null
|
||||
|
||||
fromNode = fromNode || node
|
||||
firstMark = firstMark || mark
|
||||
toNode = node
|
||||
ii++
|
||||
}
|
||||
|
||||
let fromPos = from
|
||||
let toPos = to
|
||||
|
||||
let jj = 0
|
||||
ii = from - 1
|
||||
while (ii > jj) {
|
||||
const node = doc.nodeAt(ii)
|
||||
const mark = node && node.marks.find(finder)
|
||||
if (!mark || mark !== firstMark) break
|
||||
fromPos = ii
|
||||
fromNode = node
|
||||
ii--
|
||||
}
|
||||
|
||||
ii = to + 1
|
||||
jj = doc.nodeSize - 2
|
||||
while (ii < jj) {
|
||||
const node = doc.nodeAt(ii)
|
||||
const mark = node && node.marks.find(finder)
|
||||
if (!mark || mark !== firstMark) break
|
||||
toPos = ii
|
||||
toNode = node
|
||||
ii++
|
||||
}
|
||||
|
||||
return {
|
||||
mark: firstMark,
|
||||
from: {
|
||||
node: fromNode,
|
||||
pos: fromPos,
|
||||
},
|
||||
to: {
|
||||
node: toNode,
|
||||
pos: toPos,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const equalNodeType = (nodeType: NodeType, node: Node) => {
|
||||
return Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1 || node.type === nodeType
|
||||
}
|
||||
@ -107,6 +167,7 @@ export const getTextAttrs = (view: EditorView, defaultAttrs: DefaultAttrs = {})
|
||||
const backcolor = getAttrValue(marks, 'backcolor', 'backcolor') || defaultAttrs.backcolor
|
||||
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 isBulletList = isActiveOfParentNodeType('bullet_list', view.state)
|
||||
const isOrderedList = isActiveOfParentNodeType('ordered_list', view.state)
|
||||
@ -124,6 +185,7 @@ export const getTextAttrs = (view: EditorView, defaultAttrs: DefaultAttrs = {})
|
||||
backcolor: backcolor,
|
||||
fontsize: fontsize,
|
||||
fontname: fontname,
|
||||
link: link,
|
||||
align: align,
|
||||
bulletList: isBulletList,
|
||||
orderedList: isOrderedList,
|
||||
@ -145,6 +207,7 @@ export const defaultRichTextAttrs: TextAttrs = {
|
||||
backcolor: '#000',
|
||||
fontsize: '20px',
|
||||
fontname: '微软雅黑',
|
||||
link: '',
|
||||
align: 'left',
|
||||
bulletList: false,
|
||||
orderedList: false,
|
||||
|
@ -116,6 +116,12 @@
|
||||
@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">
|
||||
@ -147,11 +153,23 @@
|
||||
@click="emitRichTextCommand('blockquote')"
|
||||
><IconQuote /></CheckboxButton>
|
||||
</Tooltip>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="清除格式">
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
@click="emitRichTextCommand('clear')"
|
||||
><IconFormat /></CheckboxButton>
|
||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.5" title="超链接">
|
||||
<Popover placement="bottomRight" trigger="click" v-model:visible="linkPopoverVisible">
|
||||
<template #content>
|
||||
<div class="link-popover">
|
||||
<Input v-model:value="link" placeholder="请输入超链接" />
|
||||
<div class="btns">
|
||||
<Button size="small" :disabled="!richTextAttrs.link" @click="updateLink()" style="margin-right: 5px;">移除</Button>
|
||||
<Button size="small" type="primary" @click="updateLink(link)">确认</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<CheckboxButton
|
||||
style="flex: 1;"
|
||||
:checked="!!richTextAttrs.link"
|
||||
@click="openLinkPopover()"
|
||||
><IconLinkOne /></CheckboxButton>
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
</CheckboxButtonGroup>
|
||||
|
||||
@ -316,6 +334,13 @@ export default defineComponent({
|
||||
const slidesStore = useSlidesStore()
|
||||
const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore())
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const updateElement = (props: Partial<PPTTextElement>) => {
|
||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
const fill = ref<string>()
|
||||
const lineHeight = ref<number>()
|
||||
const wordSpace = ref<number>()
|
||||
@ -336,23 +361,6 @@ export default defineComponent({
|
||||
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 emitRichTextCommand = (command: string, value?: string) => {
|
||||
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { command, value })
|
||||
}
|
||||
|
||||
// 发射富文本设置命令(批量)
|
||||
const emitBatchRichTextCommand = (payload: RichTextCommand[]) => {
|
||||
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, payload)
|
||||
}
|
||||
|
||||
const { addHistorySnapshot } = useHistorySnapshot()
|
||||
|
||||
const updateElement = (props: Partial<PPTTextElement>) => {
|
||||
slidesStore.updateElement({ id: handleElementId.value, props })
|
||||
addHistorySnapshot()
|
||||
}
|
||||
|
||||
// 设置行高
|
||||
const updateLineHeight = (value: number) => {
|
||||
updateElement({ lineHeight: value })
|
||||
@ -368,6 +376,31 @@ export default defineComponent({
|
||||
updateElement({ fill: value })
|
||||
}
|
||||
|
||||
// 发射富文本设置命令
|
||||
const emitRichTextCommand = (command: string, value?: string) => {
|
||||
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, { command, value })
|
||||
}
|
||||
|
||||
// 发射富文本设置命令(批量)
|
||||
const emitBatchRichTextCommand = (payload: RichTextCommand[]) => {
|
||||
emitter.emit(EmitterEvents.RICH_TEXT_COMMAND, payload)
|
||||
}
|
||||
|
||||
// 设置富文本超链接
|
||||
const link = ref('')
|
||||
const linkPopoverVisible = ref(false)
|
||||
|
||||
watch(richTextAttrs, () => linkPopoverVisible.value = false)
|
||||
|
||||
const openLinkPopover = () => {
|
||||
link.value = richTextAttrs.value.link
|
||||
linkPopoverVisible.value = true
|
||||
}
|
||||
const updateLink = (link: string) => {
|
||||
emitRichTextCommand('link', link)
|
||||
linkPopoverVisible.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
fill,
|
||||
lineHeight,
|
||||
@ -384,6 +417,10 @@ export default defineComponent({
|
||||
emitRichTextCommand,
|
||||
emitBatchRichTextCommand,
|
||||
presetStyles,
|
||||
link,
|
||||
linkPopoverVisible,
|
||||
openLinkPopover,
|
||||
updateLink,
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -440,4 +477,12 @@ export default defineComponent({
|
||||
height: 3px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.link-popover {
|
||||
width: 240px;
|
||||
|
||||
.btns {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -13,7 +13,7 @@ import { useMainStore } from '@/store'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { toggleMark, wrapIn, selectAll } from 'prosemirror-commands'
|
||||
import { initProsemirrorEditor, createDocument } from '@/utils/prosemirror'
|
||||
import { getTextAttrs } from '@/utils/prosemirror/utils'
|
||||
import { findNodesWithSameMark, getTextAttrs } from '@/utils/prosemirror/utils'
|
||||
import emitter, { EmitterEvents, RichTextCommand } from '@/utils/emitter'
|
||||
import { alignmentCommand } from '@/utils/prosemirror/commands/setTextAlign'
|
||||
import { toggleList } from '@/utils/prosemirror/commands/toggleList'
|
||||
@ -207,6 +207,23 @@ export default defineComponent({
|
||||
const { $from, $to } = editorView.state.selection
|
||||
editorView.dispatch(editorView.state.tr.removeMark($from.pos, $to.pos))
|
||||
}
|
||||
else if (item.command === 'link') {
|
||||
const markType = editorView.state.schema.marks.link
|
||||
const { from, to } = editorView.state.selection
|
||||
const result = findNodesWithSameMark(editorView.state.doc, from, to, markType)
|
||||
if (result) {
|
||||
if (item.value) {
|
||||
const mark = editorView.state.schema.marks.link.create({ href: item.value, title: item.value })
|
||||
editorView.dispatch(editorView.state.tr.addMark(result.from.pos, result.to.pos + 1, mark))
|
||||
}
|
||||
else editorView.dispatch(editorView.state.tr.removeMark(result.from.pos, result.to.pos + 1, markType))
|
||||
}
|
||||
else if (item.value) {
|
||||
const { empty } = editorView.state.selection
|
||||
if (empty) selectAll(editorView.state, editorView.dispatch)
|
||||
toggleMark(markType, { href: item.value, title: item.value })(editorView.state, editorView.dispatch)
|
||||
}
|
||||
}
|
||||
else if (item.command === 'insert' && item.value) {
|
||||
editorView.dispatch(editorView.state.tr.insertText(item.value))
|
||||
}
|
||||
|
@ -183,6 +183,10 @@ export default defineComponent({
|
||||
.text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::v-deep(a) {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
.drag-handler {
|
||||
height: 10px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user