|
@@ -304,6 +304,7 @@ import { jotaiStore } from "../jotai";
|
|
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
|
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
|
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
|
|
+import { activeEyeDropperAtom } from "./EyeDropper";
|
|
|
|
|
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
|
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
|
@@ -366,8 +367,6 @@ export const useExcalidrawActionManager = () =>
|
|
|
|
|
|
let didTapTwice: boolean = false;
|
|
|
let tappedTwiceTimer = 0;
|
|
|
-let cursorX = 0;
|
|
|
-let cursorY = 0;
|
|
|
let isHoldingSpace: boolean = false;
|
|
|
let isPanning: boolean = false;
|
|
|
let isDraggingScrollBar: boolean = false;
|
|
@@ -425,7 +424,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
hitLinkElement?: NonDeletedExcalidrawElement;
|
|
|
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
|
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
|
|
- lastScenePointer: { x: number; y: number } | null = null;
|
|
|
+ lastViewportPosition = { x: 0, y: 0 };
|
|
|
|
|
|
constructor(props: AppProps) {
|
|
|
super(props);
|
|
@@ -634,6 +633,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
</LayerUI>
|
|
|
<div className="excalidraw-textEditorContainer" />
|
|
|
<div className="excalidraw-contextMenuContainer" />
|
|
|
+ <div className="excalidraw-eye-dropper-container" />
|
|
|
{selectedElement.length === 1 &&
|
|
|
!this.state.contextMenu &&
|
|
|
this.state.showHyperlinkPopup && (
|
|
@@ -724,6 +724,49 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
|
|
+ jotaiStore.set(activeEyeDropperAtom, {
|
|
|
+ swapPreviewOnAlt: true,
|
|
|
+ previewType: type === "stroke" ? "strokeColor" : "backgroundColor",
|
|
|
+ onSelect: (color, event) => {
|
|
|
+ const shouldUpdateStrokeColor =
|
|
|
+ (type === "background" && event.altKey) ||
|
|
|
+ (type === "stroke" && !event.altKey);
|
|
|
+ const selectedElements = getSelectedElements(
|
|
|
+ this.scene.getElementsIncludingDeleted(),
|
|
|
+ this.state,
|
|
|
+ );
|
|
|
+ if (
|
|
|
+ !selectedElements.length ||
|
|
|
+ this.state.activeTool.type !== "selection"
|
|
|
+ ) {
|
|
|
+ if (shouldUpdateStrokeColor) {
|
|
|
+ this.setState({
|
|
|
+ currentItemStrokeColor: color,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ this.setState({
|
|
|
+ currentItemBackgroundColor: color,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.updateScene({
|
|
|
+ elements: this.scene.getElementsIncludingDeleted().map((el) => {
|
|
|
+ if (this.state.selectedElementIds[el.id]) {
|
|
|
+ return newElementWith(el, {
|
|
|
+ [shouldUpdateStrokeColor ? "strokeColor" : "backgroundColor"]:
|
|
|
+ color,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return el;
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ keepOpenOnAlt: false,
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
private syncActionResult = withBatchedUpdates(
|
|
|
(actionResult: ActionResult) => {
|
|
|
if (this.unmounted || actionResult === false) {
|
|
@@ -1569,7 +1612,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const elementUnderCursor = document.elementFromPoint(cursorX, cursorY);
|
|
|
+ const elementUnderCursor = document.elementFromPoint(
|
|
|
+ this.lastViewportPosition.x,
|
|
|
+ this.lastViewportPosition.y,
|
|
|
+ );
|
|
|
if (
|
|
|
event &&
|
|
|
(!(elementUnderCursor instanceof HTMLCanvasElement) ||
|
|
@@ -1597,7 +1643,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
|
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
|
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
|
|
- { clientX: cursorX, clientY: cursorY },
|
|
|
+ {
|
|
|
+ clientX: this.lastViewportPosition.x,
|
|
|
+ clientY: this.lastViewportPosition.y,
|
|
|
+ },
|
|
|
this.state,
|
|
|
);
|
|
|
|
|
@@ -1660,13 +1709,13 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
typeof opts.position === "object"
|
|
|
? opts.position.clientX
|
|
|
: opts.position === "cursor"
|
|
|
- ? cursorX
|
|
|
+ ? this.lastViewportPosition.x
|
|
|
: this.state.width / 2 + this.state.offsetLeft;
|
|
|
const clientY =
|
|
|
typeof opts.position === "object"
|
|
|
? opts.position.clientY
|
|
|
: opts.position === "cursor"
|
|
|
- ? cursorY
|
|
|
+ ? this.lastViewportPosition.y
|
|
|
: this.state.height / 2 + this.state.offsetTop;
|
|
|
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
@@ -1750,7 +1799,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
private addTextFromPaste(text: string, isPlainPaste = false) {
|
|
|
const { x, y } = viewportCoordsToSceneCoords(
|
|
|
- { clientX: cursorX, clientY: cursorY },
|
|
|
+ {
|
|
|
+ clientX: this.lastViewportPosition.x,
|
|
|
+ clientY: this.lastViewportPosition.y,
|
|
|
+ },
|
|
|
this.state,
|
|
|
);
|
|
|
|
|
@@ -2083,8 +2135,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
|
|
|
private updateCurrentCursorPosition = withBatchedUpdates(
|
|
|
(event: MouseEvent) => {
|
|
|
- cursorX = event.clientX;
|
|
|
- cursorY = event.clientY;
|
|
|
+ this.lastViewportPosition.x = event.clientX;
|
|
|
+ this.lastViewportPosition.y = event.clientY;
|
|
|
},
|
|
|
);
|
|
|
|
|
@@ -2342,6 +2394,20 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
) {
|
|
|
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
|
|
}
|
|
|
+
|
|
|
+ // eye dropper
|
|
|
+ // -----------------------------------------------------------------------
|
|
|
+ const lowerCased = event.key.toLocaleLowerCase();
|
|
|
+ const isPickingStroke = lowerCased === KEYS.S && event.shiftKey;
|
|
|
+ const isPickingBackground =
|
|
|
+ event.key === KEYS.I || (lowerCased === KEYS.G && event.shiftKey);
|
|
|
+
|
|
|
+ if (isPickingStroke || isPickingBackground) {
|
|
|
+ this.openEyeDropper({
|
|
|
+ type: isPickingStroke ? "stroke" : "background",
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // -----------------------------------------------------------------------
|
|
|
},
|
|
|
);
|
|
|
|
|
@@ -2471,8 +2537,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
this.setState((state) => ({
|
|
|
...getStateForZoom(
|
|
|
{
|
|
|
- viewportX: cursorX,
|
|
|
- viewportY: cursorY,
|
|
|
+ viewportX: this.lastViewportPosition.x,
|
|
|
+ viewportY: this.lastViewportPosition.y,
|
|
|
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
|
|
},
|
|
|
state,
|
|
@@ -6468,8 +6534,8 @@ class App extends React.Component<AppProps, AppState> {
|
|
|
this.translateCanvas((state) => ({
|
|
|
...getStateForZoom(
|
|
|
{
|
|
|
- viewportX: cursorX,
|
|
|
- viewportY: cursorY,
|
|
|
+ viewportX: this.lastViewportPosition.x,
|
|
|
+ viewportY: this.lastViewportPosition.y,
|
|
|
nextZoom: getNormalizedZoom(newZoom),
|
|
|
},
|
|
|
state,
|