'use client' import { Node, mergeAttributes } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewWrapper, type NodeViewProps } from '@tiptap/react' import { useCallback } from 'react' import type { Editor } from '@tiptap/core' import type { Node as PMNode } from '@tiptap/pm/model' import { DatabaseBlockEditor } from '@/components/database-block-editor' import { NOTE_REQUEST_SAVE_EVENT } from '@/lib/note-change-sync' import { createDefaultDatabaseBlockData, parseDatabaseBlockAttrs, serializeDatabaseBlockData, type DatabaseBlockData, } from '@/lib/editor/database-block-types' function DatabaseBlockView({ node, updateAttributes, editor }: NodeViewProps) { const data = parseDatabaseBlockAttrs(node.attrs) const requestSave = useCallback(() => { const hostNoteId = editor.storage.liveBlock?.hostNoteId if (hostNoteId) { window.dispatchEvent(new CustomEvent(NOTE_REQUEST_SAVE_EVENT, { detail: { noteId: hostNoteId, reason: 'database-block-mutation' }, })) } }, [editor]) const handleChange = useCallback((next: DatabaseBlockData) => { updateAttributes(serializeDatabaseBlockData(next)) requestSave() }, [requestSave, updateAttributes]) return ( ) } export const DatabaseBlockExtension = Node.create({ name: 'databaseBlock', group: 'block', atom: true, draggable: true, selectable: true, addAttributes() { return { dbId: { default: '', parseHTML: (element) => element.getAttribute('data-db-id') || '', renderHTML: (attributes) => (attributes.dbId ? { 'data-db-id': attributes.dbId } : {}), }, dbView: { default: 'table', parseHTML: (element) => element.getAttribute('data-db-view') || 'table', renderHTML: (attributes) => ({ 'data-db-view': attributes.dbView || 'table' }), }, dbAuthorsJson: { default: '[]', parseHTML: (element) => element.getAttribute('data-db-authors') || '[]', renderHTML: (attributes) => ({ 'data-db-authors': attributes.dbAuthorsJson || '[]' }), }, dbBooksJson: { default: '[]', parseHTML: (element) => element.getAttribute('data-db-books') || '[]', renderHTML: (attributes) => ({ 'data-db-books': attributes.dbBooksJson || '[]' }), }, } }, parseHTML() { return [{ tag: 'div[data-database-block]' }] }, renderHTML({ HTMLAttributes }) { return ['div', mergeAttributes(HTMLAttributes, { 'data-database-block': 'true' })] }, addNodeView() { return ReactNodeViewRenderer(DatabaseBlockView) }, }) export function insertDatabaseBlockAtSelection(editor: Editor): boolean { const type = editor.schema.nodes.databaseBlock if (!type) return false const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData()) const { empty, $from } = editor.state.selection const parent = $from.parent if (empty && parent.type.name === 'paragraph' && parent.content.size === 0) { const pos = $from.before() return editor .chain() .focus() .command(({ tr, dispatch }) => { tr.replaceWith(pos, pos + parent.nodeSize, type.create(attrs)) if (dispatch) dispatch(tr) return true }) .run() } return editor.chain().focus().insertContent({ type: 'databaseBlock', attrs }).run() } export function replaceBlockWithDatabase(editor: Editor, blockPos: number, blockNode: PMNode): void { const type = editor.schema.nodes.databaseBlock if (!type || blockPos < 0 || !blockNode) return const attrs = serializeDatabaseBlockData(createDefaultDatabaseBlockData()) const dbNode = type.create(attrs) editor.view.dispatch(editor.state.tr.replaceWith(blockPos, blockPos + blockNode.nodeSize, dbNode)) editor.commands.focus() }