Multiple feature additions and improvements across the application: - NextGen Editor: drag handles, smart paste, block actions - Structured views: Kanban and table layouts for notes - Architectural Grid: new brainstorming/agent interface prototype - Flashcards: SM-2 revision algorithm with AI generation - MCP server: robustness improvements - Graph/PDF chat: fix click propagation and copy behavior - Various UI/UX enhancements and bug fixes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import type { Editor } from '@tiptap/core'
|
|
import type { Node as PMNode } from '@tiptap/pm/model'
|
|
|
|
const ID_ATTR = 'data-id'
|
|
|
|
const BLOCK_TYPES_WITH_ID = new Set([
|
|
'paragraph',
|
|
'heading',
|
|
'blockquote',
|
|
'bulletList',
|
|
'orderedList',
|
|
'taskList',
|
|
'codeBlock',
|
|
])
|
|
|
|
export 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)
|
|
})
|
|
}
|
|
|
|
function readNodeId(node: PMNode | null | undefined): string | null {
|
|
const id = node?.attrs[ID_ATTR]
|
|
return typeof id === 'string' && id.length > 0 ? id : null
|
|
}
|
|
|
|
function findDataIdInSubtree(node: PMNode): string | null {
|
|
const direct = readNodeId(node)
|
|
if (direct) return direct
|
|
|
|
let found: string | null = null
|
|
node.descendants((child) => {
|
|
if (found) return false
|
|
found = readNodeId(child)
|
|
return !found
|
|
})
|
|
return found
|
|
}
|
|
|
|
function findDataIdFromDom(editor: Editor, blockPos: number): string | null {
|
|
const dom = editor.view.nodeDOM(blockPos)
|
|
if (!(dom instanceof HTMLElement)) return null
|
|
|
|
const direct = dom.getAttribute(ID_ATTR)
|
|
if (direct) return direct
|
|
|
|
const nested = dom.querySelector(`[${ID_ATTR}]`)
|
|
const nestedId = nested?.getAttribute(ID_ATTR)
|
|
return nestedId || null
|
|
}
|
|
|
|
function findTextBlockToAssign(
|
|
editor: Editor,
|
|
blockPos: number,
|
|
blockNode: PMNode,
|
|
): { pos: number; node: PMNode } | null {
|
|
if (BLOCK_TYPES_WITH_ID.has(blockNode.type.name)) {
|
|
return { pos: blockPos, node: blockNode }
|
|
}
|
|
|
|
let target: { pos: number; node: PMNode } | null = null
|
|
editor.state.doc.nodesBetween(blockPos, blockPos + blockNode.nodeSize, (node, pos) => {
|
|
if (target) return false
|
|
if (node.isTextblock && BLOCK_TYPES_WITH_ID.has(node.type.name)) {
|
|
target = { pos, node }
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
return target
|
|
}
|
|
|
|
/** Cherche un data-id sur le bloc résolu (nœud, descendants, ancêtres, DOM). */
|
|
export function findBlockReferenceId(
|
|
editor: Editor,
|
|
blockPos: number,
|
|
blockNode: PMNode | null,
|
|
): string | null {
|
|
const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null)
|
|
if (!node) return null
|
|
|
|
const fromSubtree = findDataIdInSubtree(node)
|
|
if (fromSubtree) return fromSubtree
|
|
|
|
const $pos = editor.state.doc.resolve(Math.min(blockPos + 1, editor.state.doc.content.size))
|
|
for (let depth = $pos.depth; depth > 0; depth--) {
|
|
const id = readNodeId($pos.node(depth))
|
|
if (id) return id
|
|
}
|
|
|
|
return findDataIdFromDom(editor, blockPos)
|
|
}
|
|
|
|
/** Assigne un data-id si absent, retourne l'id utilisable pour la référence. */
|
|
export function ensureBlockReferenceId(
|
|
editor: Editor,
|
|
blockPos: number,
|
|
blockNode: PMNode | null,
|
|
): string | null {
|
|
const existing = findBlockReferenceId(editor, blockPos, blockNode)
|
|
if (existing) return existing
|
|
|
|
const node = blockNode ?? (blockPos >= 0 ? editor.state.doc.nodeAt(blockPos) : null)
|
|
if (!node || blockPos < 0) return null
|
|
|
|
const target = findTextBlockToAssign(editor, blockPos, node)
|
|
if (!target) return null
|
|
|
|
const newId = generateBlockId()
|
|
editor.view.dispatch(
|
|
editor.state.tr.setNodeMarkup(target.pos, undefined, {
|
|
...target.node.attrs,
|
|
[ID_ATTR]: newId,
|
|
}),
|
|
)
|
|
return newId
|
|
}
|