diff --git a/src/components/App.tsx b/src/components/App.tsx index 367c8f7f..4dff0025 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1330,7 +1330,8 @@ class App extends React.Component { 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 { 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 { diff --git a/src/components/ColorPicker/ColorInput.tsx b/src/components/ColorPicker/ColorInput.tsx index 968729cc..f10a174d 100644 --- a/src/components/ColorPicker/ColorInput.tsx +++ b/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, }, ) } diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx index 4a378950..ff0b6f41 100644 --- a/src/components/ColorPicker/ColorPicker.tsx +++ b/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 = (
{t("colorPicker.hexCode")} { onChange(color); }} - eyeDropperType={eyeDropperType} + colorPickerType={type} />
); @@ -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 ? ( 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); export const EyeDropper: React.FC<{ onCancel: () => void; - onSelect: Required["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["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, diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 02f360e2..3bc8436c 100644 --- a/src/components/LayerUI.tsx +++ b/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 && ( { 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; diff --git a/src/hooks/useStable.ts b/src/hooks/useStable.ts new file mode 100644 index 00000000..56608489 --- /dev/null +++ b/src/hooks/useStable.ts @@ -0,0 +1,7 @@ +import { useRef } from "react"; + +export const useStable = >(value: T) => { + const ref = useRef(value); + Object.assign(ref.current, value); + return ref.current; +};