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" }) => {
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 {

View File

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

View File

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

View File

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

View File

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