a tool for shared writing and social publishing
0
fork

Configure Feed

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

pull out handle paste

+192 -169
+3 -169
components/TextBlock/index.tsx
··· 38 38 import { setMark } from "src/utils/prosemirror/setMark"; 39 39 import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 40 40 import { useEntitySetContext } from "components/EntitySetProvider"; 41 + import { useHandlePaste } from "./useHandlePaste"; 41 42 42 43 export function TextBlock(props: BlockProps & { className: string }) { 43 44 let initialized = useInitialPageLoad(); ··· 178 179 })); 179 180 }; 180 181 }, [props.entityID]); 182 + let handlePaste = useHandlePaste(props.entityID, propsRef, factID); 181 183 if (!editorState) return null; 182 184 183 185 return ( ··· 192 194 window.open(mark.attrs.href, "_blank"); 193 195 } 194 196 }} 195 - handlePaste={(view, e) => { 196 - if (!rep.rep) return; 197 - if (!e.clipboardData) return; 198 - let textHTML = e.clipboardData.getData("text/html"); 199 - let editorState = 200 - useEditorStates.getState().editorStates[props.entityID]; 201 - if (!editorState) return; 202 - if (textHTML) { 203 - const parser = ProsemirrorDOMParser.fromSchema(schema); 204 - let xml = new DOMParser().parseFromString(textHTML, "text/html"); 205 - let currentPosition = propsRef.current.position; 206 - let children = [...xml.body.children]; 207 - if ( 208 - !children.find((c) => ["P", "H1", "H2", "H3"].includes(c.tagName)) 209 - ) 210 - return; 211 - children.forEach((child, index) => { 212 - let content = parser.parse(child); 213 - let type: Fact<"block/type">["data"]["value"] | null; 214 - let headingLevel: number | null = null; 215 - switch (child.tagName) { 216 - case "SPAN": { 217 - type = "text"; 218 - break; 219 - } 220 - case "P": { 221 - type = "text"; 222 - break; 223 - } 224 - case "H1": { 225 - headingLevel = 1; 226 - type = "heading"; 227 - break; 228 - } 229 - case "H2": { 230 - headingLevel = 2; 231 - type = "heading"; 232 - break; 233 - } 234 - case "H3": { 235 - headingLevel = 3; 236 - type = "heading"; 237 - break; 238 - } 239 - default: 240 - type = null; 241 - } 242 - if (!type) return; 243 - 244 - let entityID: string; 245 - if (index === 0 && type === props.type) entityID = props.entityID; 246 - else { 247 - entityID = crypto.randomUUID(); 248 - currentPosition = generateKeyBetween( 249 - currentPosition, 250 - propsRef.current.nextPosition, 251 - ); 252 - repRef.current?.mutate.addBlock({ 253 - permission_set: entity_set.set, 254 - newEntityID: entityID, 255 - parent: propsRef.current.parent, 256 - type: type, 257 - position: currentPosition, 258 - }); 259 - if (type === "heading" && headingLevel) { 260 - repRef.current?.mutate.assertFact({ 261 - entity: entityID, 262 - attribute: "block/heading-level", 263 - data: { type: "number", value: headingLevel }, 264 - }); 265 - } 266 - } 267 - let p = currentPosition; 268 - 269 - setTimeout(() => { 270 - let block = useEditorStates.getState().editorStates[entityID]; 271 - if (block) { 272 - let tr = block.editor.tr; 273 - tr.insert(block.editor.selection.from || 0, content.content); 274 - let newState = block.editor.apply(tr); 275 - setEditorState(entityID, { 276 - editor: newState, 277 - }); 278 - } 279 - if (index === children.length - 1) { 280 - focusBlock( 281 - { 282 - value: entityID, 283 - type: type, 284 - parent: propsRef.current.parent, 285 - position: p, 286 - }, 287 - { type: "end" }, 288 - ); 289 - } 290 - }, 10); 291 - }); 292 - return true; 293 - } else { 294 - let text = e.clipboardData.getData("text"); 295 - let paragraphs = text 296 - .split("\n") 297 - .slice(1) 298 - .filter((f) => !!f); 299 - let currentPosition = propsRef.current.position; 300 - for (let p of paragraphs) { 301 - let newEntityID = crypto.randomUUID(); 302 - currentPosition = generateKeyBetween( 303 - currentPosition, 304 - propsRef.current.nextPosition, 305 - ); 306 - repRef.current?.mutate.addBlock({ 307 - permission_set: entity_set.set, 308 - newEntityID, 309 - parent: propsRef.current.parent, 310 - type: "text", 311 - position: currentPosition, 312 - }); 313 - setTimeout(() => { 314 - let block = useEditorStates.getState().editorStates[newEntityID]; 315 - if (block) { 316 - let tr = block.editor.tr; 317 - tr.insertText(p, 1); 318 - let newState = block.editor.apply(tr); 319 - setEditorState(newEntityID, { 320 - editor: newState, 321 - }); 322 - } 323 - }, 10); 324 - } 325 - } 326 - 327 - for (let item of e.clipboardData.items) { 328 - if (item?.type.includes("image")) { 329 - let file = item.getAsFile(); 330 - if (file) { 331 - let entity: string; 332 - if (editorState.editor.doc.textContent.length === 0) { 333 - entity = props.entityID; 334 - rep.rep.mutate.assertFact({ 335 - entity: props.entityID, 336 - attribute: "block/type", 337 - data: { type: "block-type-union", value: "image" }, 338 - }); 339 - if (factID) rep.rep.mutate.retractFact({ factID: factID }); 340 - } else { 341 - entity = crypto.randomUUID(); 342 - rep.rep.mutate.addBlock({ 343 - permission_set: entity_set.set, 344 - type: "image", 345 - newEntityID: entity, 346 - parent: props.parent, 347 - position: generateKeyBetween( 348 - props.position, 349 - props.nextPosition, 350 - ), 351 - }); 352 - } 353 - addImage(file, rep.rep, { 354 - attribute: "block/image", 355 - entityID: entity, 356 - }); 357 - } 358 - return; 359 - } 360 - } 361 - e.preventDefault(); 362 - e.stopPropagation(); 363 - }} 197 + handlePaste={handlePaste} 364 198 mount={mount} 365 199 state={editorState} 366 200 dispatchTransaction={(tr) => {
+189
components/TextBlock/useHandlePaste.ts
··· 1 + import { MutableRefObject, useCallback } from "react"; 2 + import { Fact, useReplicache } from "src/replicache"; 3 + import { EditorView } from "prosemirror-view"; 4 + import { setEditorState, useEditorStates } from "src/state/useEditorState"; 5 + import { MarkType, DOMParser as ProsemirrorDOMParser } from "prosemirror-model"; 6 + import { schema } from "./schema"; 7 + import { generateKeyBetween } from "fractional-indexing"; 8 + import { addImage } from "src/utils/addImage"; 9 + import { BlockProps, focusBlock } from "components/Blocks"; 10 + import { useEntitySetContext } from "components/EntitySetProvider"; 11 + 12 + export const useHandlePaste = ( 13 + entityID: string, 14 + propsRef: MutableRefObject<BlockProps>, 15 + factID?: string, 16 + ) => { 17 + let { rep } = useReplicache(); 18 + let entity_set = useEntitySetContext(); 19 + return useCallback( 20 + (view: EditorView, e: ClipboardEvent) => { 21 + if (!rep) return; 22 + if (!e.clipboardData) return; 23 + let textHTML = e.clipboardData.getData("text/html"); 24 + let editorState = useEditorStates.getState().editorStates[entityID]; 25 + if (!editorState) return; 26 + if (textHTML) { 27 + const parser = ProsemirrorDOMParser.fromSchema(schema); 28 + let xml = new DOMParser().parseFromString(textHTML, "text/html"); 29 + let currentPosition = propsRef.current.position; 30 + let children = [...xml.body.children]; 31 + if (!children.find((c) => ["P", "H1", "H2", "H3"].includes(c.tagName))) 32 + return; 33 + children.forEach((child, index) => { 34 + let content = parser.parse(child); 35 + let type: Fact<"block/type">["data"]["value"] | null; 36 + let headingLevel: number | null = null; 37 + switch (child.tagName) { 38 + case "SPAN": { 39 + type = "text"; 40 + break; 41 + } 42 + case "P": { 43 + type = "text"; 44 + break; 45 + } 46 + case "H1": { 47 + headingLevel = 1; 48 + type = "heading"; 49 + break; 50 + } 51 + case "H2": { 52 + headingLevel = 2; 53 + type = "heading"; 54 + break; 55 + } 56 + case "H3": { 57 + headingLevel = 3; 58 + type = "heading"; 59 + break; 60 + } 61 + default: 62 + type = null; 63 + } 64 + if (!type) return; 65 + 66 + let entityID: string; 67 + if (index === 0 && type === propsRef.current.type) 68 + entityID = propsRef.current.entityID; 69 + else { 70 + entityID = crypto.randomUUID(); 71 + currentPosition = generateKeyBetween( 72 + currentPosition, 73 + propsRef.current.nextPosition, 74 + ); 75 + rep.mutate.addBlock({ 76 + permission_set: entity_set.set, 77 + newEntityID: entityID, 78 + parent: propsRef.current.parent, 79 + type: type, 80 + position: currentPosition, 81 + }); 82 + if (type === "heading" && headingLevel) { 83 + rep.mutate.assertFact({ 84 + entity: entityID, 85 + attribute: "block/heading-level", 86 + data: { type: "number", value: headingLevel }, 87 + }); 88 + } 89 + } 90 + let p = currentPosition; 91 + 92 + setTimeout(() => { 93 + let block = useEditorStates.getState().editorStates[entityID]; 94 + if (block) { 95 + let tr = block.editor.tr; 96 + tr.insert(block.editor.selection.from || 0, content.content); 97 + let newState = block.editor.apply(tr); 98 + setEditorState(entityID, { 99 + editor: newState, 100 + }); 101 + } 102 + if (index === children.length - 1) { 103 + focusBlock( 104 + { 105 + value: entityID, 106 + type: type, 107 + parent: propsRef.current.parent, 108 + position: p, 109 + }, 110 + { type: "end" }, 111 + ); 112 + } 113 + }, 10); 114 + }); 115 + return true; 116 + } else { 117 + let text = e.clipboardData.getData("text"); 118 + let paragraphs = text 119 + .split("\n") 120 + .slice(1) 121 + .filter((f) => !!f); 122 + let currentPosition = propsRef.current.position; 123 + for (let p of paragraphs) { 124 + let newEntityID = crypto.randomUUID(); 125 + currentPosition = generateKeyBetween( 126 + currentPosition, 127 + propsRef.current.nextPosition, 128 + ); 129 + rep.mutate.addBlock({ 130 + permission_set: entity_set.set, 131 + newEntityID, 132 + parent: propsRef.current.parent, 133 + type: "text", 134 + position: currentPosition, 135 + }); 136 + setTimeout(() => { 137 + let block = useEditorStates.getState().editorStates[newEntityID]; 138 + if (block) { 139 + let tr = block.editor.tr; 140 + tr.insertText(p, 1); 141 + let newState = block.editor.apply(tr); 142 + setEditorState(newEntityID, { 143 + editor: newState, 144 + }); 145 + } 146 + }, 10); 147 + } 148 + } 149 + 150 + for (let item of e.clipboardData.items) { 151 + if (item?.type.includes("image")) { 152 + let file = item.getAsFile(); 153 + if (file) { 154 + let entity: string; 155 + if (editorState.editor.doc.textContent.length === 0) { 156 + entity = propsRef.current.entityID; 157 + rep.mutate.assertFact({ 158 + entity: propsRef.current.entityID, 159 + attribute: "block/type", 160 + data: { type: "block-type-union", value: "image" }, 161 + }); 162 + if (factID) rep.mutate.retractFact({ factID: factID }); 163 + } else { 164 + entity = crypto.randomUUID(); 165 + rep.mutate.addBlock({ 166 + permission_set: entity_set.set, 167 + type: "image", 168 + newEntityID: entity, 169 + parent: propsRef.current.parent, 170 + position: generateKeyBetween( 171 + propsRef.current.position, 172 + propsRef.current.nextPosition, 173 + ), 174 + }); 175 + } 176 + addImage(file, rep, { 177 + attribute: "block/image", 178 + entityID: entity, 179 + }); 180 + } 181 + return; 182 + } 183 + } 184 + e.preventDefault(); 185 + e.stopPropagation(); 186 + }, 187 + [rep, entity_set, entityID, propsRef, factID], 188 + ); 189 + };