Skip to content
File

Blob: src/shared/editor/schema/top-level-blocks.ts

typescript88 lines
1import { Extension } from "@tiptap/core";
2import type { Node as PMNode } from "@tiptap/pm/model";
3import { nanoid } from "nanoid";
4 
5export interface TopLevelBlockInfo {
6 bid: string | null;
7 index: number;
8 pos: number;
9 node: PMNode;
10}
11 
12// Only these nodes participate in the current top-level drag-and-drop UX.
13// Nested blocks remain out of scope for this pass.
14export const TOP_LEVEL_MOVABLE_NODE_TYPES = [
15 "paragraph",
16 "heading",
17 "bulletList",
18 "orderedList",
19 "taskList",
20 "blockquote",
21 "codeBlock",
22 "horizontalRule",
23 "details",
24 "callout",
25 "image",
26 "table",
27] as const;
28 
29const TOP_LEVEL_MOVABLE_NODE_TYPE_SET = new Set<string>(TOP_LEVEL_MOVABLE_NODE_TYPES);
30 
31export function generateBlockBid(): string {
32 return nanoid(6);
33}
34 
35function isTopLevelMovableNode(node: PMNode): boolean {
36 return TOP_LEVEL_MOVABLE_NODE_TYPE_SET.has(node.type.name);
37}
38 
39export function getTopLevelBlocks(doc: PMNode): TopLevelBlockInfo[] {
40 const blocks: TopLevelBlockInfo[] = [];
41 doc.forEach((node, pos, index) => {
42 if (!isTopLevelMovableNode(node)) return;
43 blocks.push({
44 bid: typeof node.attrs.bid === "string" && node.attrs.bid.length > 0 ? node.attrs.bid : null,
45 index,
46 pos,
47 node,
48 });
49 });
50 return blocks;
51}
52 
53export function getTopLevelStructureSignature(doc: PMNode): string {
54 return getTopLevelBlocks(doc)
55 .map((block) => `${block.node.type.name}:${block.bid ?? `missing@${block.index}`}`)
56 .join("|");
57}
58 
59export function findTopLevelBlockByBid(doc: PMNode, bid: string | null | undefined): TopLevelBlockInfo | null {
60 if (!bid) return null;
61 return getTopLevelBlocks(doc).find((block) => block.bid === bid) ?? null;
62}
63 
64export const SharedTopLevelBlockAttributes = Extension.create({
65 name: "topLevelBlockAttributes",
66 
67 addGlobalAttributes() {
68 return [
69 {
70 types: [...TOP_LEVEL_MOVABLE_NODE_TYPES],
71 attributes: {
72 bid: {
73 default: null,
74 parseHTML: (element) => {
75 const raw = element.getAttribute("data-bid");
76 return raw && raw.length > 0 ? raw : null;
77 },
78 renderHTML: (attributes: { bid?: string | null }) => {
79 if (!attributes.bid) return {};
80 return { "data-bid": attributes.bid };
81 },
82 },
83 },
84 },
85 ];
86 },
87});