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