fix: more eye-droper fixes (#7019)

This commit is contained in:
David Luzar 2023-09-21 06:24:03 +02:00 committed by GitHub
parent 741d5f1a18
commit f8b3692262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 59 deletions

View File

@ -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({ this.syncActionResult({
currentItemStrokeColor: color, appState: { ...this.state, currentItemStrokeColor: color },
commitToHistory: true,
}); });
} else { } else {
this.setState({ this.syncActionResult({
currentItemBackgroundColor: color, appState: { ...this.state, currentItemBackgroundColor: color },
commitToHistory: true,
}); });
} }
} else { } else {

View File

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

View File

@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
const { container } = useExcalidrawContainer(); const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice(); const { isMobile, isLandscape } = useDevice();
const eyeDropperType = const colorInputJSX = (
type === "canvasBackground"
? undefined
: type === "elementBackground"
? "backgroundColor"
: "strokeColor";
const colorInputJSX = eyeDropperType && (
<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,
}; };
}); });
}} }}

View File

@ -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"]; onSelect: EyeDropperProperties["onSelect"];
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"]; /** called when color changes, on pointerdown for preview */
previewType: EyeDropperProperties["previewType"]; onChange: (
}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => { 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 }); const stableProps = useStable({
metaStuffRef.current.selectedElements = selectedElements; app,
metaStuffRef.current.app = 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) { stableProps.onChange(
mutateElement( colorPickerType,
element, currentColor,
{ stableProps.selectedElements,
[altKey && swapPreviewOnAlt { altKey },
? previewType === "strokeColor" );
? "backgroundColor"
: "strokeColor"
: previewType]: currentColor,
},
false,
);
ShapeCache.delete(element);
}
Scene.getScene(
metaStuffRef.current.selectedElements[0],
)?.informMutation();
} }
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, clientX: stableProps.app.lastViewportPosition.x,
clientY: metaStuffRef.current.app.lastViewportPosition.y, 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, colorPickerType,
onSelect,
swapPreviewOnAlt,
previewType,
excalidrawContainer, excalidrawContainer,
appState.offsetLeft, appState.offsetLeft,
appState.offsetTop, appState.offsetTop,

View File

@ -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} colorPickerType={eyeDropperState.colorPickerType}
previewType={eyeDropperState.previewType}
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
src/hooks/useStable.ts Normal file
View 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;
};