mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 04:44:31 +01:00 
			
		
		
		
	fix: more eye-droper fixes (#7019)
This commit is contained in:
		@@ -1330,7 +1330,8 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
  private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
 | 
			
		||||
    jotaiStore.set(activeEyeDropperAtom, {
 | 
			
		||||
      swapPreviewOnAlt: true,
 | 
			
		||||
      previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
 | 
			
		||||
      colorPickerType:
 | 
			
		||||
        type === "stroke" ? "elementStroke" : "elementBackground",
 | 
			
		||||
      onSelect: (color, event) => {
 | 
			
		||||
        const shouldUpdateStrokeColor =
 | 
			
		||||
          (type === "background" && event.altKey) ||
 | 
			
		||||
@@ -1341,12 +1342,14 @@ class App extends React.Component<AppProps, AppState> {
 | 
			
		||||
          this.state.activeTool.type !== "selection"
 | 
			
		||||
        ) {
 | 
			
		||||
          if (shouldUpdateStrokeColor) {
 | 
			
		||||
            this.setState({
 | 
			
		||||
              currentItemStrokeColor: color,
 | 
			
		||||
            this.syncActionResult({
 | 
			
		||||
              appState: { ...this.state, currentItemStrokeColor: color },
 | 
			
		||||
              commitToHistory: true,
 | 
			
		||||
            });
 | 
			
		||||
          } else {
 | 
			
		||||
            this.setState({
 | 
			
		||||
              currentItemBackgroundColor: color,
 | 
			
		||||
            this.syncActionResult({
 | 
			
		||||
              appState: { ...this.state, currentItemBackgroundColor: color },
 | 
			
		||||
              commitToHistory: true,
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from "react";
 | 
			
		||||
import { getColor } from "./ColorPicker";
 | 
			
		||||
import { useAtom } from "jotai";
 | 
			
		||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
 | 
			
		||||
import {
 | 
			
		||||
  ColorPickerType,
 | 
			
		||||
  activeColorPickerSectionAtom,
 | 
			
		||||
} from "./colorPickerUtils";
 | 
			
		||||
import { eyeDropperIcon } from "../icons";
 | 
			
		||||
import { jotaiScope } from "../../jotai";
 | 
			
		||||
import { KEYS } from "../../keys";
 | 
			
		||||
@@ -15,14 +18,14 @@ interface ColorInputProps {
 | 
			
		||||
  color: string;
 | 
			
		||||
  onChange: (color: string) => void;
 | 
			
		||||
  label: string;
 | 
			
		||||
  eyeDropperType: "strokeColor" | "backgroundColor";
 | 
			
		||||
  colorPickerType: ColorPickerType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ColorInput = ({
 | 
			
		||||
  color,
 | 
			
		||||
  onChange,
 | 
			
		||||
  label,
 | 
			
		||||
  eyeDropperType,
 | 
			
		||||
  colorPickerType,
 | 
			
		||||
}: ColorInputProps) => {
 | 
			
		||||
  const device = useDevice();
 | 
			
		||||
  const [innerValue, setInnerValue] = useState(color);
 | 
			
		||||
@@ -116,7 +119,7 @@ export const ColorInput = ({
 | 
			
		||||
                  : {
 | 
			
		||||
                      keepOpenOnAlt: false,
 | 
			
		||||
                      onSelect: (color) => onChange(color),
 | 
			
		||||
                      previewType: eyeDropperType,
 | 
			
		||||
                      colorPickerType,
 | 
			
		||||
                    },
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
  const { container } = useExcalidrawContainer();
 | 
			
		||||
  const { isMobile, isLandscape } = useDevice();
 | 
			
		||||
 | 
			
		||||
  const eyeDropperType =
 | 
			
		||||
    type === "canvasBackground"
 | 
			
		||||
      ? undefined
 | 
			
		||||
      : type === "elementBackground"
 | 
			
		||||
      ? "backgroundColor"
 | 
			
		||||
      : "strokeColor";
 | 
			
		||||
 | 
			
		||||
  const colorInputJSX = eyeDropperType && (
 | 
			
		||||
  const colorInputJSX = (
 | 
			
		||||
    <div>
 | 
			
		||||
      <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
 | 
			
		||||
      <ColorInput
 | 
			
		||||
@@ -98,7 +91,7 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
        onChange={(color) => {
 | 
			
		||||
          onChange(color);
 | 
			
		||||
        }}
 | 
			
		||||
        eyeDropperType={eyeDropperType}
 | 
			
		||||
        colorPickerType={type}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
@@ -160,7 +153,7 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
            "0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {palette && eyeDropperType ? (
 | 
			
		||||
        {palette ? (
 | 
			
		||||
          <Picker
 | 
			
		||||
            palette={palette}
 | 
			
		||||
            color={color}
 | 
			
		||||
@@ -173,7 +166,7 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
                  state = state || {
 | 
			
		||||
                    keepOpenOnAlt: true,
 | 
			
		||||
                    onSelect: onChange,
 | 
			
		||||
                    previewType: eyeDropperType,
 | 
			
		||||
                    colorPickerType: type,
 | 
			
		||||
                  };
 | 
			
		||||
                  state.keepOpenOnAlt = true;
 | 
			
		||||
                  return state;
 | 
			
		||||
@@ -184,7 +177,7 @@ const ColorPickerPopupContent = ({
 | 
			
		||||
                  : {
 | 
			
		||||
                      keepOpenOnAlt: false,
 | 
			
		||||
                      onSelect: onChange,
 | 
			
		||||
                      previewType: eyeDropperType,
 | 
			
		||||
                      colorPickerType: type,
 | 
			
		||||
                    };
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,47 @@
 | 
			
		||||
import { atom } from "jotai";
 | 
			
		||||
import { useEffect, useRef } from "react";
 | 
			
		||||
import React, { useEffect, useRef } from "react";
 | 
			
		||||
import { createPortal } from "react-dom";
 | 
			
		||||
import { rgbToHex } from "../colors";
 | 
			
		||||
import { EVENT } from "../constants";
 | 
			
		||||
import { useUIAppState } from "../context/ui-appState";
 | 
			
		||||
import { mutateElement } from "../element/mutateElement";
 | 
			
		||||
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
 | 
			
		||||
import { useOutsideClick } from "../hooks/useOutsideClick";
 | 
			
		||||
import { KEYS } from "../keys";
 | 
			
		||||
import { getSelectedElements } from "../scene";
 | 
			
		||||
import Scene from "../scene/Scene";
 | 
			
		||||
import { ShapeCache } from "../scene/ShapeCache";
 | 
			
		||||
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
 | 
			
		||||
import { useStable } from "../hooks/useStable";
 | 
			
		||||
 | 
			
		||||
import "./EyeDropper.scss";
 | 
			
		||||
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
 | 
			
		||||
import { ExcalidrawElement } from "../element/types";
 | 
			
		||||
 | 
			
		||||
type EyeDropperProperties = {
 | 
			
		||||
export type EyeDropperProperties = {
 | 
			
		||||
  keepOpenOnAlt: boolean;
 | 
			
		||||
  swapPreviewOnAlt?: boolean;
 | 
			
		||||
  /** called when user picks color (on pointerup) */
 | 
			
		||||
  onSelect: (color: string, event: PointerEvent) => void;
 | 
			
		||||
  previewType: "strokeColor" | "backgroundColor";
 | 
			
		||||
  /**
 | 
			
		||||
   * property of selected elements to update live when alt-dragging.
 | 
			
		||||
   * Supply `null` if not applicable (e.g. updating the canvas bg instead of
 | 
			
		||||
   * elements)
 | 
			
		||||
   **/
 | 
			
		||||
  colorPickerType: ColorPickerType;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
 | 
			
		||||
 | 
			
		||||
export const EyeDropper: React.FC<{
 | 
			
		||||
  onCancel: () => void;
 | 
			
		||||
  onSelect: Required<EyeDropperProperties>["onSelect"];
 | 
			
		||||
  swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
 | 
			
		||||
  previewType: EyeDropperProperties["previewType"];
 | 
			
		||||
}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => {
 | 
			
		||||
  onSelect: EyeDropperProperties["onSelect"];
 | 
			
		||||
  /** called when color changes, on pointerdown for preview */
 | 
			
		||||
  onChange: (
 | 
			
		||||
    type: ColorPickerType,
 | 
			
		||||
    color: string,
 | 
			
		||||
    selectedElements: ExcalidrawElement[],
 | 
			
		||||
    event: { altKey: boolean },
 | 
			
		||||
  ) => void;
 | 
			
		||||
  colorPickerType: EyeDropperProperties["colorPickerType"];
 | 
			
		||||
}> = ({ onCancel, onChange, onSelect, colorPickerType }) => {
 | 
			
		||||
  const eyeDropperContainer = useCreatePortalContainer({
 | 
			
		||||
    className: "excalidraw-eye-dropper-backdrop",
 | 
			
		||||
    parentSelector: ".excalidraw-eye-dropper-container",
 | 
			
		||||
@@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{
 | 
			
		||||
 | 
			
		||||
  const selectedElements = getSelectedElements(elements, appState);
 | 
			
		||||
 | 
			
		||||
  const metaStuffRef = useRef({ selectedElements, app });
 | 
			
		||||
  metaStuffRef.current.selectedElements = selectedElements;
 | 
			
		||||
  metaStuffRef.current.app = app;
 | 
			
		||||
  const stableProps = useStable({
 | 
			
		||||
    app,
 | 
			
		||||
    onCancel,
 | 
			
		||||
    onChange,
 | 
			
		||||
    onSelect,
 | 
			
		||||
    selectedElements,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const { container: excalidrawContainer } = useExcalidrawContainer();
 | 
			
		||||
 | 
			
		||||
@@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{
 | 
			
		||||
      const currentColor = getCurrentColor({ clientX, clientY });
 | 
			
		||||
 | 
			
		||||
      if (isHoldingPointerDown) {
 | 
			
		||||
        for (const element of metaStuffRef.current.selectedElements) {
 | 
			
		||||
          mutateElement(
 | 
			
		||||
            element,
 | 
			
		||||
            {
 | 
			
		||||
              [altKey && swapPreviewOnAlt
 | 
			
		||||
                ? previewType === "strokeColor"
 | 
			
		||||
                  ? "backgroundColor"
 | 
			
		||||
                  : "strokeColor"
 | 
			
		||||
                : previewType]: currentColor,
 | 
			
		||||
            },
 | 
			
		||||
            false,
 | 
			
		||||
          );
 | 
			
		||||
          ShapeCache.delete(element);
 | 
			
		||||
        }
 | 
			
		||||
        Scene.getScene(
 | 
			
		||||
          metaStuffRef.current.selectedElements[0],
 | 
			
		||||
        )?.informMutation();
 | 
			
		||||
        stableProps.onChange(
 | 
			
		||||
          colorPickerType,
 | 
			
		||||
          currentColor,
 | 
			
		||||
          stableProps.selectedElements,
 | 
			
		||||
          { altKey },
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      colorPreviewDiv.style.background = currentColor;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onCancel = () => {
 | 
			
		||||
      stableProps.onCancel();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSelect: Required<EyeDropperProperties>["onSelect"] = (
 | 
			
		||||
      color,
 | 
			
		||||
      event,
 | 
			
		||||
    ) => {
 | 
			
		||||
      stableProps.onSelect(color, event);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const pointerDownListener = (event: PointerEvent) => {
 | 
			
		||||
      isHoldingPointerDown = true;
 | 
			
		||||
      // NOTE we can't event.preventDefault() as that would stop
 | 
			
		||||
@@ -148,8 +164,8 @@ export const EyeDropper: React.FC<{
 | 
			
		||||
 | 
			
		||||
    // init color preview else it would show only after the first mouse move
 | 
			
		||||
    mouseMoveListener({
 | 
			
		||||
      clientX: metaStuffRef.current.app.lastViewportPosition.x,
 | 
			
		||||
      clientY: metaStuffRef.current.app.lastViewportPosition.y,
 | 
			
		||||
      clientX: stableProps.app.lastViewportPosition.x,
 | 
			
		||||
      clientY: stableProps.app.lastViewportPosition.y,
 | 
			
		||||
      altKey: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{
 | 
			
		||||
      window.removeEventListener(EVENT.BLUR, onCancel);
 | 
			
		||||
    };
 | 
			
		||||
  }, [
 | 
			
		||||
    stableProps,
 | 
			
		||||
    app.canvas,
 | 
			
		||||
    eyeDropperContainer,
 | 
			
		||||
    onCancel,
 | 
			
		||||
    onSelect,
 | 
			
		||||
    swapPreviewOnAlt,
 | 
			
		||||
    previewType,
 | 
			
		||||
    colorPickerType,
 | 
			
		||||
    excalidrawContainer,
 | 
			
		||||
    appState.offsetLeft,
 | 
			
		||||
    appState.offsetTop,
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,9 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
 | 
			
		||||
 | 
			
		||||
import "./LayerUI.scss";
 | 
			
		||||
import "./Toolbar.scss";
 | 
			
		||||
import { mutateElement } from "../element/mutateElement";
 | 
			
		||||
import { ShapeCache } from "../scene/ShapeCache";
 | 
			
		||||
import Scene from "../scene/Scene";
 | 
			
		||||
 | 
			
		||||
interface LayerUIProps {
 | 
			
		||||
  actionManager: ActionManager;
 | 
			
		||||
@@ -368,11 +371,44 @@ const LayerUI = ({
 | 
			
		||||
      )}
 | 
			
		||||
      {eyeDropperState && !device.isMobile && (
 | 
			
		||||
        <EyeDropper
 | 
			
		||||
          swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt}
 | 
			
		||||
          previewType={eyeDropperState.previewType}
 | 
			
		||||
          colorPickerType={eyeDropperState.colorPickerType}
 | 
			
		||||
          onCancel={() => {
 | 
			
		||||
            setEyeDropperState(null);
 | 
			
		||||
          }}
 | 
			
		||||
          onChange={(colorPickerType, color, selectedElements, { altKey }) => {
 | 
			
		||||
            if (
 | 
			
		||||
              colorPickerType !== "elementBackground" &&
 | 
			
		||||
              colorPickerType !== "elementStroke"
 | 
			
		||||
            ) {
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (selectedElements.length) {
 | 
			
		||||
              for (const element of selectedElements) {
 | 
			
		||||
                mutateElement(
 | 
			
		||||
                  element,
 | 
			
		||||
                  {
 | 
			
		||||
                    [altKey && eyeDropperState.swapPreviewOnAlt
 | 
			
		||||
                      ? colorPickerType === "elementBackground"
 | 
			
		||||
                        ? "strokeColor"
 | 
			
		||||
                        : "backgroundColor"
 | 
			
		||||
                      : colorPickerType === "elementBackground"
 | 
			
		||||
                      ? "backgroundColor"
 | 
			
		||||
                      : "strokeColor"]: color,
 | 
			
		||||
                  },
 | 
			
		||||
                  false,
 | 
			
		||||
                );
 | 
			
		||||
                ShapeCache.delete(element);
 | 
			
		||||
              }
 | 
			
		||||
              Scene.getScene(selectedElements[0])?.informMutation();
 | 
			
		||||
            } else if (colorPickerType === "elementBackground") {
 | 
			
		||||
              setAppState({
 | 
			
		||||
                currentItemBackgroundColor: color,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              setAppState({ currentItemStrokeColor: color });
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          onSelect={(color, event) => {
 | 
			
		||||
            setEyeDropperState((state) => {
 | 
			
		||||
              return state?.keepOpenOnAlt && event.altKey ? state : null;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/hooks/useStable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/hooks/useStable.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import { useRef } from "react";
 | 
			
		||||
 | 
			
		||||
export const useStable = <T extends Record<string, any>>(value: T) => {
 | 
			
		||||
  const ref = useRef<T>(value);
 | 
			
		||||
  Object.assign(ref.current, value);
 | 
			
		||||
  return ref.current;
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user