a tool for shared writing and social publishing
304
fork

Configure Feed

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

at d30f8e8d8a4d68d80b09c6b2a6787f74bcb99b4e 221 lines 7.2 kB view raw
1import { useEffect, useRef, useState } from "react"; 2import * as Popover from "@radix-ui/react-popover"; 3import { blockCommands } from "./BlockCommands"; 4import { useReplicache } from "src/replicache"; 5import { useEntitySetContext } from "components/EntitySetProvider"; 6import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; 7import { UndoManager } from "src/undoManager"; 8import { useLeafletPublicationData } from "components/PageSWRDataProvider"; 9import { setEditorState, useEditorStates } from "src/state/useEditorState"; 10 11type Props = { 12 parent: string; 13 entityID: string | null; 14 position: string | null; 15 nextPosition: string | null; 16 factID?: string | undefined; 17 first?: boolean; 18 className?: string; 19}; 20 21export const BlockCommandBar = ({ 22 props, 23 searchValue, 24}: { 25 props: Props; 26 searchValue: string; 27}) => { 28 let ref = useRef<HTMLDivElement>(null); 29 30 let [highlighted, setHighlighted] = useState<string | undefined>(undefined); 31 32 let { rep, undoManager } = useReplicache(); 33 let entity_set = useEntitySetContext(); 34 let { data: pub } = useLeafletPublicationData(); 35 36 // This clears '/' AND anything typed after it 37 const clearCommandSearchText = () => { 38 if (!props.entityID) return; 39 const entityID = props.entityID; 40 41 const existingState = useEditorStates.getState().editorStates[entityID]; 42 if (!existingState) return; 43 44 const tr = existingState.editor.tr; 45 tr.deleteRange(1, tr.doc.content.size - 1); 46 setEditorState(entityID, { editor: existingState.editor.apply(tr) }); 47 }; 48 49 let commandResults = blockCommands.filter((command) => { 50 const lowerSearchValue = searchValue.toLocaleLowerCase(); 51 const matchesName = command.name 52 .toLocaleLowerCase() 53 .includes(lowerSearchValue); 54 const matchesAlternate = command.alternateNames?.some((altName) => 55 altName.toLocaleLowerCase().includes(lowerSearchValue) 56 ) ?? false; 57 const matchesSearch = matchesName || matchesAlternate; 58 const isVisible = !pub || !command.hiddenInPublication; 59 return matchesSearch && isVisible; 60 }); 61 62 useEffect(() => { 63 if ( 64 !highlighted || 65 !commandResults.find((result) => result.name === highlighted) 66 ) 67 setHighlighted(commandResults[0]?.name); 68 if (commandResults.length === 1) { 69 setHighlighted(commandResults[0].name); 70 } 71 }, [commandResults, setHighlighted, highlighted]); 72 useEffect(() => { 73 let listener = async (e: KeyboardEvent) => { 74 let reverseDir = ref.current?.dataset.side === "top"; 75 let currentHighlightIndex = commandResults.findIndex( 76 (command: { name: string }) => 77 highlighted && command.name === highlighted, 78 ); 79 80 if (reverseDir ? e.key === "ArrowUp" : e.key === "ArrowDown") { 81 setHighlighted( 82 commandResults[ 83 currentHighlightIndex === commandResults.length - 1 || 84 currentHighlightIndex === undefined 85 ? 0 86 : currentHighlightIndex + 1 87 ].name, 88 ); 89 return; 90 } 91 if (reverseDir ? e.key === "ArrowDown" : e.key === "ArrowUp") { 92 setHighlighted( 93 commandResults[ 94 currentHighlightIndex === 0 || 95 currentHighlightIndex === undefined || 96 currentHighlightIndex === -1 97 ? commandResults.length - 1 98 : currentHighlightIndex - 1 99 ].name, 100 ); 101 return; 102 } 103 104 // on enter, select the highlighted item 105 if (e.key === "Enter") { 106 undoManager.startGroup(); 107 e.preventDefault(); 108 rep && 109 (await commandResults[currentHighlightIndex]?.onSelect( 110 rep, 111 { 112 ...props, 113 entity_set: entity_set.set, 114 }, 115 undoManager, 116 )); 117 undoManager.endGroup(); 118 return; 119 } 120 }; 121 window.addEventListener("keydown", listener); 122 123 return () => window.removeEventListener("keydown", listener); 124 }, [highlighted, setHighlighted, commandResults, rep, entity_set.set, props]); 125 126 return ( 127 <Popover.Root 128 open 129 onOpenChange={(open) => { 130 if (!open) { 131 clearCommandSearchText(); 132 } 133 }} 134 > 135 <Popover.Trigger className="absolute left-0"></Popover.Trigger> 136 <Popover.Portal> 137 <Popover.Content 138 align="start" 139 sideOffset={16} 140 collisionPadding={16} 141 ref={ref} 142 onOpenAutoFocus={(e) => e.preventDefault()} 143 className={` 144 commandMenuContent group/cmd-menu 145 z-20 w-[264px] 146 flex data-[side=top]:items-end items-start 147 `} 148 > 149 <NestedCardThemeProvider> 150 <div className="commandMenuResults w-full max-h-(--radix-popover-content-available-height) overflow-auto flex flex-col group-data-[side=top]/cmd-menu:flex-col-reverse bg-bg-page py-1 gap-0.5 border border-border rounded-md shadow-md"> 151 {commandResults.length === 0 ? ( 152 <div className="w-full text-tertiary text-center italic py-2 px-2 "> 153 No blocks found 154 </div> 155 ) : ( 156 commandResults.map((result, index) => ( 157 <div key={index} className="contents"> 158 <CommandResult 159 name={result.name} 160 icon={result.icon} 161 onSelect={() => { 162 rep && 163 result.onSelect( 164 rep, 165 { 166 ...props, 167 entity_set: entity_set.set, 168 }, 169 undoManager, 170 ); 171 }} 172 highlighted={highlighted} 173 setHighlighted={(highlighted) => 174 setHighlighted(highlighted) 175 } 176 /> 177 {commandResults[index + 1] && 178 result.type !== commandResults[index + 1].type && ( 179 <hr className="mx-2 my-0.5 border-border" /> 180 )} 181 </div> 182 )) 183 )} 184 </div> 185 </NestedCardThemeProvider> 186 </Popover.Content> 187 </Popover.Portal> 188 </Popover.Root> 189 ); 190}; 191 192const CommandResult = (props: { 193 name: string; 194 icon: React.ReactNode; 195 onSelect: () => void; 196 highlighted: string | undefined; 197 setHighlighted: (state: string | undefined) => void; 198}) => { 199 let isHighlighted = props.highlighted === props.name; 200 201 return ( 202 <button 203 className={`commandResult text-left flex gap-2 mx-1 pr-2 py-0.5 rounded-md text-secondary ${isHighlighted && "bg-border-light"}`} 204 onMouseOver={() => { 205 props.setHighlighted(props.name); 206 }} 207 onMouseDown={(e) => { 208 e.preventDefault(); 209 props.onSelect(); 210 }} 211 > 212 <div className="text-tertiary w-8 shrink-0 flex justify-center"> 213 {props.icon} 214 </div> 215 {props.name} 216 </button> 217 ); 218}; 219function usePublicationContext() { 220 throw new Error("Function not implemented."); 221}