feat(editor): 添加blockquote和emoji扩展并重构引用卡片实现

- 添加@tiptap/extension-blockquote和@tiptap/extension-emoji依赖
- 将自定义Quote扩展从Extension改为Node实现
- 简化引用卡片的HTML渲染逻辑
- 改进引用卡片的键盘删除行为
- 优化引用内容的插入位置和格式
This commit is contained in:
Phoenix 2025-07-02 13:15:16 +08:00
parent 8e2c134c90
commit dd170cb50d
3 changed files with 115 additions and 91 deletions

View File

@ -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",

View File

@ -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:

View File

@ -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 titleEl = ['span', { class: 'quote-card-title' }, title || '']
let contentChildren = [titleEl]
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)
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)
const cardContent = ['span', { class: 'quote-card-content' }, ...contentChildren]
return ['div', { class: 'quote-card-wrapper' }, node]
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
@ -709,15 +673,24 @@ function onSubscribeQuote(data) {
//
const json = editor.value.getJSON()
if (json.content?.some((node) => node.type === 'quote')) {
if (json.content?.some(node => node.type === 'quote')) {
return //
}
//
editor.value.chain().focus().insertContent({
editor.value
.chain()
.focus()
.insertContentAt(0, [
{
type: 'quote',
attrs: data,
}).run()
attrs: data
},
{
type: 'paragraph'
}
])
.run()
}
/**