a tool for shared writing and social publishing
298
fork

Configure Feed

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

at feature/email 178 lines 5.6 kB view raw
1import { schema } from "components/Blocks/TextBlock/schema"; 2import { TextSelection } from "prosemirror-state"; 3import { useUIState } from "src/useUIState"; 4import { ToolbarButton } from "."; 5import { useEffect, useState } from "react"; 6import { Separator } from "components/Layout"; 7import { setEditorState, useEditorStates } from "src/state/useEditorState"; 8import { rangeHasMark } from "src/utils/prosemirror/rangeHasMark"; 9import { findMarkRange } from "src/utils/prosemirror/findMarkRange"; 10import { ensureProtocol } from "src/utils/ensureProtocol"; 11import { Input } from "components/Input"; 12import { useReplicache } from "src/replicache"; 13import { CheckTiny } from "components/Icons/CheckTiny"; 14import { LinkSmall } from "components/Icons/LinkSmall"; 15 16export function LinkButton(props: { setToolbarState: (s: "link") => void }) { 17 let focusedBlock = useUIState((s) => s.focusedEntity); 18 let focusedEditor = useEditorStates((s) => 19 focusedBlock ? s.editorStates[focusedBlock.entityID] : null, 20 ); 21 let isLink; 22 if (focusedEditor) { 23 let { to, from, $cursor } = focusedEditor.editor.selection as TextSelection; 24 if ($cursor) isLink = !!schema.marks.link.isInSet($cursor.marks()); 25 if (to !== from) 26 isLink = !!rangeHasMark( 27 focusedEditor.editor, 28 schema.marks.link, 29 from, 30 to, 31 ); 32 } 33 34 return ( 35 <ToolbarButton 36 active={isLink} 37 onClick={(e) => { 38 e.preventDefault(); 39 props.setToolbarState("link"); 40 }} 41 disabled={ 42 !focusedEditor || (focusedEditor?.editor.selection.empty && !isLink) 43 } 44 tooltipContent={ 45 <div className="text-accent-contrast underline">Inline Link</div> 46 } 47 > 48 <LinkSmall /> 49 </ToolbarButton> 50 ); 51} 52 53export function InlineLinkToolbar(props: { onClose: () => void }) { 54 let focusedBlock = useUIState((s) => s.focusedEntity); 55 let focusedEditor = useEditorStates((s) => 56 focusedBlock ? s.editorStates[focusedBlock.entityID] : null, 57 ); 58 let { undoManager } = useReplicache(); 59 useEffect(() => { 60 if (focusedEditor) { 61 let isLink; 62 let { to, from, $cursor } = focusedEditor.editor 63 .selection as TextSelection; 64 if ($cursor) isLink = !!schema.marks.link.isInSet($cursor.marks()); 65 if (to !== from) 66 isLink = !!rangeHasMark( 67 focusedEditor.editor, 68 schema.marks.link, 69 from, 70 to, 71 ); 72 if (isLink) return; 73 } 74 if (focusedEditor?.editor.selection.empty) props.onClose(); 75 }, [focusedEditor, props]); 76 let content = ""; 77 let start: number | null = null; 78 let end: number | null = null; 79 if (focusedEditor) { 80 let { to, from, $cursor } = focusedEditor.editor.selection as TextSelection; 81 if (to !== from) { 82 start = from; 83 end = to; 84 } else { 85 let markRange = findMarkRange( 86 focusedEditor.editor.doc, 87 schema.marks.link, 88 from, 89 ); 90 start = markRange.start; 91 end = markRange.end; 92 } 93 if ($cursor) { 94 let link = $cursor.marks().find((f) => f.type === schema.marks.link); 95 if (link) { 96 content = link.attrs.href; 97 } 98 } 99 } 100 let [linkValue, setLinkValue] = useState(content); 101 let setLink = () => { 102 let href = ensureProtocol(linkValue); 103 104 let editor = focusedEditor?.editor; 105 if (!editor || start === null || !end || !focusedBlock) return; 106 let tr = editor.tr; 107 tr.addMark(start, end, schema.marks.link.create({ href })); 108 tr.setSelection(TextSelection.create(tr.doc, tr.selection.to)); 109 110 let oldState = editor; 111 let newState = editor.apply(tr); 112 undoManager.add({ 113 undo: () => { 114 if (!focusedEditor?.view?.hasFocus()) focusedEditor?.view?.focus(); 115 setEditorState(focusedBlock.entityID, { 116 editor: oldState, 117 }); 118 }, 119 redo: () => { 120 if (!focusedEditor?.view?.hasFocus()) focusedEditor?.view?.focus(); 121 setEditorState(focusedBlock.entityID, { 122 editor: newState, 123 }); 124 }, 125 }); 126 setEditorState(focusedBlock?.entityID, { 127 editor: newState, 128 }); 129 props.onClose(); 130 }; 131 132 return ( 133 <div className="w-full flex items-center gap-[6px] grow"> 134 <LinkSmall /> 135 <Separator classname="h-6!" /> 136 <Input 137 autoFocus 138 className="w-full grow bg-transparent border-none outline-hidden " 139 placeholder="www.example.com" 140 value={linkValue} 141 onChange={(e) => setLinkValue(e.target.value)} 142 onKeyDown={(e) => { 143 if (e.key === "Enter") { 144 e.preventDefault(); 145 setLink(); 146 } 147 if (e.key === "Escape") { 148 props.onClose(); 149 } 150 }} 151 /> 152 {/* 153 TODO: 154 to avoid all sort of messiness, editing any portion of link will edit the 155 entire range that includes the link rather than just the link text that is selected. 156 157 ALSO TODO: 158 if there is already a link mark, the input should be prefilled with the link value 159 and the check mark should be a garbage can to remove the link. 160 161 if the user changes the link, then the button reverts to a check mark. 162 */} 163 <div className="flex items-center gap-3 w-4"> 164 <button 165 disabled={!linkValue || linkValue === ""} 166 className="hover:text-accent-contrast -mr-6 disabled:text-border" 167 onMouseDown={(e) => { 168 e.preventDefault(); 169 setLink(); 170 }} 171 > 172 <CheckTiny /> 173 </button> 174 </div> 175 </div> 176 ); 177} 178