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}