a tool for shared writing and social publishing
0
fork

Configure Feed

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

add more undo grouping

+219 -159
+20 -15
components/Blocks/BlockCommandBar.tsx
··· 4 4 import { useReplicache } from "src/replicache"; 5 5 import { useEntitySetContext } from "components/EntitySetProvider"; 6 6 import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; 7 + import { UndoManager } from "src/undoManager"; 7 8 8 9 type Props = { 9 10 parent: string; ··· 26 27 27 28 let [highlighted, setHighlighted] = useState<string | undefined>(undefined); 28 29 29 - let { rep } = useReplicache(); 30 + let { rep, undoManager } = useReplicache(); 30 31 let entity_set = useEntitySetContext(); 31 32 32 33 let commandResults = blockCommands.filter((command) => ··· 44 45 }, [commandResults, setHighlighted, highlighted]); 45 46 useEffect(() => { 46 47 let listener = async (e: KeyboardEvent) => { 47 - let input = document.getElementById("block-search"); 48 48 let reverseDir = ref.current?.dataset.side === "top"; 49 49 let currentHighlightIndex = commandResults.findIndex( 50 50 (command: { name: string }) => ··· 77 77 78 78 // on enter, select the highlighted item 79 79 if (e.key === "Enter") { 80 + undoManager.startGroup(); 80 81 e.preventDefault(); 81 82 rep && 82 - commandResults[currentHighlightIndex]?.onSelect(rep, { 83 - ...props, 84 - entity_set: entity_set.set, 85 - }); 83 + (await commandResults[currentHighlightIndex]?.onSelect( 84 + rep, 85 + { 86 + ...props, 87 + entity_set: entity_set.set, 88 + }, 89 + undoManager, 90 + )); 91 + undoManager.endGroup(); 86 92 return; 87 93 } 88 94 89 95 // radix menu component handles esc 90 96 if (e.key === "Escape") return; 91 - 92 - // any keypress that is not up down, left right, enter, esc, space focuses the search 93 - if (input) { 94 - input.focus(); 95 - } 96 97 }; 97 98 window.addEventListener("keydown", listener); 98 99 ··· 130 131 icon={result.icon} 131 132 onSelect={() => { 132 133 rep && 133 - result.onSelect(rep, { 134 - ...props, 135 - entity_set: entity_set.set, 136 - }); 134 + result.onSelect( 135 + rep, 136 + { 137 + ...props, 138 + entity_set: entity_set.set, 139 + }, 140 + undoManager, 141 + ); 137 142 }} 138 143 highlighted={highlighted} 139 144 setHighlighted={(highlighted) =>
+41 -3
components/Blocks/BlockCommands.tsx
··· 22 22 import { keepFocus } from "components/Toolbar/TextBlockTypeToolbar"; 23 23 import { useEditorStates } from "src/state/useEditorState"; 24 24 import { elementId } from "src/utils/elementId"; 25 + import { UndoManager } from "src/undoManager"; 26 + import { focusBlock } from "src/utils/focusBlock"; 25 27 26 28 type Props = { 27 29 parent: string; ··· 95 97 onSelect: ( 96 98 rep: Replicache<ReplicacheMutators>, 97 99 props: Props & { entity_set: string }, 98 - ) => void; 100 + undoManager: UndoManager, 101 + ) => Promise<any>; 99 102 }; 100 103 export const blockCommands: Command[] = [ 101 104 // please keep these in the order that they appear in the menu, grouped by type ··· 226 229 name: "New Page", 227 230 icon: <BlockDocPageSmall />, 228 231 type: "page", 229 - onSelect: async (rep, props) => { 232 + onSelect: async (rep, props, um) => { 230 233 let entity = await createBlockWithType(rep, props, "card"); 231 234 232 235 let newPage = v7(); ··· 238 241 type: "doc", 239 242 permission_set: props.entity_set, 240 243 }); 244 + 241 245 useUIState.getState().openPage(props.parent, newPage); 246 + um.add({ 247 + undo: () => { 248 + useUIState.getState().closePage(newPage); 249 + setTimeout( 250 + () => 251 + focusBlock( 252 + { parent: props.parent, value: entity, type: "text" }, 253 + { type: "end" }, 254 + ), 255 + 100, 256 + ); 257 + }, 258 + redo: () => { 259 + useUIState.getState().openPage(props.parent, newPage); 260 + focusPage(newPage, rep, "focusFirstBlock"); 261 + }, 262 + }); 242 263 focusPage(newPage, rep, "focusFirstBlock"); 243 264 }, 244 265 }, ··· 246 267 name: "New Canvas", 247 268 icon: <BlockCanvasPageSmall />, 248 269 type: "page", 249 - onSelect: async (rep, props) => { 270 + onSelect: async (rep, props, um) => { 250 271 let entity = await createBlockWithType(rep, props, "card"); 251 272 252 273 let newPage = v7(); ··· 260 281 }); 261 282 useUIState.getState().openPage(props.parent, newPage); 262 283 focusPage(newPage, rep, "focusFirstBlock"); 284 + um.add({ 285 + undo: () => { 286 + useUIState.getState().closePage(newPage); 287 + setTimeout( 288 + () => 289 + focusBlock( 290 + { parent: props.parent, value: entity, type: "text" }, 291 + { type: "end" }, 292 + ), 293 + 100, 294 + ); 295 + }, 296 + redo: () => { 297 + useUIState.getState().openPage(props.parent, newPage); 298 + focusPage(newPage, rep, "focusFirstBlock"); 299 + }, 300 + }); 263 301 }, 264 302 }, 265 303 ];
+3 -1
components/Blocks/useBlockKeyboardHandlers.ts
··· 21 21 areYouSure: boolean, 22 22 setAreYouSure: (value: boolean) => void, 23 23 ) { 24 - let { rep } = useReplicache(); 24 + let { rep, undoManager } = useReplicache(); 25 25 let entity_set = useEntitySetContext(); 26 26 let isLocked = !!useEntity(props.entityID, "block/is-locked")?.data.value; 27 27 ··· 54 54 if ((el as HTMLInputElement).value !== "" || e.key === "Tab") return; 55 55 } 56 56 57 + undoManager.startGroup(); 57 58 command?.({ 58 59 e, 59 60 props, ··· 63 64 setAreYouSure, 64 65 isLocked, 65 66 }); 67 + undoManager.endGroup(); 66 68 }; 67 69 window.addEventListener("keydown", listener); 68 70 return () => window.removeEventListener("keydown", listener);
+147 -140
components/SelectionManager.tsx
··· 27 27 export function SelectionManager() { 28 28 let moreThanOneSelected = useUIState((s) => s.selectedBlocks.length > 1); 29 29 let entity_set = useEntitySetContext(); 30 - let { rep } = useReplicache(); 30 + let { rep, undoManager } = useReplicache(); 31 31 let isMobile = useIsMobile(); 32 32 useEffect(() => { 33 33 if (!entity_set.permissions.write) return; ··· 68 68 sortedBlocksWithChildren, 69 69 ]; 70 70 }; 71 - let removeListener = addShortcut([ 72 - { 73 - metaKey: true, 74 - key: "ArrowUp", 75 - handler: async () => { 76 - let [firstBlock] = 77 - (await rep?.query((tx) => 78 - getBlocksWithType( 79 - tx, 80 - useUIState.getState().selectedBlocks[0].parent, 81 - ), 82 - )) || []; 83 - if (firstBlock) focusBlock(firstBlock, { type: "start" }); 71 + let removeListener = addShortcut( 72 + [ 73 + { 74 + metaKey: true, 75 + key: "ArrowUp", 76 + handler: async () => { 77 + let [firstBlock] = 78 + (await rep?.query((tx) => 79 + getBlocksWithType( 80 + tx, 81 + useUIState.getState().selectedBlocks[0].parent, 82 + ), 83 + )) || []; 84 + if (firstBlock) focusBlock(firstBlock, { type: "start" }); 85 + }, 84 86 }, 85 - }, 86 - { 87 - metaKey: true, 88 - key: "ArrowDown", 89 - handler: async () => { 90 - let blocks = 91 - (await rep?.query((tx) => 92 - getBlocksWithType( 93 - tx, 94 - useUIState.getState().selectedBlocks[0].parent, 95 - ), 96 - )) || []; 97 - let folded = useUIState.getState().foldedBlocks; 98 - blocks = blocks.filter( 99 - (f) => 100 - !f.listData || 101 - !f.listData.path.find( 102 - (path) => 103 - folded.includes(path.entity) && f.value !== path.entity, 104 - ), 105 - ); 106 - let lastBlock = blocks[blocks.length - 1]; 107 - if (lastBlock) focusBlock(lastBlock, { type: "end" }); 87 + { 88 + metaKey: true, 89 + key: "ArrowDown", 90 + handler: async () => { 91 + let blocks = 92 + (await rep?.query((tx) => 93 + getBlocksWithType( 94 + tx, 95 + useUIState.getState().selectedBlocks[0].parent, 96 + ), 97 + )) || []; 98 + let folded = useUIState.getState().foldedBlocks; 99 + blocks = blocks.filter( 100 + (f) => 101 + !f.listData || 102 + !f.listData.path.find( 103 + (path) => 104 + folded.includes(path.entity) && f.value !== path.entity, 105 + ), 106 + ); 107 + let lastBlock = blocks[blocks.length - 1]; 108 + if (lastBlock) focusBlock(lastBlock, { type: "end" }); 109 + }, 108 110 }, 109 - }, 110 - { 111 - metaKey: true, 112 - altKey: true, 113 - key: ["l", "¬"], 114 - handler: async () => { 115 - let [sortedBlocks, siblings] = await getSortedSelection(); 116 - for (let block of sortedBlocks) { 117 - if (!block.listData) { 118 - await rep?.mutate.assertFact({ 119 - entity: block.value, 120 - attribute: "block/is-list", 121 - data: { type: "boolean", value: true }, 111 + { 112 + metaKey: true, 113 + altKey: true, 114 + key: ["l", "¬"], 115 + handler: async () => { 116 + let [sortedBlocks, siblings] = await getSortedSelection(); 117 + for (let block of sortedBlocks) { 118 + if (!block.listData) { 119 + await rep?.mutate.assertFact({ 120 + entity: block.value, 121 + attribute: "block/is-list", 122 + data: { type: "boolean", value: true }, 123 + }); 124 + } else { 125 + outdentFull(block, rep); 126 + } 127 + } 128 + }, 129 + }, 130 + { 131 + metaKey: true, 132 + shift: true, 133 + key: ["ArrowDown"], 134 + handler: async () => { 135 + let [sortedBlocks, siblings] = await getSortedSelection(); 136 + let block = sortedBlocks[0]; 137 + let nextBlock = siblings 138 + .slice(siblings.findIndex((s) => s.value === block.value) + 1) 139 + .find( 140 + (f) => 141 + f.listData && 142 + block.listData && 143 + !f.listData.path.find((f) => f.entity === block.value), 144 + ); 145 + if ( 146 + nextBlock?.listData && 147 + block.listData && 148 + nextBlock.listData.depth === block.listData.depth - 1 149 + ) { 150 + if (useUIState.getState().foldedBlocks.includes(nextBlock.value)) 151 + useUIState.getState().toggleFold(nextBlock.value); 152 + rep?.mutate.moveBlock({ 153 + block: block.value, 154 + oldParent: block.listData?.parent, 155 + newParent: nextBlock.value, 156 + position: { type: "first" }, 122 157 }); 123 158 } else { 124 - outdentFull(block, rep); 159 + rep?.mutate.moveBlockDown({ 160 + entityID: block.value, 161 + parent: block.listData?.parent || block.parent, 162 + }); 125 163 } 126 - } 164 + }, 127 165 }, 128 - }, 129 - { 130 - metaKey: true, 131 - shift: true, 132 - key: ["ArrowDown"], 133 - handler: async () => { 134 - let [sortedBlocks, siblings] = await getSortedSelection(); 135 - let block = sortedBlocks[0]; 136 - let nextBlock = siblings 137 - .slice(siblings.findIndex((s) => s.value === block.value) + 1) 138 - .find( 139 - (f) => 140 - f.listData && 141 - block.listData && 142 - !f.listData.path.find((f) => f.entity === block.value), 143 - ); 144 - if ( 145 - nextBlock?.listData && 146 - block.listData && 147 - nextBlock.listData.depth === block.listData.depth - 1 148 - ) { 149 - if (useUIState.getState().foldedBlocks.includes(nextBlock.value)) 150 - useUIState.getState().toggleFold(nextBlock.value); 151 - rep?.mutate.moveBlock({ 152 - block: block.value, 153 - oldParent: block.listData?.parent, 154 - newParent: nextBlock.value, 155 - position: { type: "first" }, 156 - }); 157 - } else { 158 - rep?.mutate.moveBlockDown({ 159 - entityID: block.value, 160 - parent: block.listData?.parent || block.parent, 161 - }); 162 - } 163 - }, 164 - }, 165 - { 166 - metaKey: true, 167 - shift: true, 168 - key: ["ArrowUp"], 169 - handler: async () => { 170 - let [sortedBlocks, siblings] = await getSortedSelection(); 171 - let block = sortedBlocks[0]; 172 - let previousBlock = 173 - siblings?.[siblings.findIndex((s) => s.value === block.value) - 1]; 174 - if (previousBlock.value === block.listData?.parent) { 175 - previousBlock = 166 + { 167 + metaKey: true, 168 + shift: true, 169 + key: ["ArrowUp"], 170 + handler: async () => { 171 + let [sortedBlocks, siblings] = await getSortedSelection(); 172 + let block = sortedBlocks[0]; 173 + let previousBlock = 176 174 siblings?.[ 177 - siblings.findIndex((s) => s.value === block.value) - 2 175 + siblings.findIndex((s) => s.value === block.value) - 1 178 176 ]; 179 - } 177 + if (previousBlock.value === block.listData?.parent) { 178 + previousBlock = 179 + siblings?.[ 180 + siblings.findIndex((s) => s.value === block.value) - 2 181 + ]; 182 + } 180 183 181 - if ( 182 - previousBlock?.listData && 183 - block.listData && 184 - block.listData.depth > 1 && 185 - !previousBlock.listData.path.find( 186 - (f) => f.entity === block.listData?.parent, 187 - ) 188 - ) { 189 - let depth = block.listData.depth; 190 - let newParent = previousBlock.listData.path.find( 191 - (f) => f.depth === depth - 1, 192 - ); 193 - if (!newParent) return; 194 - if (useUIState.getState().foldedBlocks.includes(newParent.entity)) 195 - useUIState.getState().toggleFold(newParent.entity); 196 - rep?.mutate.moveBlock({ 197 - block: block.value, 198 - oldParent: block.listData?.parent, 199 - newParent: newParent.entity, 200 - position: { type: "end" }, 201 - }); 202 - } else { 203 - rep?.mutate.moveBlockUp({ 204 - entityID: block.value, 205 - parent: block.listData?.parent || block.parent, 206 - }); 207 - } 184 + if ( 185 + previousBlock?.listData && 186 + block.listData && 187 + block.listData.depth > 1 && 188 + !previousBlock.listData.path.find( 189 + (f) => f.entity === block.listData?.parent, 190 + ) 191 + ) { 192 + let depth = block.listData.depth; 193 + let newParent = previousBlock.listData.path.find( 194 + (f) => f.depth === depth - 1, 195 + ); 196 + if (!newParent) return; 197 + if (useUIState.getState().foldedBlocks.includes(newParent.entity)) 198 + useUIState.getState().toggleFold(newParent.entity); 199 + rep?.mutate.moveBlock({ 200 + block: block.value, 201 + oldParent: block.listData?.parent, 202 + newParent: newParent.entity, 203 + position: { type: "end" }, 204 + }); 205 + } else { 206 + rep?.mutate.moveBlockUp({ 207 + entityID: block.value, 208 + parent: block.listData?.parent || block.parent, 209 + }); 210 + } 211 + }, 208 212 }, 209 - }, 210 - { 211 - metaKey: true, 212 - shift: true, 213 - key: "Enter", 214 - handler: async () => { 215 - let [sortedBlocks, siblings] = await getSortedSelection(); 216 - if (!sortedBlocks[0].listData) return; 217 - useUIState.getState().toggleFold(sortedBlocks[0].value); 213 + { 214 + metaKey: true, 215 + shift: true, 216 + key: "Enter", 217 + handler: async () => { 218 + let [sortedBlocks, siblings] = await getSortedSelection(); 219 + if (!sortedBlocks[0].listData) return; 220 + useUIState.getState().toggleFold(sortedBlocks[0].value); 221 + }, 218 222 }, 219 - }, 220 - ]); 223 + ].map((shortcut) => ({ 224 + ...shortcut, 225 + handler: () => undoManager.withUndoGroup(() => shortcut.handler()), 226 + })), 227 + ); 221 228 let listener = async (e: KeyboardEvent) => { 222 229 if (e.key === "Backspace" || e.key === "Delete") { 223 230 if (!entity_set.permissions.write) return;
+8
src/replicache/index.tsx
··· 84 84 { 85 85 metaKey: true, 86 86 shift: true, 87 + key: "z", 88 + handler: () => { 89 + undoManager.redo(); 90 + }, 91 + }, 92 + { 93 + metaKey: true, 94 + shift: true, 87 95 key: "Z", 88 96 handler: () => { 89 97 undoManager.redo();