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;