a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve drag to select to prevent gaps

+42 -43
+25 -17
components/Blocks.tsx
··· 14 14 import { useBlocks } from "src/hooks/queries/useBlocks"; 15 15 import { setEditorState, useEditorStates } from "src/state/useEditorState"; 16 16 import { useEntitySetContext } from "./EntitySetProvider"; 17 + import { scanIndex } from "src/replicache/utils"; 17 18 18 19 export type Block = { 19 20 parent: string; ··· 243 244 useUIState.getState().addBlockToSelection(props); 244 245 } else useUIState.getState().setSelectedBlock(props); 245 246 }} 246 - onMouseEnter={(e) => { 247 + onMouseEnter={async (e) => { 248 + if (e.buttons !== 1) return; 247 249 let selection = useSelectingMouse.getState(); 248 250 if (!selection.start) return; 249 - useUIState.getState().addBlockToSelection(props); 250 - }} 251 - onMouseLeave={(e) => { 252 - let selection = useSelectingMouse.getState(); 253 - if (!selection.start) return; 254 - let rect = e.currentTarget.getBoundingClientRect(); 255 - let topMin = Math.min(selection.start.top, e.clientY); 256 - let topMax = Math.max(selection.start.top, e.clientY); 257 - if (rect.top >= topMin && rect.top < topMax) { 258 - return; 259 - } 260 - 261 - if (rect.bottom > topMin && rect.bottom <= topMax) { 262 - return; 263 - } 264 - useUIState.getState().removeBlockFromSelection(props); 251 + let siblings = 252 + (await rep?.query((tx) => 253 + scanIndex(tx).eav(props.parent, "card/block"), 254 + )) || []; 255 + let sortedSiblings = siblings.sort((a, b) => 256 + a.data.position > b.data.position ? 1 : -1, 257 + ); 258 + let startIndex = sortedSiblings.findIndex( 259 + (b) => b.data.value === selection.start, 260 + ); 261 + if (startIndex === -1) return; 262 + let endIndex = sortedSiblings.findIndex( 263 + (b) => b.data.value === props.entityID, 264 + ); 265 + let start = Math.min(startIndex, endIndex); 266 + let end = Math.max(startIndex, endIndex); 267 + let selected = sortedSiblings.slice(start, end + 1).map((b) => ({ 268 + value: b.data.value, 269 + position: b.data.position, 270 + parent: props.parent, 271 + })); 272 + useUIState.getState().setSelectedBlocks(selected); 265 273 }} 266 274 // text and heading blocks handle thier own padding so that 267 275 // clicking anywhere on them (even the padding between blocks) will focus the textarea
+16 -20
components/SelectionManager.tsx
··· 1 1 "use client"; 2 - import { useEffect, useRef } from "react"; 2 + import { useEffect, useRef, useState } from "react"; 3 3 import { create } from "zustand"; 4 4 import { useReplicache } from "src/replicache"; 5 5 import { useUIState } from "src/useUIState"; ··· 8 8 import { focusBlock } from "./Blocks"; 9 9 import { useEditorStates } from "src/state/useEditorState"; 10 10 export const useSelectingMouse = create(() => ({ 11 - start: null as null | { top: number; left: number }, 11 + start: null as null | string, 12 12 })); 13 13 14 14 //How should I model selection? As ranges w/ a start and end? Store *blocks* so that I can just construct ranges? ··· 252 252 window.removeEventListener("keydown", listener); 253 253 }; 254 254 }, [moreThanOneSelected, rep]); 255 + 256 + let [mouseDown, setMouseDown] = useState(false); 255 257 let dragStart = useSelectingMouse((s) => s.start); 256 258 let initialContentEditableParent = useRef<null | Node>(null); 257 259 let savedSelection = useRef<SavedRange[] | null>(); 258 260 useEffect(() => { 259 261 let mouseDownListener = (e: MouseEvent) => { 260 - initialContentEditableParent.current = getContentEditableParent( 261 - e.target as Node, 262 - ); 263 - useSelectingMouse.setState({ 264 - start: { left: e.clientX, top: e.clientY }, 265 - }); 262 + setMouseDown(true); 263 + let contentEditableParent = getContentEditableParent(e.target as Node); 264 + if (contentEditableParent) { 265 + let entityID = (contentEditableParent as Element).getAttribute( 266 + "data-entityid", 267 + ); 268 + useSelectingMouse.setState({ start: entityID }); 269 + } 270 + initialContentEditableParent.current = contentEditableParent; 266 271 }; 267 272 let mouseUpListener = (e: MouseEvent) => { 268 - if ( 269 - initialContentEditableParent.current && 270 - getContentEditableParent(e.target as Node) !== 271 - initialContentEditableParent.current 272 - ) { 273 - setTimeout(() => { 274 - window.getSelection()?.removeAllRanges(); 275 - }, 5); 276 - } 277 273 savedSelection.current = null; 278 - useSelectingMouse.setState({ start: null }); 274 + setMouseDown(false); 279 275 }; 280 276 window.addEventListener("mousedown", mouseDownListener); 281 277 window.addEventListener("mouseup", mouseUpListener); ··· 285 281 }; 286 282 }, []); 287 283 useEffect(() => { 288 - if (!dragStart) return; 284 + if (!mouseDown) return; 289 285 let mouseMoveListener = (e: MouseEvent) => { 290 286 if (e.buttons !== 1) return; 291 287 if (initialContentEditableParent.current) { ··· 307 303 return () => { 308 304 window.removeEventListener("mousemove", mouseMoveListener); 309 305 }; 310 - }, [dragStart]); 306 + }, [mouseDown]); 311 307 return null; 312 308 } 313 309
+1 -6
components/TextBlock/index.tsx
··· 214 214 }} 215 215 > 216 216 <pre 217 + data-entityid={props.entityID} 217 218 onBlur={async () => { 218 219 if (editorState.doc.textContent.startsWith("http")) { 219 220 await addLinkBlock( ··· 234 235 }, 235 236 })); 236 237 }, 5); 237 - }} 238 - onSelect={() => { 239 - useUIState.setState((s) => ({ 240 - ...s, 241 - focusedTextBlock: props.entityID, 242 - })); 243 238 }} 244 239 id={elementId.block(props.entityID).text} 245 240 className={`