a tool for shared writing and social publishing
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}