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 console.log(pos);
89 break;
90 }
91 case "bottom": {
92 pos = nextBlock.view.posAtCoords({
93 top: nextBlockViewClientRect.bottom - 12,
94 left: Math.max(position.left, nextBlockViewClientRect.left),
95 });
96 break;
97 }
98 case "coord": {
99 pos = nextBlock.view.posAtCoords({
100 top: position.top,
101 left: position.left,
102 });
103 break;
104 }
105 }
106
107 nextBlock.view.dispatch(
108 tr.setSelection(TextSelection.create(tr.doc, pos?.pos || 1)),
109 );
110 nextBlock.view.focus();
111}
112
113type Position =
114 | {
115 type: "start";
116 }
117 | { type: "end" }
118 | {
119 type: "coord";
120 top: number;
121 left: number;
122 }
123 | {
124 type: "top";
125 left: number;
126 }
127 | {
128 type: "bottom";
129 left: number;
130 };