a tool for shared writing and social publishing
at feature/post-options 864 lines 30 kB view raw
1"use client"; 2import { useEffect, useRef, useState } from "react"; 3import { useReplicache } from "src/replicache"; 4import { useUIState } from "src/useUIState"; 5import { scanIndex } from "src/replicache/utils"; 6import { focusBlock } from "src/utils/focusBlock"; 7import { useEditorStates } from "src/state/useEditorState"; 8import { useEntitySetContext } from "../EntitySetProvider"; 9import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 10import { indent, outdent, outdentFull } from "src/utils/list-operations"; 11import { addShortcut, Shortcut } from "src/shortcuts"; 12import { elementId } from "src/utils/elementId"; 13import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded"; 14import { copySelection } from "src/utils/copySelection"; 15import { useIsMobile } from "src/hooks/isMobile"; 16import { deleteBlock } from "src/utils/deleteBlock"; 17import { schema } from "../Blocks/TextBlock/schema"; 18import { MarkType } from "prosemirror-model"; 19import { useSelectingMouse, getSortedSelection } from "./selectionState"; 20 21//How should I model selection? As ranges w/ a start and end? Store *blocks* so that I can just construct ranges? 22// How does this relate to *when dragging* ? 23 24export function SelectionManager() { 25 let moreThanOneSelected = useUIState((s) => s.selectedBlocks.length > 1); 26 let entity_set = useEntitySetContext(); 27 let { rep, undoManager } = useReplicache(); 28 let isMobile = useIsMobile(); 29 useEffect(() => { 30 if (!entity_set.permissions.write || !rep) return; 31 const getSortedSelectionBound = getSortedSelection.bind(null, rep); 32 let shortcuts: Shortcut[] = [ 33 { 34 metaKey: true, 35 key: "ArrowUp", 36 handler: async () => { 37 let [firstBlock] = 38 (await rep?.query((tx) => 39 getBlocksWithType( 40 tx, 41 useUIState.getState().selectedBlocks[0].parent, 42 ), 43 )) || []; 44 if (firstBlock) focusBlock(firstBlock, { type: "start" }); 45 }, 46 }, 47 { 48 metaKey: true, 49 key: "ArrowDown", 50 handler: async () => { 51 let blocks = 52 (await rep?.query((tx) => 53 getBlocksWithType( 54 tx, 55 useUIState.getState().selectedBlocks[0].parent, 56 ), 57 )) || []; 58 let folded = useUIState.getState().foldedBlocks; 59 blocks = blocks.filter( 60 (f) => 61 !f.listData || 62 !f.listData.path.find( 63 (path) => 64 folded.includes(path.entity) && f.value !== path.entity, 65 ), 66 ); 67 let lastBlock = blocks[blocks.length - 1]; 68 if (lastBlock) focusBlock(lastBlock, { type: "end" }); 69 }, 70 }, 71 { 72 metaKey: true, 73 altKey: true, 74 key: ["l", "¬"], 75 handler: async () => { 76 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 77 for (let block of sortedBlocks) { 78 if (!block.listData) { 79 await rep?.mutate.assertFact({ 80 entity: block.value, 81 attribute: "block/is-list", 82 data: { type: "boolean", value: true }, 83 }); 84 } else { 85 outdentFull(block, rep); 86 } 87 } 88 }, 89 }, 90 { 91 metaKey: true, 92 altKey: true, 93 key: ["1", "¡"], 94 handler: async () => { 95 let [sortedBlocks] = await getSortedSelectionBound(); 96 for (let block of sortedBlocks) { 97 await rep?.mutate.assertFact({ 98 entity: block.value, 99 attribute: "block/heading-level", 100 data: { type: "number", value: 1 }, 101 }); 102 await rep?.mutate.assertFact({ 103 entity: block.value, 104 attribute: "block/type", 105 data: { type: "block-type-union", value: "heading" }, 106 }); 107 } 108 }, 109 }, 110 { 111 metaKey: true, 112 altKey: true, 113 key: ["2", "™"], 114 handler: async () => { 115 let [sortedBlocks] = await getSortedSelectionBound(); 116 for (let block of sortedBlocks) { 117 await rep?.mutate.assertFact({ 118 entity: block.value, 119 attribute: "block/heading-level", 120 data: { type: "number", value: 2 }, 121 }); 122 await rep?.mutate.assertFact({ 123 entity: block.value, 124 attribute: "block/type", 125 data: { type: "block-type-union", value: "heading" }, 126 }); 127 } 128 }, 129 }, 130 { 131 metaKey: true, 132 altKey: true, 133 key: ["3", "£"], 134 handler: async () => { 135 let [sortedBlocks] = await getSortedSelectionBound(); 136 for (let block of sortedBlocks) { 137 await rep?.mutate.assertFact({ 138 entity: block.value, 139 attribute: "block/heading-level", 140 data: { type: "number", value: 3 }, 141 }); 142 await rep?.mutate.assertFact({ 143 entity: block.value, 144 attribute: "block/type", 145 data: { type: "block-type-union", value: "heading" }, 146 }); 147 } 148 }, 149 }, 150 { 151 metaKey: true, 152 altKey: true, 153 key: ["0", "º"], 154 handler: async () => { 155 let [sortedBlocks] = await getSortedSelectionBound(); 156 for (let block of sortedBlocks) { 157 // Convert to text block 158 await rep?.mutate.assertFact({ 159 entity: block.value, 160 attribute: "block/type", 161 data: { type: "block-type-union", value: "text" }, 162 }); 163 // Remove heading level if exists 164 let headingLevel = await rep?.query((tx) => 165 scanIndex(tx).eav(block.value, "block/heading-level"), 166 ); 167 if (headingLevel?.[0]) { 168 await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 169 } 170 // Remove text-size to make it default 171 let textSizeFact = await rep?.query((tx) => 172 scanIndex(tx).eav(block.value, "block/text-size"), 173 ); 174 if (textSizeFact?.[0]) { 175 await rep?.mutate.retractFact({ factID: textSizeFact[0].id }); 176 } 177 } 178 }, 179 }, 180 { 181 metaKey: true, 182 altKey: true, 183 key: ["+", "≠"], 184 handler: async () => { 185 let [sortedBlocks] = await getSortedSelectionBound(); 186 for (let block of sortedBlocks) { 187 // Convert to text block 188 await rep?.mutate.assertFact({ 189 entity: block.value, 190 attribute: "block/type", 191 data: { type: "block-type-union", value: "text" }, 192 }); 193 // Remove heading level if exists 194 let headingLevel = await rep?.query((tx) => 195 scanIndex(tx).eav(block.value, "block/heading-level"), 196 ); 197 if (headingLevel?.[0]) { 198 await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 199 } 200 // Set text size to large 201 await rep?.mutate.assertFact({ 202 entity: block.value, 203 attribute: "block/text-size", 204 data: { type: "text-size-union", value: "large" }, 205 }); 206 } 207 }, 208 }, 209 { 210 metaKey: true, 211 altKey: true, 212 key: ["-", "–"], 213 handler: async () => { 214 let [sortedBlocks] = await getSortedSelectionBound(); 215 for (let block of sortedBlocks) { 216 // Convert to text block 217 await rep?.mutate.assertFact({ 218 entity: block.value, 219 attribute: "block/type", 220 data: { type: "block-type-union", value: "text" }, 221 }); 222 // Remove heading level if exists 223 let headingLevel = await rep?.query((tx) => 224 scanIndex(tx).eav(block.value, "block/heading-level"), 225 ); 226 if (headingLevel?.[0]) { 227 await rep?.mutate.retractFact({ factID: headingLevel[0].id }); 228 } 229 // Set text size to small 230 await rep?.mutate.assertFact({ 231 entity: block.value, 232 attribute: "block/text-size", 233 data: { type: "text-size-union", value: "small" }, 234 }); 235 } 236 }, 237 }, 238 { 239 metaKey: true, 240 shift: true, 241 key: ["ArrowDown", "J"], 242 handler: async () => { 243 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 244 let block = sortedBlocks[0]; 245 let nextBlock = siblings 246 .slice(siblings.findIndex((s) => s.value === block.value) + 1) 247 .find( 248 (f) => 249 f.listData && 250 block.listData && 251 !f.listData.path.find((f) => f.entity === block.value), 252 ); 253 if ( 254 nextBlock?.listData && 255 block.listData && 256 nextBlock.listData.depth === block.listData.depth - 1 257 ) { 258 if (useUIState.getState().foldedBlocks.includes(nextBlock.value)) 259 useUIState.getState().toggleFold(nextBlock.value); 260 await rep?.mutate.moveBlock({ 261 block: block.value, 262 oldParent: block.listData?.parent, 263 newParent: nextBlock.value, 264 position: { type: "first" }, 265 }); 266 } else { 267 await rep?.mutate.moveBlockDown({ 268 entityID: block.value, 269 parent: block.listData?.parent || block.parent, 270 }); 271 } 272 }, 273 }, 274 { 275 metaKey: true, 276 shift: true, 277 key: ["ArrowUp", "K"], 278 handler: async () => { 279 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 280 let block = sortedBlocks[0]; 281 let previousBlock = 282 siblings?.[siblings.findIndex((s) => s.value === block.value) - 1]; 283 if (previousBlock.value === block.listData?.parent) { 284 previousBlock = 285 siblings?.[ 286 siblings.findIndex((s) => s.value === block.value) - 2 287 ]; 288 } 289 290 if ( 291 previousBlock?.listData && 292 block.listData && 293 block.listData.depth > 1 && 294 !previousBlock.listData.path.find( 295 (f) => f.entity === block.listData?.parent, 296 ) 297 ) { 298 let depth = block.listData.depth; 299 let newParent = previousBlock.listData.path.find( 300 (f) => f.depth === depth - 1, 301 ); 302 if (!newParent) return; 303 if (useUIState.getState().foldedBlocks.includes(newParent.entity)) 304 useUIState.getState().toggleFold(newParent.entity); 305 rep?.mutate.moveBlock({ 306 block: block.value, 307 oldParent: block.listData?.parent, 308 newParent: newParent.entity, 309 position: { type: "end" }, 310 }); 311 } else { 312 rep?.mutate.moveBlockUp({ 313 entityID: block.value, 314 parent: block.listData?.parent || block.parent, 315 }); 316 } 317 }, 318 }, 319 320 { 321 metaKey: true, 322 shift: true, 323 key: "Enter", 324 handler: async () => { 325 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 326 if (!sortedBlocks[0].listData) return; 327 useUIState.getState().toggleFold(sortedBlocks[0].value); 328 }, 329 }, 330 ]; 331 if (moreThanOneSelected) 332 shortcuts = shortcuts.concat([ 333 { 334 metaKey: true, 335 key: "u", 336 handler: async () => { 337 let [sortedBlocks] = await getSortedSelectionBound(); 338 toggleMarkInBlocks( 339 sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 340 schema.marks.underline, 341 ); 342 }, 343 }, 344 { 345 metaKey: true, 346 key: "i", 347 handler: async () => { 348 let [sortedBlocks] = await getSortedSelectionBound(); 349 toggleMarkInBlocks( 350 sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 351 schema.marks.em, 352 ); 353 }, 354 }, 355 { 356 metaKey: true, 357 key: "b", 358 handler: async () => { 359 let [sortedBlocks] = await getSortedSelectionBound(); 360 toggleMarkInBlocks( 361 sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 362 schema.marks.strong, 363 ); 364 }, 365 }, 366 { 367 metaAndCtrl: true, 368 key: "h", 369 handler: async () => { 370 let [sortedBlocks] = await getSortedSelectionBound(); 371 toggleMarkInBlocks( 372 sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 373 schema.marks.highlight, 374 { 375 color: useUIState.getState().lastUsedHighlight, 376 }, 377 ); 378 }, 379 }, 380 { 381 metaAndCtrl: true, 382 key: "x", 383 handler: async () => { 384 let [sortedBlocks] = await getSortedSelectionBound(); 385 toggleMarkInBlocks( 386 sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 387 schema.marks.strikethrough, 388 ); 389 }, 390 }, 391 ]); 392 let removeListener = addShortcut( 393 shortcuts.map((shortcut) => ({ 394 ...shortcut, 395 handler: () => undoManager.withUndoGroup(() => shortcut.handler()), 396 })), 397 ); 398 let listener = async (e: KeyboardEvent) => 399 undoManager.withUndoGroup(async () => { 400 //used here and in cut 401 const deleteBlocks = async () => { 402 if (!entity_set.permissions.write) return; 403 if (moreThanOneSelected) { 404 e.preventDefault(); 405 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 406 let selectedBlocks = useUIState.getState().selectedBlocks; 407 let firstBlock = sortedBlocks[0]; 408 409 await rep?.mutate.removeBlock( 410 selectedBlocks.map((block) => ({ blockEntity: block.value })), 411 ); 412 useUIState.getState().closePage(selectedBlocks.map((b) => b.value)); 413 414 let nextBlock = 415 siblings?.[ 416 siblings.findIndex((s) => s.value === firstBlock.value) - 1 417 ]; 418 if (nextBlock) { 419 useUIState.getState().setSelectedBlock({ 420 value: nextBlock.value, 421 parent: nextBlock.parent, 422 }); 423 let type = await rep?.query((tx) => 424 scanIndex(tx).eav(nextBlock.value, "block/type"), 425 ); 426 if (!type?.[0]) return; 427 if ( 428 type[0]?.data.value === "text" || 429 type[0]?.data.value === "heading" 430 ) 431 focusBlock( 432 { 433 value: nextBlock.value, 434 type: "text", 435 parent: nextBlock.parent, 436 }, 437 { type: "end" }, 438 ); 439 } 440 } 441 }; 442 if (e.key === "Backspace" || e.key === "Delete") { 443 deleteBlocks(); 444 } 445 if (e.key === "ArrowUp") { 446 let [sortedBlocks, siblings] = await getSortedSelectionBound(); 447 let focusedBlock = useUIState.getState().focusedEntity; 448 if (!e.shiftKey && !e.ctrlKey) { 449 if (e.defaultPrevented) return; 450 if (sortedBlocks.length === 1) return; 451 let firstBlock = sortedBlocks[0]; 452 if (!firstBlock) return; 453 let type = await rep?.query((tx) => 454 scanIndex(tx).eav(firstBlock.value, "block/type"), 455 ); 456 if (!type?.[0]) return; 457 useUIState.getState().setSelectedBlock(firstBlock); 458 focusBlock( 459 { ...firstBlock, type: type[0].data.value }, 460 { type: "start" }, 461 ); 462 } else { 463 if (e.defaultPrevented) return; 464 if ( 465 sortedBlocks.length <= 1 || 466 !focusedBlock || 467 focusedBlock.entityType === "page" 468 ) 469 return; 470 let b = focusedBlock; 471 let focusedBlockIndex = sortedBlocks.findIndex( 472 (s) => s.value == b.entityID, 473 ); 474 if (focusedBlockIndex === 0) { 475 let index = siblings.findIndex((s) => s.value === b.entityID); 476 let nextSelectedBlock = siblings[index - 1]; 477 if (!nextSelectedBlock) return; 478 479 scrollIntoViewIfNeeded( 480 document.getElementById( 481 elementId.block(nextSelectedBlock.value).container, 482 ), 483 false, 484 ); 485 useUIState.getState().addBlockToSelection({ 486 ...nextSelectedBlock, 487 }); 488 useUIState.getState().setFocusedBlock({ 489 entityType: "block", 490 parent: nextSelectedBlock.parent, 491 entityID: nextSelectedBlock.value, 492 }); 493 } else { 494 let nextBlock = sortedBlocks[sortedBlocks.length - 2]; 495 useUIState.getState().setFocusedBlock({ 496 entityType: "block", 497 parent: b.parent, 498 entityID: nextBlock.value, 499 }); 500 scrollIntoViewIfNeeded( 501 document.getElementById( 502 elementId.block(nextBlock.value).container, 503 ), 504 false, 505 ); 506 if (sortedBlocks.length === 2) { 507 useEditorStates 508 .getState() 509 .editorStates[nextBlock.value]?.view?.focus(); 510 } 511 useUIState 512 .getState() 513 .removeBlockFromSelection(sortedBlocks[focusedBlockIndex]); 514 } 515 } 516 } 517 if (e.key === "ArrowLeft") { 518 let [sortedSelection, siblings] = await getSortedSelectionBound(); 519 if (sortedSelection.length === 1) return; 520 let firstBlock = sortedSelection[0]; 521 if (!firstBlock) return; 522 let type = await rep?.query((tx) => 523 scanIndex(tx).eav(firstBlock.value, "block/type"), 524 ); 525 if (!type?.[0]) return; 526 useUIState.getState().setSelectedBlock(firstBlock); 527 focusBlock( 528 { ...firstBlock, type: type[0].data.value }, 529 { type: "start" }, 530 ); 531 } 532 if (e.key === "ArrowRight") { 533 let [sortedSelection, siblings] = await getSortedSelectionBound(); 534 if (sortedSelection.length === 1) return; 535 let lastBlock = sortedSelection[sortedSelection.length - 1]; 536 if (!lastBlock) return; 537 let type = await rep?.query((tx) => 538 scanIndex(tx).eav(lastBlock.value, "block/type"), 539 ); 540 if (!type?.[0]) return; 541 useUIState.getState().setSelectedBlock(lastBlock); 542 focusBlock( 543 { ...lastBlock, type: type[0].data.value }, 544 { type: "end" }, 545 ); 546 } 547 if (e.key === "Tab") { 548 let [sortedSelection, siblings] = await getSortedSelectionBound(); 549 if (sortedSelection.length <= 1) return; 550 e.preventDefault(); 551 if (e.shiftKey) { 552 for (let i = siblings.length - 1; i >= 0; i--) { 553 let block = siblings[i]; 554 if (!sortedSelection.find((s) => s.value === block.value)) 555 continue; 556 if ( 557 sortedSelection.find((s) => s.value === block.listData?.parent) 558 ) 559 continue; 560 let parentoffset = 1; 561 let previousBlock = siblings[i - parentoffset]; 562 while ( 563 previousBlock && 564 sortedSelection.find((s) => previousBlock.value === s.value) 565 ) { 566 parentoffset += 1; 567 previousBlock = siblings[i - parentoffset]; 568 } 569 if (!block.listData || !previousBlock.listData) continue; 570 outdent(block, previousBlock, rep); 571 } 572 } else { 573 for (let i = 0; i < siblings.length; i++) { 574 let block = siblings[i]; 575 if (!sortedSelection.find((s) => s.value === block.value)) 576 continue; 577 if ( 578 sortedSelection.find((s) => s.value === block.listData?.parent) 579 ) 580 continue; 581 let parentoffset = 1; 582 let previousBlock = siblings[i - parentoffset]; 583 while ( 584 previousBlock && 585 sortedSelection.find((s) => previousBlock.value === s.value) 586 ) { 587 parentoffset += 1; 588 previousBlock = siblings[i - parentoffset]; 589 } 590 if (!block.listData || !previousBlock.listData) continue; 591 indent(block, previousBlock, rep); 592 } 593 } 594 } 595 if (e.key === "ArrowDown") { 596 let [sortedSelection, siblings] = await getSortedSelectionBound(); 597 let focusedBlock = useUIState.getState().focusedEntity; 598 if (!e.shiftKey) { 599 if (sortedSelection.length === 1) return; 600 let lastBlock = sortedSelection[sortedSelection.length - 1]; 601 if (!lastBlock) return; 602 let type = await rep?.query((tx) => 603 scanIndex(tx).eav(lastBlock.value, "block/type"), 604 ); 605 if (!type?.[0]) return; 606 useUIState.getState().setSelectedBlock(lastBlock); 607 focusBlock( 608 { ...lastBlock, type: type[0].data.value }, 609 { type: "end" }, 610 ); 611 } 612 if (e.shiftKey) { 613 if (e.defaultPrevented) return; 614 if ( 615 sortedSelection.length <= 1 || 616 !focusedBlock || 617 focusedBlock.entityType === "page" 618 ) 619 return; 620 let b = focusedBlock; 621 let focusedBlockIndex = sortedSelection.findIndex( 622 (s) => s.value == b.entityID, 623 ); 624 if (focusedBlockIndex === sortedSelection.length - 1) { 625 let index = siblings.findIndex((s) => s.value === b.entityID); 626 let nextSelectedBlock = siblings[index + 1]; 627 if (!nextSelectedBlock) return; 628 useUIState.getState().addBlockToSelection({ 629 ...nextSelectedBlock, 630 }); 631 632 scrollIntoViewIfNeeded( 633 document.getElementById( 634 elementId.block(nextSelectedBlock.value).container, 635 ), 636 false, 637 ); 638 useUIState.getState().setFocusedBlock({ 639 entityType: "block", 640 parent: nextSelectedBlock.parent, 641 entityID: nextSelectedBlock.value, 642 }); 643 } else { 644 let nextBlock = sortedSelection[1]; 645 useUIState 646 .getState() 647 .removeBlockFromSelection({ value: b.entityID }); 648 scrollIntoViewIfNeeded( 649 document.getElementById( 650 elementId.block(nextBlock.value).container, 651 ), 652 false, 653 ); 654 useUIState.getState().setFocusedBlock({ 655 entityType: "block", 656 parent: b.parent, 657 entityID: nextBlock.value, 658 }); 659 if (sortedSelection.length === 2) { 660 useEditorStates 661 .getState() 662 .editorStates[nextBlock.value]?.view?.focus(); 663 } 664 } 665 } 666 } 667 if ((e.key === "c" || e.key === "x") && (e.metaKey || e.ctrlKey)) { 668 if (!rep) return; 669 if (e.shiftKey || (e.metaKey && e.ctrlKey)) return; 670 let [, , selectionWithFoldedChildren] = 671 await getSortedSelectionBound(); 672 if (!selectionWithFoldedChildren) return; 673 let el = document.activeElement as HTMLElement; 674 if ( 675 el?.tagName === "LABEL" || 676 el?.tagName === "INPUT" || 677 el?.tagName === "TEXTAREA" 678 ) { 679 return; 680 } 681 682 if ( 683 el.contentEditable === "true" && 684 selectionWithFoldedChildren.length <= 1 685 ) 686 return; 687 e.preventDefault(); 688 await copySelection(rep, selectionWithFoldedChildren); 689 if (e.key === "x") deleteBlocks(); 690 } 691 }); 692 window.addEventListener("keydown", listener); 693 return () => { 694 removeListener(); 695 window.removeEventListener("keydown", listener); 696 }; 697 }, [moreThanOneSelected, rep, entity_set.permissions.write]); 698 699 let [mouseDown, setMouseDown] = useState(false); 700 let initialContentEditableParent = useRef<null | Node>(null); 701 let savedSelection = useRef<SavedRange[] | null>(undefined); 702 useEffect(() => { 703 if (isMobile) return; 704 if (!entity_set.permissions.write) return; 705 let mouseDownListener = (e: MouseEvent) => { 706 if ((e.target as Element).getAttribute("data-draggable")) return; 707 let contentEditableParent = getContentEditableParent(e.target as Node); 708 if (contentEditableParent) { 709 setMouseDown(true); 710 let entityID = (contentEditableParent as Element).getAttribute( 711 "data-entityid", 712 ); 713 useSelectingMouse.setState({ start: entityID }); 714 } 715 initialContentEditableParent.current = contentEditableParent; 716 }; 717 let mouseUpListener = (e: MouseEvent) => { 718 savedSelection.current = null; 719 if ( 720 initialContentEditableParent.current && 721 !(e.target as Element).getAttribute("data-draggable") && 722 getContentEditableParent(e.target as Node) !== 723 initialContentEditableParent.current 724 ) { 725 setTimeout(() => { 726 window.getSelection()?.removeAllRanges(); 727 }, 5); 728 } 729 initialContentEditableParent.current = null; 730 useSelectingMouse.setState({ start: null }); 731 setMouseDown(false); 732 }; 733 window.addEventListener("mousedown", mouseDownListener); 734 window.addEventListener("mouseup", mouseUpListener); 735 return () => { 736 window.removeEventListener("mousedown", mouseDownListener); 737 window.removeEventListener("mouseup", mouseUpListener); 738 }; 739 }, [entity_set.permissions.write, isMobile]); 740 useEffect(() => { 741 if (!mouseDown) return; 742 if (isMobile) return; 743 let mouseMoveListener = (e: MouseEvent) => { 744 if (e.buttons !== 1) return; 745 if (initialContentEditableParent.current) { 746 if ( 747 initialContentEditableParent.current === 748 getContentEditableParent(e.target as Node) 749 ) { 750 if (savedSelection.current) { 751 restoreSelection(savedSelection.current); 752 } 753 savedSelection.current = null; 754 return; 755 } 756 if (!savedSelection.current) savedSelection.current = saveSelection(); 757 window.getSelection()?.removeAllRanges(); 758 } 759 }; 760 window.addEventListener("mousemove", mouseMoveListener); 761 return () => { 762 window.removeEventListener("mousemove", mouseMoveListener); 763 }; 764 }, [mouseDown, isMobile]); 765 return null; 766} 767 768type SavedRange = { 769 startContainer: Node; 770 startOffset: number; 771 endContainer: Node; 772 endOffset: number; 773 direction: "forward" | "backward"; 774}; 775function saveSelection() { 776 let selection = window.getSelection(); 777 if (selection && selection.rangeCount > 0) { 778 let ranges: SavedRange[] = []; 779 for (let i = 0; i < selection.rangeCount; i++) { 780 let range = selection.getRangeAt(i); 781 ranges.push({ 782 startContainer: range.startContainer, 783 startOffset: range.startOffset, 784 endContainer: range.endContainer, 785 endOffset: range.endOffset, 786 direction: 787 selection.anchorNode === range.startContainer && 788 selection.anchorOffset === range.startOffset 789 ? "forward" 790 : "backward", 791 }); 792 } 793 return ranges; 794 } 795 return []; 796} 797 798function restoreSelection(savedRanges: SavedRange[]) { 799 if (savedRanges && savedRanges.length > 0) { 800 let selection = window.getSelection(); 801 if (!selection) return; 802 selection.removeAllRanges(); 803 for (let i = 0; i < savedRanges.length; i++) { 804 let range = document.createRange(); 805 range.setStart(savedRanges[i].startContainer, savedRanges[i].startOffset); 806 range.setEnd(savedRanges[i].endContainer, savedRanges[i].endOffset); 807 808 selection.addRange(range); 809 810 // If the direction is backward, collapse the selection to the end and then extend it backward 811 if (savedRanges[i].direction === "backward") { 812 selection.collapseToEnd(); 813 selection.extend( 814 savedRanges[i].startContainer, 815 savedRanges[i].startOffset, 816 ); 817 } 818 } 819 } 820} 821 822function getContentEditableParent(e: Node | null): Node | null { 823 let element: Node | null = e; 824 while (element && element !== document) { 825 if ( 826 (element as HTMLElement).contentEditable === "true" || 827 (element as HTMLElement).getAttribute("data-editable-block") 828 ) { 829 return element; 830 } 831 element = element.parentNode; 832 } 833 return null; 834} 835 836function toggleMarkInBlocks(blocks: string[], mark: MarkType, attrs?: any) { 837 let everyBlockHasMark = blocks.reduce((acc, block) => { 838 let editor = useEditorStates.getState().editorStates[block]; 839 if (!editor) return acc; 840 let { view } = editor; 841 let from = 0; 842 let to = view.state.doc.content.size; 843 let hasMarkInRange = view.state.doc.rangeHasMark(from, to, mark); 844 return acc && hasMarkInRange; 845 }, true); 846 for (let block of blocks) { 847 let editor = useEditorStates.getState().editorStates[block]; 848 if (!editor) return; 849 let { view } = editor; 850 let tr = view.state.tr; 851 852 let from = 0; 853 let to = view.state.doc.content.size; 854 855 tr.setMeta("bulkOp", true); 856 if (everyBlockHasMark) { 857 tr.removeMark(from, to, mark); 858 } else { 859 tr.addMark(from, to, mark.create(attrs)); 860 } 861 862 view.dispatch(tr); 863 } 864}