a tool for shared writing and social publishing
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}