import { isTransparent } from "../../utils"; import type { ExcalidrawElement } from "../../element/types"; import type { AppState } from "../../types"; import { TopPicks } from "./TopPicks"; import { ButtonSeparator } from "../ButtonSeparator"; import { Picker } from "./Picker"; import * as Popover from "@radix-ui/react-popover"; import type { ColorPickerType } from "./colorPickerUtils"; import { activeColorPickerSectionAtom } from "./colorPickerUtils"; import { useExcalidrawContainer } from "../App"; import type { ColorTuple, ColorPaletteCustom } from "../../colors"; import { COLOR_PALETTE } from "../../colors"; import PickerHeading from "./PickerHeading"; import { t } from "../../i18n"; import clsx from "clsx"; import { useRef } from "react"; import { useAtom } from "../../editor-jotai"; import { ColorInput } from "./ColorInput"; import { activeEyeDropperAtom } from "../EyeDropper"; import { PropertiesPopover } from "../PropertiesPopover"; import "./ColorPicker.scss"; const isValidColor = (color: string) => { const style = new Option().style; style.color = color; return !!style.color; }; export const getColor = (color: string): string | null => { if (isTransparent(color)) { return color; } // testing for `#` first fixes a bug on Electron (more specfically, an // Obsidian popout window), where a hex color without `#` is (incorrectly) // considered valid return isValidColor(`#${color}`) ? `#${color}` : isValidColor(color) ? color : null; }; interface ColorPickerProps { type: ColorPickerType; color: string; onChange: (color: string) => void; label: string; elements: readonly ExcalidrawElement[]; appState: AppState; palette?: ColorPaletteCustom | null; topPicks?: ColorTuple; updateData: (formData?: any) => void; } const ColorPickerPopupContent = ({ type, color, onChange, label, elements, palette = COLOR_PALETTE, updateData, }: Pick< ColorPickerProps, | "type" | "color" | "onChange" | "label" | "elements" | "palette" | "updateData" >) => { const { container } = useExcalidrawContainer(); const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom); const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom); const colorInputJSX = (
{t("colorPicker.hexCode")} { onChange(color); }} colorPickerType={type} />
); const popoverRef = useRef(null); const focusPickerContent = () => { popoverRef.current ?.querySelector(".color-picker-content") ?.focus(); }; return ( { // refocus due to eye dropper focusPickerContent(); event.preventDefault(); }} onPointerDownOutside={(event) => { if (eyeDropperState) { // prevent from closing if we click outside the popover // while eyedropping (e.g. click when clicking the sidebar; // the eye-dropper-backdrop is prevented downstream) event.preventDefault(); } }} onClose={() => { updateData({ openPopup: null }); setActiveColorPickerSection(null); }} > {palette ? ( { onChange(changedColor); }} onEyeDropperToggle={(force) => { setEyeDropperState((state) => { if (force) { state = state || { keepOpenOnAlt: true, onSelect: onChange, colorPickerType: type, }; state.keepOpenOnAlt = true; return state; } return force === false || state ? null : { keepOpenOnAlt: false, onSelect: onChange, colorPickerType: type, }; }); }} onEscape={(event) => { if (eyeDropperState) { setEyeDropperState(null); } else { updateData({ openPopup: null }); } }} label={label} type={type} elements={elements} updateData={updateData} > {colorInputJSX} ) : ( colorInputJSX )} ); }; const ColorPickerTrigger = ({ label, color, type, }: { color: string; label: string; type: ColorPickerType; }) => { return (
); }; export const ColorPicker = ({ type, color, onChange, label, elements, palette = COLOR_PALETTE, topPicks, updateData, appState, }: ColorPickerProps) => { return (
{ updateData({ openPopup: open ? type : null }); }} > {/* serves as an active color indicator as well */} {/* popup content */} {appState.openPopup === type && ( )}
); };