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 undefined,
28 -80,
29 );
30 if (block.type === "math" || block.type === "code") {
31 let el = document.getElementById(
32 elementId.block(block.value).input,
33 ) as HTMLTextAreaElement;
34 let pos;
35 if (position.type === "start") {
36 pos = { offset: 0 };
37 }
38
39 if (position.type === "end") {
40 pos = { offset: el.textContent?.length || 0 };
41 }
42 if (position.type === "top" || position.type === "bottom") {
43 let inputRect = el?.getBoundingClientRect();
44 let left = Math.max(position.left, inputRect?.left || 0);
45 let top =
46 position.type === "top"
47 ? (inputRect?.top || 0) + 10
48 : (inputRect?.bottom || 0) - 10;
49 pos = getPosAtCoordinates(left, top);
50 }
51
52 if (pos?.offset !== undefined) {
53 el?.focus();
54 requestAnimationFrame(() => {
55 el?.setSelectionRange(pos.offset, pos.offset);
56 });
57 }
58 }
59
60 // if its not a text block, that's all we need to do
61 if (
62 block.type !== "text" &&
63 block.type !== "heading" &&
64 block.type !== "blockquote"
65 ) {
66 return true;
67 }
68 // if its a text block, and not an empty block that is last on the page,
69 // focus the editor using the mouse position if needed
70 let nextBlockID = block.value;
71 let nextBlock = useEditorStates.getState().editorStates[nextBlockID];
72 if (!nextBlock || !nextBlock.view) return;
73 let nextBlockViewClientRect = nextBlock.view.dom.getBoundingClientRect();
74 let tr = nextBlock.editor.tr;
75 let pos: { pos: number } | null = null;
76 switch (position.type) {
77 case "end": {
78 pos = { pos: tr.doc.content.size - 1 };
79 break;
80 }
81 case "start": {
82 pos = { pos: 1 };
83 break;
84 }
85 case "top": {
86 pos = nextBlock.view.posAtCoords({
87 top: nextBlockViewClientRect.top + 12,
88 left: Math.max(position.left, nextBlockViewClientRect.left),
89 });
90 break;
91 }
92 case "bottom": {
93 pos = nextBlock.view.posAtCoords({
94 top: nextBlockViewClientRect.bottom - 12,
95 left: Math.max(position.left, nextBlockViewClientRect.left),
96 });
97 break;
98 }
99 case "coord": {
100 pos = nextBlock.view.posAtCoords({
101 top: position.top,
102 left: position.left,
103 });
104 break;
105 }
106 }
107
108 nextBlock.view.dispatch(
109 tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)),
110 );
111 nextBlock.view.focus();
112}
113
114type Position =
115 | {
116 type: "start";
117 }
118 | { type: "end" }
119 | {
120 type: "coord";
121 top: number;
122 left: number;
123 }
124 | {
125 type: "top";
126 left: number;
127 }
128 | {
129 type: "bottom";
130 left: number;
131 };