import { useSelectingMouse } from "components/SelectionManager/selectionState"; import { MouseEvent, useCallback, useRef } from "react"; import { useUIState } from "src/useUIState"; import { Block } from "./Block"; import { isTextBlock } from "src/utils/isTextBlock"; import { useEntitySetContext } from "components/EntitySetProvider"; import { useReplicache } from "src/replicache"; import { getPageBlocks } from "src/replicache/getBlocks"; import { focusBlock } from "src/utils/focusBlock"; import { useIsMobile } from "src/hooks/isMobile"; import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded"; import { elementId } from "src/utils/elementId"; let debounce: number | null = null; // Track scrolling state for mobile let isScrolling = false; let scrollTimeout: number | null = null; if (typeof window !== "undefined") { window.addEventListener( "scroll", () => { isScrolling = true; if (scrollTimeout) window.clearTimeout(scrollTimeout); scrollTimeout = window.setTimeout(() => { isScrolling = false; }, 150); }, true, ); } export function useBlockMouseHandlers(props: Block) { let entity_set = useEntitySetContext(); let isMobile = useIsMobile(); let { rep } = useReplicache(); // Handlers read everything through a ref so their identity is stable across // renders (a `props` dep would recreate them every render). let latest = useRef({ props, write: entity_set.permissions.write, isMobile, rep, }); latest.current = { props, write: entity_set.permissions.write, isMobile, rep }; let onMouseDown = useCallback((e: MouseEvent) => { let { props, write, isMobile } = latest.current; if ((e.target as Element).getAttribute("data-draggable")) return; if ((e.target as Element).tagName === "BUTTON") return; if ((e.target as Element).tagName === "SELECT") return; if ((e.target as Element).tagName === "OPTION") return; if (isMobile && isScrolling) return; if (!write) return; useSelectingMouse.setState({ start: props.entityID }); if (e.shiftKey) { if ( useUIState.getState().selectedBlocks[0]?.entityID === props.entityID && useUIState.getState().selectedBlocks.length === 1 ) return; e.preventDefault(); useUIState.getState().addBlockToSelection(props); } else { if (e.isDefaultPrevented()) return; useUIState.getState().focusAndSelectBlock(props); // scroll to the page containing the block, if offscreen let parentPage = elementId.page(props.parent).container; setTimeout(() => { scrollIntoViewIfNeeded( document.getElementById(parentPage), false, "smooth", ); }, 50); } }, []); let onMouseEnter = useCallback(async (e: MouseEvent) => { let { props, write, isMobile, rep } = latest.current; if (isMobile && isScrolling) return; if (!write) return; if (debounce) window.clearTimeout(debounce); debounce = window.setTimeout(async () => { debounce = null; if (e.buttons !== 1) return; let selection = useSelectingMouse.getState(); if (!selection.start) return; let siblings = rep ? getPageBlocks(rep, props.parent) : []; let startIndex = siblings.findIndex((b) => b.entityID === selection.start); if (startIndex === -1) return; let endIndex = siblings.findIndex((b) => b.entityID === props.entityID); let start = Math.min(startIndex, endIndex); let end = Math.max(startIndex, endIndex); let selected = siblings.slice(start, end + 1).map((b) => ({ entityID: b.entityID, parent: props.parent, })); useUIState.getState().setSelectedBlocks(selected); }, 15); }, []); return { onMouseDown, onMouseEnter }; }