diff --git a/src/plugins/icon.ts b/src/plugins/icon.ts index 682dab2b..0192de3c 100644 --- a/src/plugins/icon.ts +++ b/src/plugins/icon.ts @@ -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) diff --git a/src/utils/prosemirror/schema/marks.ts b/src/utils/prosemirror/schema/marks.ts index b4800b67..867fecef 100644 --- a/src/utils/prosemirror/schema/marks.ts +++ b/src/utils/prosemirror/schema/marks.ts @@ -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, } \ No newline at end of file diff --git a/src/utils/prosemirror/utils.ts b/src/utils/prosemirror/utils.ts index e43e63bd..41b1b5e0 100644 --- a/src/utils/prosemirror/utils.ts +++ b/src/utils/prosemirror/utils.ts @@ -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, diff --git a/src/views/Editor/Toolbar/ElementStylePanel/TextStylePanel.vue b/src/views/Editor/Toolbar/ElementStylePanel/TextStylePanel.vue index bdcf0c79..daddc3ea 100644 --- a/src/views/Editor/Toolbar/ElementStylePanel/TextStylePanel.vue +++ b/src/views/Editor/Toolbar/ElementStylePanel/TextStylePanel.vue @@ -116,6 +116,12 @@ @click="emitRichTextCommand('strikethrough')" > + + + @@ -147,11 +153,23 @@ @click="emitRichTextCommand('blockquote')" > - - + + + + + @@ -316,6 +334,13 @@ export default defineComponent({ const slidesStore = useSlidesStore() const { handleElement, handleElementId, richTextAttrs, availableFonts } = storeToRefs(useMainStore()) + const { addHistorySnapshot } = useHistorySnapshot() + + const updateElement = (props: Partial) => { + slidesStore.updateElement({ id: handleElementId.value, props }) + addHistorySnapshot() + } + const fill = ref() const lineHeight = ref() const wordSpace = ref() @@ -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) => { - 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; + } +} \ No newline at end of file diff --git a/src/views/components/element/ProsemirrorEditor.vue b/src/views/components/element/ProsemirrorEditor.vue index 68c09089..bd7189a1 100644 --- a/src/views/components/element/ProsemirrorEditor.vue +++ b/src/views/components/element/ProsemirrorEditor.vue @@ -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)) } diff --git a/src/views/components/element/TextElement/index.vue b/src/views/components/element/TextElement/index.vue index 16f310b2..731ce1c8 100644 --- a/src/views/components/element/TextElement/index.vue +++ b/src/views/components/element/TextElement/index.vue @@ -183,6 +183,10 @@ export default defineComponent({ .text { position: relative; } + + ::v-deep(a) { + cursor: text; + } } .drag-handler { height: 10px;