a tool for shared writing and social publishing
1"use client";
2
3import React, { useEffect, useState } from "react";
4import { TextBlockTypeToolbar } from "./TextBlockTypeToolbar";
5import { InlineLinkToolbar } from "./InlineLinkToolbar";
6import { useEditorStates } from "src/state/useEditorState";
7import { useUIState } from "src/useUIState";
8import { useEntity, useReplicache } from "src/replicache";
9import * as Tooltip from "@radix-ui/react-tooltip";
10import { addShortcut } from "src/shortcuts";
11import { ListToolbar } from "./ListToolbar";
12import { HighlightToolbar } from "./HighlightToolbar";
13import { TextToolbar } from "./TextToolbar";
14import { BlockToolbar } from "./BlockToolbar";
15import { MultiselectToolbar } from "./MultiSelectToolbar";
16import { AreYouSure, deleteBlock } from "components/Blocks/DeleteBlock";
17import { TooltipButton } from "components/Buttons";
18import { TextAlignmentToolbar } from "./TextAlignmentToolbar";
19import { useIsMobile } from "src/hooks/isMobile";
20import { CloseTiny } from "components/Icons/CloseTiny";
21
22export type ToolbarTypes =
23 | "areYouSure"
24 | "default"
25 | "block"
26 | "multiselect"
27 | "highlight"
28 | "link"
29 | "heading"
30 | "text-alignment"
31 | "list"
32 | "linkBlock"
33 | "img-alt-text";
34
35export const Toolbar = (props: { pageID: string; blockID: string }) => {
36 let { rep } = useReplicache();
37
38 let [toolbarState, setToolbarState] = useState<ToolbarTypes>("default");
39
40 let focusedEntity = useUIState((s) => s.focusedEntity);
41 let selectedBlocks = useUIState((s) => s.selectedBlocks);
42 let activeEditor = useEditorStates((s) => s.editorStates[props.blockID]);
43
44 let blockType = useEntity(props.blockID, "block/type")?.data.value;
45
46 let lastUsedHighlight = useUIState((s) => s.lastUsedHighlight);
47 let setLastUsedHighlight = (color: "1" | "2" | "3") =>
48 useUIState.setState({
49 lastUsedHighlight: color,
50 });
51
52 useEffect(() => {
53 if (toolbarState !== "default") return;
54 let removeShortcut = addShortcut({
55 metaKey: true,
56 key: "k",
57 handler: () => {
58 setToolbarState("link");
59 },
60 });
61 return () => {
62 removeShortcut();
63 };
64 }, [toolbarState]);
65
66 useEffect(() => {
67 if (!blockType) return;
68 if (
69 blockType !== "heading" &&
70 blockType !== "text" &&
71 blockType !== "blockquote"
72 ) {
73 setToolbarState("block");
74 } else {
75 setToolbarState("default");
76 }
77 }, [blockType]);
78
79 useEffect(() => {
80 if (
81 selectedBlocks.length > 1 &&
82 !["areYousure", "text-alignment"].includes(toolbarState)
83 ) {
84 setToolbarState("multiselect");
85 } else if (toolbarState === "multiselect") {
86 setToolbarState("default");
87 }
88 }, [selectedBlocks.length, toolbarState]);
89 let isMobile = useIsMobile();
90
91 return (
92 <Tooltip.Provider>
93 <div
94 className={`toolbar flex gap-2 items-center justify-between w-full
95 ${isMobile ? "h-[calc(15px+var(--safe-padding-bottom))]" : "h-[26px]"}`}
96 >
97 <div className="toolbarOptions flex gap-1 sm:gap-[6px] items-center grow">
98 {toolbarState === "default" ? (
99 <TextToolbar
100 lastUsedHighlight={lastUsedHighlight}
101 setToolbarState={(s) => {
102 setToolbarState(s);
103 }}
104 />
105 ) : toolbarState === "highlight" ? (
106 <HighlightToolbar
107 pageID={props.pageID}
108 onClose={() => setToolbarState("default")}
109 lastUsedHighlight={lastUsedHighlight}
110 setLastUsedHighlight={(color: "1" | "2" | "3") =>
111 setLastUsedHighlight(color)
112 }
113 />
114 ) : toolbarState === "list" ? (
115 <ListToolbar onClose={() => setToolbarState("default")} />
116 ) : toolbarState === "link" ? (
117 <InlineLinkToolbar
118 onClose={() => {
119 activeEditor?.view?.focus();
120 setToolbarState("default");
121 }}
122 />
123 ) : toolbarState === "heading" ? (
124 <TextBlockTypeToolbar onClose={() => setToolbarState("default")} />
125 ) : toolbarState === "text-alignment" ? (
126 <TextAlignmentToolbar />
127 ) : toolbarState === "block" ? (
128 <BlockToolbar setToolbarState={setToolbarState} />
129 ) : toolbarState === "multiselect" ? (
130 <MultiselectToolbar setToolbarState={setToolbarState} />
131 ) : toolbarState === "areYouSure" ? (
132 <AreYouSure
133 compact
134 type={blockType}
135 entityID={selectedBlocks.map((b) => b.value)}
136 onClick={() => {
137 rep &&
138 deleteBlock(
139 selectedBlocks.map((b) => b.value),
140 rep,
141 );
142 }}
143 closeAreYouSure={() => {
144 setToolbarState(
145 selectedBlocks.length > 1
146 ? "multiselect"
147 : blockType !== "heading" && blockType !== "text"
148 ? "block"
149 : "default",
150 );
151 }}
152 />
153 ) : null}
154 </div>
155 {/* if the thing is are you sure state, don't show the x... is each thing handling its own are you sure? theres no need for that */}
156 {toolbarState !== "areYouSure" && (
157 <button
158 className="toolbarBackToDefault hover:text-accent-contrast"
159 onMouseDown={(e) => {
160 e.preventDefault();
161 if (
162 toolbarState === "multiselect" ||
163 toolbarState === "block" ||
164 toolbarState === "default"
165 ) {
166 useUIState.setState(() => ({
167 focusedEntity: {
168 entityType: "page",
169 entityID: props.pageID,
170 },
171 selectedBlocks: [],
172 }));
173 } else {
174 if (blockType !== "heading" && blockType !== "text") {
175 setToolbarState("block");
176 } else {
177 setToolbarState("default");
178 }
179 }
180 }}
181 >
182 <CloseTiny />
183 </button>
184 )}
185 </div>
186 </Tooltip.Provider>
187 );
188};
189
190export const ToolbarButton = (props: {
191 className?: string;
192 onClick?: (e: React.MouseEvent) => void;
193 tooltipContent: React.ReactNode;
194 children: React.ReactNode;
195 active?: boolean;
196 disabled?: boolean;
197 hiddenOnCanvas?: boolean;
198}) => {
199 let focusedEntity = useUIState((s) => s.focusedEntity);
200 let isLocked = useEntity(focusedEntity?.entityID || null, "block/is-locked");
201 let isDisabled =
202 props.disabled === undefined ? !!isLocked?.data.value : props.disabled;
203
204 let focusedEntityType = useEntity(
205 focusedEntity?.entityType === "page"
206 ? focusedEntity.entityID
207 : focusedEntity?.parent || null,
208 "page/type",
209 );
210 if (focusedEntityType?.data.value === "canvas" && props.hiddenOnCanvas)
211 return;
212 return (
213 <TooltipButton
214 onMouseDown={(e) => {
215 e.preventDefault();
216 props.onClick && props.onClick(e);
217 }}
218 disabled={isDisabled}
219 tooltipContent={props.tooltipContent}
220 className={`
221 flex items-center rounded-md border border-transparent
222 ${props.className}
223 ${
224 props.active && !isDisabled
225 ? "bg-border-light text-primary"
226 : isDisabled
227 ? "text-border cursor-not-allowed"
228 : "text-secondary hover:text-primary hover:border-border active:bg-border-light active:text-primary"
229 }
230 `}
231 >
232 {props.children}
233 </TooltipButton>
234 );
235};