a tool for shared writing and social publishing

add bulk block text decoration shortcuts

+88 -7
+9 -7
components/Blocks/TextBlock/index.tsx
··· 257 257 let oldEditorState = this.state; 258 258 let newState = this.state.apply(tr); 259 259 let addToHistory = tr.getMeta("addToHistory"); 260 + let isBulkOp = tr.getMeta("bulkOp"); 260 261 let docHasChanges = tr.steps.length !== 0 || tr.docChanged; 261 262 if (addToHistory !== false && docHasChanges) { 262 263 if (actionTimeout.current) { 263 264 window.clearTimeout(actionTimeout.current); 264 265 } else { 265 - rep.undoManager.startGroup(); 266 + if (!isBulkOp) rep.undoManager.startGroup(); 266 267 } 267 268 268 - actionTimeout.current = window.setTimeout(() => { 269 - rep.undoManager.endGroup(); 270 - actionTimeout.current = null; 271 - }, 200); 269 + if (!isBulkOp) 270 + actionTimeout.current = window.setTimeout(() => { 271 + rep.undoManager.endGroup(); 272 + actionTimeout.current = null; 273 + }, 200); 272 274 rep.undoManager.add({ 273 275 redo: () => { 274 276 useEditorStates.setState((oldState) => { 275 277 let view = oldState.editorStates[props.entityID]?.view; 276 - if (!view?.hasFocus()) view?.focus(); 278 + if (!view?.hasFocus() && !isBulkOp) view?.focus(); 277 279 return { 278 280 editorStates: { 279 281 ...oldState.editorStates, ··· 288 290 undo: () => { 289 291 useEditorStates.setState((oldState) => { 290 292 let view = oldState.editorStates[props.entityID]?.view; 291 - if (!view?.hasFocus()) view?.focus(); 293 + if (!view?.hasFocus() && !isBulkOp) view?.focus(); 292 294 return { 293 295 editorStates: { 294 296 ...oldState.editorStates,
+79
components/SelectionManager.tsx
··· 19 19 import { useIsMobile } from "src/hooks/isMobile"; 20 20 import { deleteBlock } from "./Blocks/DeleteBlock"; 21 21 import { Replicache } from "replicache"; 22 + import { schema } from "./Blocks/TextBlock/schema"; 23 + import { TextSelection } from "prosemirror-state"; 24 + import { MarkType } from "prosemirror-model"; 22 25 export const useSelectingMouse = create(() => ({ 23 26 start: null as null | string, 24 27 })); ··· 179 182 }, 180 183 { 181 184 metaKey: true, 185 + key: "u", 186 + handler: async () => { 187 + let [sortedBlocks] = await getSortedSelectionBound(); 188 + toggleMarkInBlocks( 189 + schema.marks.underline, 190 + sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 191 + ); 192 + }, 193 + }, 194 + { 195 + metaKey: true, 196 + key: "i", 197 + handler: async () => { 198 + let [sortedBlocks] = await getSortedSelectionBound(); 199 + toggleMarkInBlocks( 200 + schema.marks.em, 201 + sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 202 + ); 203 + }, 204 + }, 205 + { 206 + metaKey: true, 207 + key: "b", 208 + handler: async () => { 209 + let [sortedBlocks] = await getSortedSelectionBound(); 210 + toggleMarkInBlocks( 211 + schema.marks.strong, 212 + sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 213 + ); 214 + }, 215 + }, 216 + { 217 + metaKey: true, 218 + shift: true, 219 + key: "X", 220 + handler: async () => { 221 + let [sortedBlocks] = await getSortedSelectionBound(); 222 + toggleMarkInBlocks( 223 + schema.marks.strikethrough, 224 + sortedBlocks.filter((b) => b.type === "text").map((b) => b.value), 225 + ); 226 + }, 227 + }, 228 + { 229 + metaKey: true, 182 230 shift: true, 183 231 key: "Enter", 184 232 handler: async () => { ··· 463 511 } 464 512 if ((e.key === "c" || e.key === "x") && (e.metaKey || e.ctrlKey)) { 465 513 if (!rep) return; 514 + if (e.shiftKey) return; 466 515 let [, , selectionWithFoldedChildren] = 467 516 await getSortedSelectionBound(); 468 517 if (!selectionWithFoldedChildren) return; ··· 667 716 sortedBlocksWithChildren, 668 717 ]; 669 718 }; 719 + 720 + function toggleMarkInBlocks(mark: MarkType, blocks: string[]) { 721 + let everyBlockHasMark = blocks.reduce((acc, block) => { 722 + let editor = useEditorStates.getState().editorStates[block]; 723 + if (!editor) return acc; 724 + let { view } = editor; 725 + let from = 0; 726 + let to = view.state.doc.content.size; 727 + let hasMarkInRange = view.state.doc.rangeHasMark(from, to, mark); 728 + return acc && hasMarkInRange; 729 + }, true); 730 + for (let block of blocks) { 731 + let editor = useEditorStates.getState().editorStates[block]; 732 + if (!editor) return; 733 + let { view } = editor; 734 + let tr = view.state.tr; 735 + 736 + let from = 0; 737 + let to = view.state.doc.content.size; 738 + 739 + tr.setMeta("bulkOp", true); 740 + if (everyBlockHasMark) { 741 + tr.removeMark(from, to, mark); 742 + } else { 743 + tr.addMark(from, to, mark.create()); 744 + } 745 + 746 + view.dispatch(tr); 747 + } 748 + }