diff --git a/package.json b/package.json index 92879b1..f0a1f0a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "@kangc/v-md-editor": "^2.3.18", "@onlyoffice/document-editor-vue": "^1.5.0", "@tiptap/core": "^2.23.1", + "@tiptap/extension-blockquote": "^2.23.1", + "@tiptap/extension-emoji": "^2.23.1", "@tiptap/extension-image": "^2.23.1", "@tiptap/extension-link": "^2.23.1", "@tiptap/extension-mention": "^2.23.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21901b2..3ded82e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@tiptap/core': specifier: ^2.23.1 version: 2.23.1(@tiptap/pm@2.23.1) + '@tiptap/extension-blockquote': + specifier: ^2.23.1 + version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1)) + '@tiptap/extension-emoji': + specifier: ^2.23.1 + version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(@tiptap/suggestion@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(emojibase@16.0.0) '@tiptap/extension-image': specifier: ^2.23.1 version: 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1)) @@ -928,6 +934,13 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-emoji@2.23.1': + resolution: {integrity: sha512-bqTn+hbq0bDIcrPIIjVq3GndJ/PYQfReMDlyTv0mUCtRbP7zReJ1oFx02d25RmwgS6XL3U8WW4kEFomhliwWSQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/suggestion': ^2.7.0 + '@tiptap/extension-floating-menu@2.23.1': resolution: {integrity: sha512-GMWkpH+p/OUOk1Y5UGOnKuHSDEVBN7DhYIJiWt5g9LK/mpPeuqoCmQg3RQDgjtZXb74SlxLK2pS/3YcAnemdfQ==} peerDependencies: @@ -2013,9 +2026,21 @@ packages: elkjs@0.9.3: resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emojibase-data@15.3.2: + resolution: {integrity: sha512-TpDyTDDTdqWIJixV5sTA6OQ0P0JfIIeK2tFRR3q56G9LK65ylAZ7z3KyBXokpvTTJ+mLUXQXbLNyVkjvnTLE+A==} + peerDependencies: + emojibase: '*' + + emojibase@16.0.0: + resolution: {integrity: sha512-Nw2m7JLIO4Ou2X/yZPRNscHQXVbbr6SErjkJ7EooG7MbR3yDZszCv9KTizsXFc7yZl0n3WF+qUKIC/Lw6H9xaQ==} + engines: {node: '>=18.12.0'} + enhanced-resolve@5.18.2: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} @@ -2454,6 +2479,9 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-emoji-supported@0.0.5: + resolution: {integrity: sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==} + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -4548,6 +4576,17 @@ snapshots: '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1) '@tiptap/pm': 2.23.1 + '@tiptap/extension-emoji@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)(@tiptap/suggestion@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1))(emojibase@16.0.0)': + dependencies: + '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1) + '@tiptap/pm': 2.23.1 + '@tiptap/suggestion': 2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1) + emoji-regex: 10.4.0 + emojibase-data: 15.3.2(emojibase@16.0.0) + is-emoji-supported: 0.0.5 + transitivePeerDependencies: + - emojibase + '@tiptap/extension-floating-menu@2.23.1(@tiptap/core@2.23.1(@tiptap/pm@2.23.1))(@tiptap/pm@2.23.1)': dependencies: '@tiptap/core': 2.23.1(@tiptap/pm@2.23.1) @@ -5877,8 +5916,16 @@ snapshots: elkjs@0.9.3: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} + emojibase-data@15.3.2(emojibase@16.0.0): + dependencies: + emojibase: 16.0.0 + + emojibase@16.0.0: {} + enhanced-resolve@5.18.2: dependencies: graceful-fs: 4.2.11 @@ -6353,6 +6400,8 @@ snapshots: is-docker@3.0.0: {} + is-emoji-supported@0.0.5: {} + is-extendable@0.1.1: {} is-extendable@1.0.1: diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue index eaa0742..0604af1 100644 --- a/src/components/editor/TiptapEditor.vue +++ b/src/components/editor/TiptapEditor.vue @@ -121,110 +121,74 @@ const Emoji = Node.create({ }) // 自定义Quote扩展 -const Quote = Extension.create({ +const Quote = Node.create({ name: 'quote', - + group: 'block', + atom: true, + draggable: true, + addAttributes() { return { - id: { - default: null, - }, - title: { - default: null, - }, - describe: { - default: null, - }, - image: { - default: '', - }, + id: { default: null }, + title: { default: null }, + describe: { default: null }, + image: { default: '' } } }, - + parseHTML() { - return [ - { - tag: 'div.quote-card', - }, - ] + return [{ tag: 'div.quote-card' }] }, - + renderHTML({ HTMLAttributes }) { const { id, title, describe, image } = HTMLAttributes - - // 创建引用卡片的HTML结构 - const quoteCardContent = document.createElement('span') - quoteCardContent.classList.add('quote-card-content') - - const close = document.createElement('span') - close.classList.add('quote-card-remove') - close.textContent = '×' - close.addEventListener('click', (e) => { - e.stopPropagation() - // 移除引用 - if (editor.value) { - editor.value.commands.deleteNode('quote') - } - }) - - const quoteCardTitle = document.createElement('span') - quoteCardTitle.classList.add('quote-card-title') - quoteCardTitle.textContent = title - quoteCardTitle.appendChild(close) - - quoteCardContent.appendChild(quoteCardTitle) - - if (!image || image.length === 0) { - const quoteCardMeta = document.createElement('span') - quoteCardMeta.classList.add('quote-card-meta') - quoteCardMeta.textContent = describe - quoteCardContent.appendChild(quoteCardMeta) - } else { - const iconImg = document.createElement('img') - iconImg.setAttribute('src', image) - iconImg.setAttribute('style', 'width:30px;height:30px;margin-right:10px;') - quoteCardContent.appendChild(iconImg) + + const titleEl = ['span', { class: 'quote-card-title' }, title || ''] + let contentChildren = [titleEl] + + if (image && image.length > 0) { + contentChildren.push(['img', { src: image, style: 'width:30px;height:30px;margin-right:10px;' }]) + } else if (describe) { + contentChildren.push(['span', { class: 'quote-card-meta' }, describe]) } - - const node = document.createElement('div') - node.classList.add('quote-card') - node.setAttribute('data-id', id) - node.setAttribute('data-title', title) - node.setAttribute('data-describe', describe) - node.setAttribute('data-image', image || '') - node.setAttribute('contenteditable', 'false') - node.appendChild(quoteCardContent) - - return ['div', { class: 'quote-card-wrapper' }, node] + + const cardContent = ['span', { class: 'quote-card-content' }, ...contentChildren] + + return [ + 'div', + { + class: 'quote-card', + 'data-id': id, + 'data-title': title, + 'data-describe': describe, + 'data-image': image || '', + contenteditable: 'false' + }, + cardContent + ] }, - + addKeyboardShortcuts() { return { - 'Backspace': () => { + Backspace: () => { const { selection } = this.editor.state - const { empty, anchor } = selection - + const { $from, empty } = selection + if (!empty) { return false } - - const isAtStart = anchor === 0 - - if (!isAtStart) { - return false + + if ($from.parent.isTextblock && $from.parentOffset === 0) { + const nodeBefore = $from.nodeBefore + if (nodeBefore && nodeBefore.type.name === this.name) { + return this.editor.commands.deleteNode(this.name) + } } - - // 检查是否有引用节点 - const quoteNode = this.editor.state.doc.firstChild - if (quoteNode && quoteNode.type.name === 'quote') { - this.editor.commands.deleteNode('quote') - return true - } - + return false - }, + } } - }, + } }) // 创建自定义键盘处理插件,处理Enter键发送消息 @@ -706,18 +670,27 @@ function onSubscribeMention(data) { */ function onSubscribeQuote(data) { if (!editor.value) return - + // 检查是否已有引用内容 const json = editor.value.getJSON() - if (json.content?.some((node) => node.type === 'quote')) { - return // 已有引用则不再添加 + if (json.content?.some(node => node.type === 'quote')) { + return // 已有引用则不再添加 } // 在编辑器开头插入引用 - editor.value.chain().focus().insertContent({ - type: 'quote', - attrs: data, - }).run() + editor.value + .chain() + .focus() + .insertContentAt(0, [ + { + type: 'quote', + attrs: data + }, + { + type: 'paragraph' + } + ]) + .run() } /**