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