File
Blob: src/client/components/editor/extensions/top-level-block-identity.ts
| 1 | import { Plugin, PluginKey } from "@tiptap/pm/state"; |
| 2 | import type { EditorView } from "@tiptap/pm/view"; |
| 3 | import { SharedTopLevelBlockAttributes, generateBlockBid, getTopLevelBlocks } from "@/shared/editor/schema"; |
| 4 | |
| 5 | const topLevelBlockIdentityKey = new PluginKey("topLevelBlockIdentity"); |
| 6 | const TOP_LEVEL_DOC_ID_META = "topLevelBlockIdentity"; |
| 7 | |
| 8 | export 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 | |
| 44 | function 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 | |
| 56 | export 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 | }); |