import { Extension } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' import type { Transaction } from '@tiptap/pm/state' import type { Node as PMNode } from '@tiptap/pm/model' const BLOCK_TYPES = ['paragraph', 'heading', 'blockquote', 'bulletList', 'orderedList', 'taskList', 'codeBlock'] function assignMissingBlockIds(tr: Transaction, doc: PMNode) { let modified = false doc.descendants((node, pos) => { if (!BLOCK_TYPES.includes(node.type.name)) return if (node.attrs['data-id']) return tr.setNodeMarkup(pos, undefined, { ...node.attrs, 'data-id': generateBlockId(), }) modified = true }) return modified } function generateBlockId(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0 const v = c === 'x' ? r : (r & 0x3) | 0x8 return v.toString(16) }) } export const UniqueIdExtension = Extension.create({ name: 'uniqueId', onCreate({ editor }) { const assign = () => { if (editor.isDestroyed) return const { tr, doc } = editor.state if (assignMissingBlockIds(tr, doc)) { editor.view.dispatch(tr) } } assign() requestAnimationFrame(assign) }, addProseMirrorPlugins() { return [ new Plugin({ key: new PluginKey('uniqueId'), appendTransaction(transactions, _oldState, newState) { const hasDocChanged = transactions.some(t => t.docChanged) if (!hasDocChanged) return null const { tr, doc } = newState return assignMissingBlockIds(tr, doc) ? tr : null }, }), ] }, addGlobalAttributes() { return [ { types: BLOCK_TYPES, attributes: { 'data-id': { default: null, parseHTML: element => element.getAttribute('data-id'), renderHTML: attributes => { if (!attributes['data-id']) return {} return { 'data-id': attributes['data-id'] } }, }, }, }, ] }, })