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 };