|
| 1 | +import { BlockNoteEditor, getNodeById } from "@blocknote/core"; |
| 2 | +import { VirtualElement } from "@floating-ui/utils"; |
| 3 | +import { posToDOMRect } from "@tiptap/core"; |
| 4 | +import { useEditorState } from "./useEditorState.js"; |
| 5 | + |
| 6 | +/** |
| 7 | + * Flattens a DOMRect to the nearest integer values |
| 8 | + */ |
| 9 | +export function flattenDOMRect(domRect: DOMRect): DOMRect { |
| 10 | + const { x, y, width, height } = domRect; |
| 11 | + return new DOMRect( |
| 12 | + Math.round(x), |
| 13 | + Math.round(y), |
| 14 | + Math.round(width), |
| 15 | + Math.round(height), |
| 16 | + ); |
| 17 | +} |
| 18 | + |
| 19 | +/** |
| 20 | + * Based on a position (or range), returns a virtual element that can be used as a reference for floating UI. |
| 21 | + */ |
| 22 | +export function useVirtualElementAtPos( |
| 23 | + editor: BlockNoteEditor<any, any, any>, |
| 24 | + whichPos: (ctx: { editor: BlockNoteEditor<any, any, any> }) => |
| 25 | + | undefined |
| 26 | + | { |
| 27 | + // TODO use the location API for this |
| 28 | + from: number; |
| 29 | + to?: number; |
| 30 | + element?: HTMLElement; |
| 31 | + }, |
| 32 | +): VirtualElement | undefined { |
| 33 | + const { domRect, contextElement } = useEditorState({ |
| 34 | + editor, |
| 35 | + selector: (ctx) => { |
| 36 | + const range = whichPos(ctx); |
| 37 | + if (!range) { |
| 38 | + return { domRect: undefined, contextElement: undefined }; |
| 39 | + } |
| 40 | + return { |
| 41 | + // flatten to JSON to avoid re-renders |
| 42 | + domRect: flattenDOMRect( |
| 43 | + posToDOMRect( |
| 44 | + editor.prosemirrorView, |
| 45 | + range.from, |
| 46 | + range.to ?? range.from, |
| 47 | + ), |
| 48 | + ), |
| 49 | + contextElement: range.element ?? editor.prosemirrorView.dom, |
| 50 | + }; |
| 51 | + }, |
| 52 | + }); |
| 53 | + |
| 54 | + if (!domRect) { |
| 55 | + return undefined; |
| 56 | + } |
| 57 | + |
| 58 | + return { |
| 59 | + getBoundingClientRect: () => domRect, |
| 60 | + contextElement, |
| 61 | + }; |
| 62 | +} |
| 63 | + |
| 64 | +/** |
| 65 | + * Gets a virtual element at a block (either before, after, or across the block) |
| 66 | + */ |
| 67 | +export function useVirtualElementAtBlock( |
| 68 | + editor: BlockNoteEditor<any, any, any>, |
| 69 | + blockID: string, |
| 70 | + placement: "before" | "after" | "across" = "across", |
| 71 | +): VirtualElement | undefined { |
| 72 | + return useVirtualElementAtPos(editor, () => { |
| 73 | + return editor.transact((tr) => { |
| 74 | + // TODO use the location API for this |
| 75 | + const nodePosInfo = getNodeById(blockID, tr.doc); |
| 76 | + if (!nodePosInfo) { |
| 77 | + return undefined; |
| 78 | + } |
| 79 | + |
| 80 | + const startPos = nodePosInfo.posBeforeNode + 1; |
| 81 | + const endPos = |
| 82 | + nodePosInfo.posBeforeNode + nodePosInfo.node.content.size + 1; |
| 83 | + // TODO should not need to know the DOM structure here |
| 84 | + const blockElement = editor.prosemirrorView.dom.querySelector( |
| 85 | + `[data-id="${blockID}"]`, |
| 86 | + ); |
| 87 | + |
| 88 | + switch (placement) { |
| 89 | + case "before": { |
| 90 | + return { |
| 91 | + from: startPos, |
| 92 | + contextElement: blockElement, |
| 93 | + }; |
| 94 | + } |
| 95 | + case "after": { |
| 96 | + return { |
| 97 | + from: endPos, |
| 98 | + contextElement: blockElement, |
| 99 | + }; |
| 100 | + } |
| 101 | + case "across": { |
| 102 | + return { |
| 103 | + from: startPos, |
| 104 | + to: endPos, |
| 105 | + contextElement: blockElement, |
| 106 | + }; |
| 107 | + } |
| 108 | + default: { |
| 109 | + throw new Error(`Invalid placement: ${placement}`); |
| 110 | + } |
| 111 | + } |
| 112 | + }); |
| 113 | + }); |
| 114 | +} |
| 115 | + |
| 116 | +// I'm unsure at this point whether the above should be implemented as a hook, or just as utility functions (and on call of `getBoundingClientRect` it can calculate the DOMRect) |
| 117 | +// But, I think that the principle will be the same, select what we need as positions, and then calculate the DOMRect on demand, re-evaluate when we need to. |
| 118 | +// These should be able to help with hooks that can actually position a menu at a given position. |
0 commit comments