a tool for shared writing and social publishing
at feature/backdate 236 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 } 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};