a tool for shared writing and social publishing
at fix/bottom-scroll-margin-on-docs 235 lines 7.7 kB view raw
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};