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 c666be3a5500aee0bcd61a53332c7608dbc209ad 198 lines 6.4 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"; 9 10type Props = { 11 parent: string; 12 entityID: string | null; 13 position: string | null; 14 nextPosition: string | null; 15 factID?: string | undefined; 16 first?: boolean; 17 className?: string; 18}; 19 20export const BlockCommandBar = ({ 21 props, 22 searchValue, 23}: { 24 props: Props; 25 searchValue: string; 26}) => { 27 let ref = useRef<HTMLDivElement>(null); 28 29 let [highlighted, setHighlighted] = useState<string | undefined>(undefined); 30 31 let { rep, undoManager } = useReplicache(); 32 let entity_set = useEntitySetContext(); 33 let { data: pub } = useLeafletPublicationData(); 34 35 let commandResults = blockCommands.filter((command) => { 36 const matchesSearch = command.name 37 .toLocaleLowerCase() 38 .includes(searchValue.toLocaleLowerCase()); 39 const isVisible = !pub || !command.hiddenInPublication; 40 return matchesSearch && isVisible; 41 }); 42 43 useEffect(() => { 44 if ( 45 !highlighted || 46 !commandResults.find((result) => result.name === highlighted) 47 ) 48 setHighlighted(commandResults[0]?.name); 49 if (commandResults.length === 1) { 50 setHighlighted(commandResults[0].name); 51 } 52 }, [commandResults, setHighlighted, highlighted]); 53 useEffect(() => { 54 let listener = async (e: KeyboardEvent) => { 55 let reverseDir = ref.current?.dataset.side === "top"; 56 let currentHighlightIndex = commandResults.findIndex( 57 (command: { name: string }) => 58 highlighted && command.name === highlighted, 59 ); 60 61 if (reverseDir ? e.key === "ArrowUp" : e.key === "ArrowDown") { 62 setHighlighted( 63 commandResults[ 64 currentHighlightIndex === commandResults.length - 1 || 65 currentHighlightIndex === undefined 66 ? 0 67 : currentHighlightIndex + 1 68 ].name, 69 ); 70 return; 71 } 72 if (reverseDir ? e.key === "ArrowDown" : e.key === "ArrowUp") { 73 setHighlighted( 74 commandResults[ 75 currentHighlightIndex === 0 || 76 currentHighlightIndex === undefined || 77 currentHighlightIndex === -1 78 ? commandResults.length - 1 79 : currentHighlightIndex - 1 80 ].name, 81 ); 82 return; 83 } 84 85 // on enter, select the highlighted item 86 if (e.key === "Enter") { 87 undoManager.startGroup(); 88 e.preventDefault(); 89 rep && 90 (await commandResults[currentHighlightIndex]?.onSelect( 91 rep, 92 { 93 ...props, 94 entity_set: entity_set.set, 95 }, 96 undoManager, 97 )); 98 undoManager.endGroup(); 99 return; 100 } 101 102 // radix menu component handles esc 103 if (e.key === "Escape") return; 104 }; 105 window.addEventListener("keydown", listener); 106 107 return () => window.removeEventListener("keydown", listener); 108 }, [highlighted, setHighlighted, commandResults, rep, entity_set.set, props]); 109 110 return ( 111 <Popover.Root open> 112 <Popover.Trigger className="absolute left-0"></Popover.Trigger> 113 <Popover.Portal> 114 <Popover.Content 115 align="start" 116 sideOffset={16} 117 collisionPadding={16} 118 ref={ref} 119 onOpenAutoFocus={(e) => e.preventDefault()} 120 className={` 121 commandMenuContent group/cmd-menu 122 z-20 w-[264px] 123 flex data-[side=top]:items-end items-start 124 `} 125 > 126 <NestedCardThemeProvider> 127 <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"> 128 {commandResults.length === 0 ? ( 129 <div className="w-full text-tertiary text-center italic py-2 px-2 "> 130 No blocks found 131 </div> 132 ) : ( 133 commandResults.map((result, index) => ( 134 <div key={index} className="contents"> 135 <CommandResult 136 name={result.name} 137 icon={result.icon} 138 onSelect={() => { 139 rep && 140 result.onSelect( 141 rep, 142 { 143 ...props, 144 entity_set: entity_set.set, 145 }, 146 undoManager, 147 ); 148 }} 149 highlighted={highlighted} 150 setHighlighted={(highlighted) => 151 setHighlighted(highlighted) 152 } 153 /> 154 {commandResults[index + 1] && 155 result.type !== commandResults[index + 1].type && ( 156 <hr className="mx-2 my-0.5 border-border" /> 157 )} 158 </div> 159 )) 160 )} 161 </div> 162 </NestedCardThemeProvider> 163 </Popover.Content> 164 </Popover.Portal> 165 </Popover.Root> 166 ); 167}; 168 169const CommandResult = (props: { 170 name: string; 171 icon: React.ReactNode; 172 onSelect: () => void; 173 highlighted: string | undefined; 174 setHighlighted: (state: string | undefined) => void; 175}) => { 176 let isHighlighted = props.highlighted === props.name; 177 178 return ( 179 <button 180 className={`commandResult text-left flex gap-2 mx-1 pr-2 py-0.5 rounded-md text-secondary ${isHighlighted && "bg-border-light"}`} 181 onMouseOver={() => { 182 props.setHighlighted(props.name); 183 }} 184 onMouseDown={(e) => { 185 e.preventDefault(); 186 props.onSelect(); 187 }} 188 > 189 <div className="text-tertiary w-8 shrink-0 flex justify-center"> 190 {props.icon} 191 </div> 192 {props.name} 193 </button> 194 ); 195}; 196function usePublicationContext() { 197 throw new Error("Function not implemented."); 198}