a tool for shared writing and social publishing
1import { NodeSelection, TextSelection } from "prosemirror-state";
2import { useUIState } from "src/useUIState";
3import { Block } from "components/Blocks/Block";
4import { elementId } from "src/utils/elementId";
5
6import { useEditorStates } from "src/state/useEditorState";
7import { scrollIntoViewIfNeeded } from "./scrollIntoViewIfNeeded";
8import { getPosAtCoordinates } from "./getCoordinatesInTextarea";
9import { flushSync } from "react-dom";
10
11export function focusBlock(
12 block: Pick<Block, "type" | "value" | "parent">,
13 position: Position,
14) {
15 // focus the block
16 flushSync(() => {
17 useUIState.getState().setSelectedBlock(block);
18 useUIState.getState().setFocusedBlock({
19 entityType: "block",
20 entityID: block.value,
21 parent: block.parent,
22 });
23 });
24 scrollIntoViewIfNeeded(
25 document.getElementById(elementId.block(block.value).container),
26 false,
27 );
28 if (block.type === "math" || block.type === "code") {
29 let el = document.getElementById(
30 elementId.block(block.value).input,
31 ) as HTMLTextAreaElement;
32 let pos;
33 if (position.type === "start") {
34 pos = { offset: 0 };
35 }
36
37 if (position.type === "end") {
38 pos = { offset: el.textContent?.length || 0 };
39 }
40 if (position.type === "top" || position.type === "bottom") {
41 let inputRect = el?.getBoundingClientRect();
42 let left = Math.max(position.left, inputRect?.left || 0);
43 let top =
44 position.type === "top"
45 ? (inputRect?.top || 0) + 10
46 : (inputRect?.bottom || 0) - 10;
47 pos = getPosAtCoordinates(left, top);
48 }
49
50 if (pos?.offset !== undefined) {
51 el?.focus();
52 requestAnimationFrame(() => {
53 el?.setSelectionRange(pos.offset, pos.offset);
54 });
55 }
56 }
57
58 // if its not a text block, that's all we need to do
59 if (
60 block.type !== "text" &&
61 block.type !== "heading" &&
62 block.type !== "blockquote"
63 ) {
64 return true;
65 }
66 // if its a text block, and not an empty block that is last on the page,
67 // focus the editor using the mouse position if needed
68 let nextBlockID = block.value;
69 let nextBlock = useEditorStates.getState().editorStates[nextBlockID];
70 if (!nextBlock || !nextBlock.view) return;
71 let nextBlockViewClientRect = nextBlock.view.dom.getBoundingClientRect();
72 let tr = nextBlock.editor.tr;
73 let pos: { pos: number } | null = null;
74 switch (position.type) {
75 case "end": {
76 pos = { pos: tr.doc.content.size - 1 };
77 break;
78 }
79 case "start": {
80 pos = { pos: 1 };
81 break;
82 }
83 case "top": {
84 pos = nextBlock.view.posAtCoords({
85 top: nextBlockViewClientRect.top + 12,
86 left: Math.max(position.left, nextBlockViewClientRect.left),
87 });
88 break;
89 }
90 case "bottom": {
91 pos = nextBlock.view.posAtCoords({
92 top: nextBlockViewClientRect.bottom - 12,
93 left: Math.max(position.left, nextBlockViewClientRect.left),
94 });
95 break;
96 }
97 case "coord": {
98 pos = nextBlock.view.posAtCoords({
99 top: position.top,
100 left: position.left,
101 });
102 break;
103 }
104 }
105
106 nextBlock.view.dispatch(
107 tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)),
108 );
109 nextBlock.view.focus();
110}
111
112type Position =
113 | {
114 type: "start";
115 }
116 | { type: "end" }
117 | {
118 type: "coord";
119 top: number;
120 left: number;
121 }
122 | {
123 type: "top";
124 left: number;
125 }
126 | {
127 type: "bottom";
128 left: number;
129 };