fix: more eye-droper fixes (#7019)
This commit is contained in:
parent
741d5f1a18
commit
f8b3692262
@ -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 {
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -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,
|
||||||
|
@ -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
7
src/hooks/useStable.ts
Normal 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;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user