a tool for shared writing and social publishing
at main 3.5 kB view raw
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 };