feat(editor): preserve comment-id on blockquotes
Extend the default Blockquote with a `commentId` attribute that round-trips through HTML as `data-comment-id`. This single attribute is the canonical record of a reply: it survives TipTap serialize / parse so the backend listener and the in-app renderer can both find the parent comment without a separate schema field.
This commit is contained in:
parent
6fc36cb700
commit
46dbeb5784
|
|
@ -60,6 +60,7 @@
|
|||
"@kyvg/vue3-notification": "3.4.2",
|
||||
"@sentry/vue": "10.36.0",
|
||||
"@tiptap/core": "3.17.0",
|
||||
"@tiptap/extension-blockquote": "3.17.0",
|
||||
"@tiptap/extension-code-block-lowlight": "3.17.0",
|
||||
"@tiptap/extension-hard-break": "3.17.0",
|
||||
"@tiptap/extension-image": "3.17.0",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ importers:
|
|||
'@tiptap/core':
|
||||
specifier: 3.17.0
|
||||
version: 3.17.0(@tiptap/pm@3.17.0)
|
||||
'@tiptap/extension-blockquote':
|
||||
specifier: 3.17.0
|
||||
version: 3.17.0(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))
|
||||
'@tiptap/extension-code-block-lowlight':
|
||||
specifier: 3.17.0
|
||||
version: 3.17.0(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))(@tiptap/extension-code-block@3.17.1(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))(@tiptap/pm@3.17.0))(@tiptap/pm@3.17.0)(highlight.js@11.11.1)(lowlight@3.3.0)
|
||||
|
|
@ -2650,10 +2653,10 @@ packages:
|
|||
peerDependencies:
|
||||
'@tiptap/pm': ^3.17.0
|
||||
|
||||
'@tiptap/extension-blockquote@3.17.1':
|
||||
resolution: {integrity: sha512-X4jU/fllJQ8QbjCHUafU4QIHBobyXP3yGBoOcXxUaKlWbLvUs0SQTREM3n6/86m2YyAxwTPG1cn3Xypf42DMAQ==}
|
||||
'@tiptap/extension-blockquote@3.17.0':
|
||||
resolution: {integrity: sha512-TVslb79JVoZUFO+O4lAHveu38asi1OEqNpLdnQr+SIijIi8WgvJv3VwQwZfkja91WUAHbOHGbnYN0QySOcVCtA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^3.17.1
|
||||
'@tiptap/core': ^3.17.0
|
||||
|
||||
'@tiptap/extension-bold@3.17.1':
|
||||
resolution: {integrity: sha512-PZmrljcVBziJkQDXT/QJv4ESxVVQ0iRH+ruTzPda56Kk4h2310cSXGjI33W7rlCikGPoBAAjY/inujm46YB4bw==}
|
||||
|
|
@ -9441,7 +9444,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@tiptap/pm': 3.17.0
|
||||
|
||||
'@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))':
|
||||
'@tiptap/extension-blockquote@3.17.0(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.17.0(@tiptap/pm@3.17.0)
|
||||
|
||||
|
|
@ -9600,7 +9603,7 @@ snapshots:
|
|||
'@tiptap/starter-kit@3.17.0':
|
||||
dependencies:
|
||||
'@tiptap/core': 3.17.0(@tiptap/pm@3.17.0)
|
||||
'@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))
|
||||
'@tiptap/extension-blockquote': 3.17.0(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))
|
||||
'@tiptap/extension-bold': 3.17.1(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))
|
||||
'@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.0(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))(@tiptap/pm@3.17.0))
|
||||
'@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.0(@tiptap/pm@3.17.0))
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ import Mention from '@tiptap/extension-mention'
|
|||
|
||||
import {TaskList} from '@tiptap/extension-list'
|
||||
import {TaskItemWithId} from './taskItemWithId'
|
||||
import {BlockquoteWithCommentId} from './blockquoteWithCommentId'
|
||||
import HardBreak from '@tiptap/extension-hard-break'
|
||||
|
||||
import Commands from './commands'
|
||||
|
|
@ -417,7 +418,9 @@ const extensions : Extensions = [
|
|||
StarterKit.configure({
|
||||
codeBlock: false,
|
||||
hardBreak: false,
|
||||
blockquote: false,
|
||||
}),
|
||||
BlockquoteWithCommentId,
|
||||
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight: createLowlight(common),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
import {Editor} from '@tiptap/core'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import {BlockquoteWithCommentId} from './blockquoteWithCommentId'
|
||||
|
||||
describe('BlockquoteWithCommentId extension', () => {
|
||||
const createEditor = (content: string = '') => {
|
||||
return new Editor({
|
||||
extensions: [
|
||||
StarterKit.configure({blockquote: false}),
|
||||
BlockquoteWithCommentId,
|
||||
],
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
it('preserves data-comment-id through setContent → getHTML round-trip', () => {
|
||||
const editor = createEditor('<blockquote data-comment-id="42"><p>hi</p></blockquote>')
|
||||
|
||||
const html = editor.getHTML()
|
||||
expect(html).toContain('data-comment-id="42"')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('renders a plain blockquote (no attribute) unchanged', () => {
|
||||
const editor = createEditor('<blockquote><p>just a quote</p></blockquote>')
|
||||
|
||||
const html = editor.getHTML()
|
||||
expect(html).toContain('<blockquote>')
|
||||
expect(html).not.toContain('data-comment-id')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('preserves nested rich content inside the blockquote', () => {
|
||||
const editor = createEditor(
|
||||
'<blockquote data-comment-id="7"><p>this is <strong>bold</strong> text</p></blockquote>',
|
||||
)
|
||||
|
||||
const html = editor.getHTML()
|
||||
expect(html).toContain('data-comment-id="7"')
|
||||
expect(html).toContain('<strong>bold</strong>')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('drops a malformed data-comment-id (non-integer)', () => {
|
||||
const editor = createEditor('<blockquote data-comment-id="abc"><p>x</p></blockquote>')
|
||||
|
||||
const html = editor.getHTML()
|
||||
expect(html).not.toContain('data-comment-id')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('drops a non-positive data-comment-id', () => {
|
||||
const editor = createEditor('<blockquote data-comment-id="0"><p>x</p></blockquote>')
|
||||
|
||||
const html = editor.getHTML()
|
||||
expect(html).not.toContain('data-comment-id')
|
||||
|
||||
editor.destroy()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import Blockquote from '@tiptap/extension-blockquote'
|
||||
|
||||
/**
|
||||
* Blockquote extension that preserves `data-comment-id` across parse/serialize.
|
||||
* Used as the canonical reply marker: a comment that quotes another comment
|
||||
* stores the referenced comment's id on the wrapping blockquote, so both the
|
||||
* backend (for implicit-mention notifications) and the frontend (for the
|
||||
* jump-to-original chevron) can find it without a separate schema field.
|
||||
*/
|
||||
export const BlockquoteWithCommentId = Blockquote.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
commentId: {
|
||||
default: null,
|
||||
parseHTML: (element: HTMLElement) => {
|
||||
const raw = element.getAttribute('data-comment-id')
|
||||
if (raw === null) {
|
||||
return null
|
||||
}
|
||||
const id = Number(raw)
|
||||
if (!Number.isInteger(id) || id <= 0) {
|
||||
return null
|
||||
}
|
||||
return id
|
||||
},
|
||||
renderHTML: (attributes) => {
|
||||
if (attributes.commentId === null || attributes.commentId === undefined) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
'data-comment-id': String(attributes.commentId),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue