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