Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
6.5 kB · 206 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207"use client";
import React, { useEffect, useState } from "react";import { TextBlockTypeToolbar } from "./TextBlockTypeToolbar";import { InlineLinkToolbar } from "./InlineLinkToolbar";import { useEditorStates } from "src/state/useEditorState";import { useUIState } from "src/useUIState";import { useEntity, useReplicache } from "src/replicache";import * as Tooltip from "@radix-ui/react-tooltip";import { addShortcut } from "src/shortcuts";import { ListToolbar } from "./ListToolbar";import { HighlightToolbar } from "./HighlightToolbar";import { TextToolbar } from "./TextToolbar";import { ImageToolbar } from "./ImageToolbar";import { MultiselectToolbar } from "./MultiSelectToolbar";import { TooltipButton } from "components/Buttons";import { TextAlignmentToolbar } from "./TextAlignmentToolbar";import { useIsMobile } from "src/hooks/isMobile";import { CloseTiny } from "components/Icons/CloseTiny";
export type ToolbarTypes = | "default" | "multiselect" | "highlight" | "link" | "heading" | "text-alignment" | "list" | "linkBlock" | "img-alt-text" | "image";
export const Toolbar = (props: { pageID: string; blockID: string; blockType: string | null | undefined;}) => { let [toolbarState, setToolbarState] = useState<ToolbarTypes>("default");
let activeEditor = useEditorStates((s) => s.editorStates[props.blockID]); let selectedBlocks = useUIState((s) => s.selectedBlocks);
let lastUsedHighlight = useUIState((s) => s.lastUsedHighlight); let setLastUsedHighlight = (color: "1" | "2" | "3") => useUIState.setState({ lastUsedHighlight: color, });
useEffect(() => { if (toolbarState !== "default") return; let removeShortcut = addShortcut({ metaKey: true, key: "k", handler: () => { setToolbarState("link"); }, }); return () => { removeShortcut(); }; }, [toolbarState]);
let isTextBlock = props.blockType === "heading" || props.blockType === "text" || props.blockType === "blockquote";
useEffect(() => { if (selectedBlocks.length > 1) { setToolbarState("multiselect"); return; } if (isTextBlock) { setToolbarState("default"); } if (props.blockType === "image") { setToolbarState("image"); } if (props.blockType === "button" || props.blockType === "datetime") { setToolbarState("text-alignment"); } else null; }, [props.blockType, selectedBlocks]);
let isMobile = useIsMobile(); return ( <Tooltip.Provider> <div className={`toolbar flex gap-2 items-center justify-between w-full ${isMobile ? "h-[calc(15px+var(--safe-padding-bottom))]" : "h-[26px]"}`} > <div className="toolbarOptions flex gap-1 sm:gap-[6px] items-center grow"> {toolbarState === "default" ? ( <TextToolbar lastUsedHighlight={lastUsedHighlight} setToolbarState={(s) => { setToolbarState(s); }} /> ) : toolbarState === "highlight" ? ( <HighlightToolbar pageID={props.pageID} onClose={() => setToolbarState("default")} lastUsedHighlight={lastUsedHighlight} setLastUsedHighlight={(color: "1" | "2" | "3") => setLastUsedHighlight(color) } /> ) : toolbarState === "list" ? ( <ListToolbar onClose={() => setToolbarState("default")} /> ) : toolbarState === "link" ? ( <InlineLinkToolbar onClose={() => { activeEditor?.view?.focus(); setToolbarState("default"); }} /> ) : toolbarState === "heading" ? ( <TextBlockTypeToolbar onClose={() => setToolbarState("default")} /> ) : toolbarState === "text-alignment" ? ( <TextAlignmentToolbar /> ) : toolbarState === "image" ? ( <ImageToolbar setToolbarState={setToolbarState} /> ) : toolbarState === "multiselect" ? ( <MultiselectToolbar setToolbarState={setToolbarState} /> ) : null} </div> {/* 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 */}
<button className="toolbarBackToDefault hover:text-accent-contrast" onMouseDown={(e) => { e.preventDefault(); if ( toolbarState === "multiselect" || toolbarState === "image" || toolbarState === "default" ) { // close the toolbar useUIState.setState(() => ({ focusedEntity: { entityType: "page", entityID: props.pageID, }, selectedBlocks: [], })); } else { if (props.blockType === "image") { setToolbarState("image"); } if (isTextBlock) { setToolbarState("default"); } } }} > <CloseTiny /> </button> </div> </Tooltip.Provider> );};
export const ToolbarButton = (props: { className?: string; onClick?: (e: React.MouseEvent) => void; tooltipContent: React.ReactNode; children: React.ReactNode; active?: boolean; disabled?: boolean; hiddenOnCanvas?: boolean;}) => { let focusedEntity = useUIState((s) => s.focusedEntity); let isDisabled = props.disabled;
let focusedEntityType = useEntity( focusedEntity?.entityType === "page" ? focusedEntity.entityID : focusedEntity?.parent || null, "page/type", ); if (focusedEntityType?.data.value === "canvas" && props.hiddenOnCanvas) return; return ( <TooltipButton onMouseDown={(e) => { e.preventDefault(); props.onClick && props.onClick(e); }} disabled={isDisabled} tooltipContent={props.tooltipContent} className={` flex items-center rounded-md border border-transparent ${props.className} ${ props.active && !isDisabled ? "bg-border-light text-primary" : isDisabled ? "text-border cursor-not-allowed" : "text-secondary hover:text-primary hover:border-border active:bg-border-light active:text-primary" } `} > {props.children} </TooltipButton> );};