Browse Source

fix: more eye-droper fixes (#7019)

David Luzar 1 year ago
parent
commit
f8b3692262

+ 8 - 5
src/components/App.tsx

@@ -1330,7 +1330,8 @@ class App extends React.Component<AppProps, AppState> {
   private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
   private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
     jotaiStore.set(activeEyeDropperAtom, {
     jotaiStore.set(activeEyeDropperAtom, {
       swapPreviewOnAlt: true,
       swapPreviewOnAlt: true,
-      previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
+      colorPickerType:
+        type === "stroke" ? "elementStroke" : "elementBackground",
       onSelect: (color, event) => {
       onSelect: (color, event) => {
         const shouldUpdateStrokeColor =
         const shouldUpdateStrokeColor =
           (type === "background" && event.altKey) ||
           (type === "background" && event.altKey) ||
@@ -1341,12 +1342,14 @@ class App extends React.Component<AppProps, AppState> {
           this.state.activeTool.type !== "selection"
           this.state.activeTool.type !== "selection"
         ) {
         ) {
           if (shouldUpdateStrokeColor) {
           if (shouldUpdateStrokeColor) {
-            this.setState({
-              currentItemStrokeColor: color,
+            this.syncActionResult({
+              appState: { ...this.state, currentItemStrokeColor: color },
+              commitToHistory: true,
             });
             });
           } else {
           } else {
-            this.setState({
-              currentItemBackgroundColor: color,
+            this.syncActionResult({
+              appState: { ...this.state, currentItemBackgroundColor: color },
+              commitToHistory: true,
             });
             });
           }
           }
         } else {
         } else {

+ 7 - 4
src/components/ColorPicker/ColorInput.tsx

@@ -1,7 +1,10 @@
 import { useCallback, useEffect, useRef, useState } from "react";
 import { useCallback, useEffect, useRef, useState } from "react";
 import { getColor } from "./ColorPicker";
 import { getColor } from "./ColorPicker";
 import { useAtom } from "jotai";
 import { useAtom } from "jotai";
-import { activeColorPickerSectionAtom } from "./colorPickerUtils";
+import {
+  ColorPickerType,
+  activeColorPickerSectionAtom,
+} from "./colorPickerUtils";
 import { eyeDropperIcon } from "../icons";
 import { eyeDropperIcon } from "../icons";
 import { jotaiScope } from "../../jotai";
 import { jotaiScope } from "../../jotai";
 import { KEYS } from "../../keys";
 import { KEYS } from "../../keys";
@@ -15,14 +18,14 @@ interface ColorInputProps {
   color: string;
   color: string;
   onChange: (color: string) => void;
   onChange: (color: string) => void;
   label: string;
   label: string;
-  eyeDropperType: "strokeColor" | "backgroundColor";
+  colorPickerType: ColorPickerType;
 }
 }
 
 
 export const ColorInput = ({
 export const ColorInput = ({
   color,
   color,
   onChange,
   onChange,
   label,
   label,
-  eyeDropperType,
+  colorPickerType,
 }: ColorInputProps) => {
 }: ColorInputProps) => {
   const device = useDevice();
   const device = useDevice();
   const [innerValue, setInnerValue] = useState(color);
   const [innerValue, setInnerValue] = useState(color);
@@ -116,7 +119,7 @@ export const ColorInput = ({
                   : {
                   : {
                       keepOpenOnAlt: false,
                       keepOpenOnAlt: false,
                       onSelect: (color) => onChange(color),
                       onSelect: (color) => onChange(color),
-                      previewType: eyeDropperType,
+                      colorPickerType,
                     },
                     },
               )
               )
             }
             }

+ 5 - 12
src/components/ColorPicker/ColorPicker.tsx

@@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
   const { container } = useExcalidrawContainer();
   const { container } = useExcalidrawContainer();
   const { isMobile, isLandscape } = useDevice();
   const { isMobile, isLandscape } = useDevice();
 
 
-  const eyeDropperType =
-    type === "canvasBackground"
-      ? undefined
-      : type === "elementBackground"
-      ? "backgroundColor"
-      : "strokeColor";
-
-  const colorInputJSX = eyeDropperType && (
+  const colorInputJSX = (
     <div>
     <div>
       <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
       <PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
       <ColorInput
       <ColorInput
@@ -98,7 +91,7 @@ const ColorPickerPopupContent = ({
         onChange={(color) => {
         onChange={(color) => {
           onChange(color);
           onChange(color);
         }}
         }}
-        eyeDropperType={eyeDropperType}
+        colorPickerType={type}
       />
       />
     </div>
     </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)",
             "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
           <Picker
             palette={palette}
             palette={palette}
             color={color}
             color={color}
@@ -173,7 +166,7 @@ const ColorPickerPopupContent = ({
                   state = state || {
                   state = state || {
                     keepOpenOnAlt: true,
                     keepOpenOnAlt: true,
                     onSelect: onChange,
                     onSelect: onChange,
-                    previewType: eyeDropperType,
+                    colorPickerType: type,
                   };
                   };
                   state.keepOpenOnAlt = true;
                   state.keepOpenOnAlt = true;
                   return state;
                   return state;
@@ -184,7 +177,7 @@ const ColorPickerPopupContent = ({
                   : {
                   : {
                       keepOpenOnAlt: false,
                       keepOpenOnAlt: false,
                       onSelect: onChange,
                       onSelect: onChange,
-                      previewType: eyeDropperType,
+                      colorPickerType: type,
                     };
                     };
               });
               });
             }}
             }}

+ 50 - 36
src/components/EyeDropper.tsx

@@ -1,35 +1,47 @@
 import { atom } from "jotai";
 import { atom } from "jotai";
-import { useEffect, useRef } from "react";
+import React, { useEffect, useRef } from "react";
 import { createPortal } from "react-dom";
 import { createPortal } from "react-dom";
 import { rgbToHex } from "../colors";
 import { rgbToHex } from "../colors";
 import { EVENT } from "../constants";
 import { EVENT } from "../constants";
 import { useUIAppState } from "../context/ui-appState";
 import { useUIAppState } from "../context/ui-appState";
-import { mutateElement } from "../element/mutateElement";
 import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
 import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
 import { useOutsideClick } from "../hooks/useOutsideClick";
 import { useOutsideClick } from "../hooks/useOutsideClick";
 import { KEYS } from "../keys";
 import { KEYS } from "../keys";
 import { getSelectedElements } from "../scene";
 import { getSelectedElements } from "../scene";
-import Scene from "../scene/Scene";
-import { ShapeCache } from "../scene/ShapeCache";
 import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
 import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
+import { useStable } from "../hooks/useStable";
 
 
 import "./EyeDropper.scss";
 import "./EyeDropper.scss";
+import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
+import { ExcalidrawElement } from "../element/types";
 
 
-type EyeDropperProperties = {
+export type EyeDropperProperties = {
   keepOpenOnAlt: boolean;
   keepOpenOnAlt: boolean;
   swapPreviewOnAlt?: boolean;
   swapPreviewOnAlt?: boolean;
+  /** called when user picks color (on pointerup) */
   onSelect: (color: string, event: PointerEvent) => void;
   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 activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
 
 
 export const EyeDropper: React.FC<{
 export const EyeDropper: React.FC<{
   onCancel: () => void;
   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({
   const eyeDropperContainer = useCreatePortalContainer({
     className: "excalidraw-eye-dropper-backdrop",
     className: "excalidraw-eye-dropper-backdrop",
     parentSelector: ".excalidraw-eye-dropper-container",
     parentSelector: ".excalidraw-eye-dropper-container",
@@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{
 
 
   const selectedElements = getSelectedElements(elements, appState);
   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();
   const { container: excalidrawContainer } = useExcalidrawContainer();
 
 
@@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{
       const currentColor = getCurrentColor({ clientX, clientY });
       const currentColor = getCurrentColor({ clientX, clientY });
 
 
       if (isHoldingPointerDown) {
       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;
       colorPreviewDiv.style.background = currentColor;
     };
     };
 
 
+    const onCancel = () => {
+      stableProps.onCancel();
+    };
+
+    const onSelect: Required<EyeDropperProperties>["onSelect"] = (
+      color,
+      event,
+    ) => {
+      stableProps.onSelect(color, event);
+    };
+
     const pointerDownListener = (event: PointerEvent) => {
     const pointerDownListener = (event: PointerEvent) => {
       isHoldingPointerDown = true;
       isHoldingPointerDown = true;
       // NOTE we can't event.preventDefault() as that would stop
       // 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
     // init color preview else it would show only after the first mouse move
     mouseMoveListener({
     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,
       altKey: false,
     });
     });
 
 
@@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{
       window.removeEventListener(EVENT.BLUR, onCancel);
       window.removeEventListener(EVENT.BLUR, onCancel);
     };
     };
   }, [
   }, [
+    stableProps,
     app.canvas,
     app.canvas,
     eyeDropperContainer,
     eyeDropperContainer,
-    onCancel,
-    onSelect,
-    swapPreviewOnAlt,
-    previewType,
+    colorPickerType,
     excalidrawContainer,
     excalidrawContainer,
     appState.offsetLeft,
     appState.offsetLeft,
     appState.offsetTop,
     appState.offsetTop,

+ 38 - 2
src/components/LayerUI.tsx

@@ -52,6 +52,9 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
 
 
 import "./LayerUI.scss";
 import "./LayerUI.scss";
 import "./Toolbar.scss";
 import "./Toolbar.scss";
+import { mutateElement } from "../element/mutateElement";
+import { ShapeCache } from "../scene/ShapeCache";
+import Scene from "../scene/Scene";
 
 
 interface LayerUIProps {
 interface LayerUIProps {
   actionManager: ActionManager;
   actionManager: ActionManager;
@@ -368,11 +371,44 @@ const LayerUI = ({
       )}
       )}
       {eyeDropperState && !device.isMobile && (
       {eyeDropperState && !device.isMobile && (
         <EyeDropper
         <EyeDropper
-          swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt}
-          previewType={eyeDropperState.previewType}
+          colorPickerType={eyeDropperState.colorPickerType}
           onCancel={() => {
           onCancel={() => {
             setEyeDropperState(null);
             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) => {
           onSelect={(color, event) => {
             setEyeDropperState((state) => {
             setEyeDropperState((state) => {
               return state?.keepOpenOnAlt && event.altKey ? state : null;
               return state?.keepOpenOnAlt && event.altKey ? state : null;

+ 7 - 0
src/hooks/useStable.ts

@@ -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;
+};