Procházet zdrojové kódy

fix: more eye-droper fixes (#7019)

David Luzar před 1 rokem
rodič
revize
f8b3692262

+ 8 - 5
src/components/App.tsx

@@ -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 {

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

@@ -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,
                     },
               )
             }

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

@@ -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,
                     };
               });
             }}

+ 50 - 36
src/components/EyeDropper.tsx

@@ -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,

+ 38 - 2
src/components/LayerUI.tsx

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