a tool for shared writing and social publishing
1import { useSelectingMouse } from "components/SelectionManager/selectionState";
2import { MouseEvent, useCallback } from "react";
3import { useUIState } from "src/useUIState";
4import { Block } from "./Block";
5import { isTextBlock } from "src/utils/isTextBlock";
6import { useEntitySetContext } from "components/EntitySetProvider";
7import { useReplicache } from "src/replicache";
8import { getBlocksWithType } from "src/hooks/queries/useBlocks";
9import { focusBlock } from "src/utils/focusBlock";
10import { useIsMobile } from "src/hooks/isMobile";
11import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded";
12import { elementId } from "src/utils/elementId";
13
14let debounce: number | null = null;
15
16// Track scrolling state for mobile
17let isScrolling = false;
18let scrollTimeout: number | null = null;
19
20if (typeof window !== "undefined") {
21 window.addEventListener(
22 "scroll",
23 () => {
24 isScrolling = true;
25 if (scrollTimeout) window.clearTimeout(scrollTimeout);
26 scrollTimeout = window.setTimeout(() => {
27 isScrolling = false;
28 }, 150);
29 },
30 true,
31 );
32}
33export function useBlockMouseHandlers(props: Block) {
34 let entity_set = useEntitySetContext();
35 let isMobile = useIsMobile();
36 let { rep } = useReplicache();
37 let onMouseDown = useCallback(
38 (e: MouseEvent) => {
39 if ((e.target as Element).getAttribute("data-draggable")) return;
40 if ((e.target as Element).tagName === "BUTTON") return;
41 if ((e.target as Element).tagName === "SELECT") return;
42 if ((e.target as Element).tagName === "OPTION") return;
43 if (isMobile && isScrolling) return;
44 if (!entity_set.permissions.write) return;
45 useSelectingMouse.setState({ start: props.value });
46 if (e.shiftKey) {
47 if (
48 useUIState.getState().selectedBlocks[0]?.value === props.value &&
49 useUIState.getState().selectedBlocks.length === 1
50 )
51 return;
52 e.preventDefault();
53 useUIState.getState().addBlockToSelection(props);
54 } else {
55 if (e.isDefaultPrevented()) return;
56 useUIState.getState().setFocusedBlock({
57 entityType: "block",
58 entityID: props.value,
59 parent: props.parent,
60 });
61 useUIState.getState().setSelectedBlock(props);
62
63 // scroll to the page containing the block, if offscreen
64 let parentPage = elementId.page(props.parent).container;
65 setTimeout(() => {
66 scrollIntoViewIfNeeded(
67 document.getElementById(parentPage),
68 false,
69 "smooth",
70 );
71 }, 50);
72 }
73 },
74 [props, entity_set.permissions.write, isMobile],
75 );
76 let onMouseEnter = useCallback(
77 async (e: MouseEvent) => {
78 if (isMobile && isScrolling) return;
79 if (!entity_set.permissions.write) return;
80 if (debounce) window.clearTimeout(debounce);
81 debounce = window.setTimeout(async () => {
82 debounce = null;
83 if (e.buttons !== 1) return;
84 let selection = useSelectingMouse.getState();
85 if (!selection.start) return;
86 let siblings =
87 (await rep?.query((tx) => getBlocksWithType(tx, props.parent))) || [];
88 let startIndex = siblings.findIndex((b) => b.value === selection.start);
89 if (startIndex === -1) return;
90 let endIndex = siblings.findIndex((b) => b.value === props.value);
91 let start = Math.min(startIndex, endIndex);
92 let end = Math.max(startIndex, endIndex);
93 let selected = siblings.slice(start, end + 1).map((b) => ({
94 value: b.value,
95 position: b.position,
96 parent: props.parent,
97 }));
98 useUIState.getState().setSelectedBlocks(selected);
99 }, 15);
100 },
101 [rep, props, entity_set.permissions.write, isMobile],
102 );
103 return { onMouseDown, onMouseEnter };
104}