Skip to content
File

Blob: src/client/components/editor/extensions/top-level-block-identity.ts

typescript84 lines
1import { Plugin, PluginKey } from "@tiptap/pm/state";
2import type { EditorView } from "@tiptap/pm/view";
3import { SharedTopLevelBlockAttributes, generateBlockBid, getTopLevelBlocks } from "@/shared/editor/schema";
4 
5const topLevelBlockIdentityKey = new PluginKey("topLevelBlockIdentity");
6const TOP_LEVEL_DOC_ID_META = "topLevelBlockIdentity";
7 
8export function applyTopLevelBlockIdNormalization(tr: Parameters<EditorView["dispatch"]>[0]): boolean {
9 const seen = new Set<string>();
10 let mutated = false;
11 
12 const topLevelBlocks = getTopLevelBlocks(tr.doc);
13 for (const block of topLevelBlocks) {
14 let nextBid = block.bid;
15 if (!nextBid || seen.has(nextBid)) {
16 do {
17 nextBid = generateBlockBid();
18 } while (seen.has(nextBid));
19 
20 tr.setNodeMarkup(block.pos, undefined, {
21 ...block.node.attrs,
22 bid: nextBid,
23 });
24 mutated = true;
25 }
26 seen.add(nextBid);
27 }
28 
29 tr.doc.descendants((node, pos, parent) => {
30 if (pos < 0 || parent?.type.name === "doc") return true;
31 if (typeof node.attrs.bid === "undefined" || node.attrs.bid == null) return true;
32 
33 tr.setNodeMarkup(pos, undefined, {
34 ...node.attrs,
35 bid: null,
36 });
37 mutated = true;
38 return true;
39 });
40 
41 return mutated;
42}
43 
44function dispatchInitialNormalization(view: EditorView) {
45 queueMicrotask(() => {
46 if (view.isDestroyed) return;
47 const tr = view.state.tr;
48 if (!applyTopLevelBlockIdNormalization(tr)) return;
49 tr.setMeta(topLevelBlockIdentityKey, true);
50 tr.setMeta(TOP_LEVEL_DOC_ID_META, true);
51 tr.setMeta("addToHistory", false);
52 view.dispatch(tr);
53 });
54}
55 
56export const TopLevelBlockIdentity = SharedTopLevelBlockAttributes.extend({
57 name: "topLevelBlockIdentity",
58 
59 addProseMirrorPlugins() {
60 return [
61 new Plugin({
62 key: topLevelBlockIdentityKey,
63 appendTransaction(transactions, _oldState, newState) {
64 if (!transactions.some((tr) => tr.docChanged)) return null;
65 if (transactions.some((tr) => tr.getMeta(topLevelBlockIdentityKey) || tr.getMeta(TOP_LEVEL_DOC_ID_META))) {
66 return null;
67 }
68 
69 const tr = newState.tr;
70 if (!applyTopLevelBlockIdNormalization(tr)) return null;
71 tr.setMeta(topLevelBlockIdentityKey, true);
72 tr.setMeta(TOP_LEVEL_DOC_ID_META, true);
73 tr.setMeta("addToHistory", false);
74 return tr;
75 },
76 view(view) {
77 dispatchInitialNormalization(view);
78 return {};
79 },
80 }),
81 ];
82 },
83});