Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax-maint-stage

This commit is contained in:
Daniel J. Geiger 2023-09-17 15:40:45 -05:00
commit 208285b7ba
18 changed files with 275 additions and 104 deletions

View file

@ -60,6 +60,7 @@ const flipSelectedElements = (
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);

View file

@ -15,9 +15,15 @@ interface ColorInputProps {
color: string;
onChange: (color: string) => void;
label: string;
eyeDropperType: "strokeColor" | "backgroundColor";
}
export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
export const ColorInput = ({
color,
onChange,
label,
eyeDropperType,
}: ColorInputProps) => {
const device = useDevice();
const [innerValue, setInnerValue] = useState(color);
const [activeSection, setActiveColorPickerSection] = useAtom(
@ -110,6 +116,7 @@ export const ColorInput = ({ color, onChange, label }: ColorInputProps) => {
: {
keepOpenOnAlt: false,
onSelect: (color) => onChange(color),
previewType: eyeDropperType,
},
)
}

View file

@ -82,7 +82,14 @@ const ColorPickerPopupContent = ({
const { container } = useExcalidrawContainer();
const { isMobile, isLandscape } = useDevice();
const colorInputJSX = (
const eyeDropperType =
type === "canvasBackground"
? undefined
: type === "elementBackground"
? "backgroundColor"
: "strokeColor";
const colorInputJSX = eyeDropperType && (
<div>
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
<ColorInput
@ -91,6 +98,7 @@ const ColorPickerPopupContent = ({
onChange={(color) => {
onChange(color);
}}
eyeDropperType={eyeDropperType}
/>
</div>
);
@ -140,7 +148,7 @@ const ColorPickerPopupContent = ({
alignOffset={-16}
sideOffset={20}
style={{
zIndex: 9999,
zIndex: "var(--zIndex-layerUI)",
backgroundColor: "var(--popup-bg-color)",
maxWidth: "208px",
maxHeight: window.innerHeight,
@ -152,7 +160,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 ? (
{palette && eyeDropperType ? (
<Picker
palette={palette}
color={color}
@ -165,6 +173,7 @@ const ColorPickerPopupContent = ({
state = state || {
keepOpenOnAlt: true,
onSelect: onChange,
previewType: eyeDropperType,
};
state.keepOpenOnAlt = true;
return state;
@ -175,6 +184,7 @@ const ColorPickerPopupContent = ({
: {
keepOpenOnAlt: false,
onSelect: onChange,
previewType: eyeDropperType,
};
});
}}

View file

@ -4,7 +4,7 @@
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
z-index: var(--zIndex-eyeDropperBackdrop);
touch-action: none;
}
@ -21,7 +21,7 @@
width: 3rem;
height: 3rem;
position: fixed;
z-index: 999999;
z-index: var(--zIndex-eyeDropperPreview);
border-radius: 1rem;
border: 1px solid var(--default-border-color);
filter: var(--theme-filter);

View file

@ -1,7 +1,7 @@
import { atom } from "jotai";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { COLOR_PALETTE, rgbToHex } from "../colors";
import { rgbToHex } from "../colors";
import { EVENT } from "../constants";
import { useUIAppState } from "../context/ui-appState";
import { mutateElement } from "../element/mutateElement";
@ -18,8 +18,8 @@ import "./EyeDropper.scss";
type EyeDropperProperties = {
keepOpenOnAlt: boolean;
swapPreviewOnAlt?: boolean;
onSelect?: (color: string, event: PointerEvent) => void;
previewType?: "strokeColor" | "backgroundColor";
onSelect: (color: string, event: PointerEvent) => void;
previewType: "strokeColor" | "backgroundColor";
};
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
@ -28,13 +28,8 @@ export const EyeDropper: React.FC<{
onCancel: () => void;
onSelect: Required<EyeDropperProperties>["onSelect"];
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
previewType?: EyeDropperProperties["previewType"];
}> = ({
onCancel,
onSelect,
swapPreviewOnAlt,
previewType = "backgroundColor",
}) => {
previewType: EyeDropperProperties["previewType"];
}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => {
const eyeDropperContainer = useCreatePortalContainer({
className: "excalidraw-eye-dropper-backdrop",
parentSelector: ".excalidraw-eye-dropper-container",
@ -58,11 +53,27 @@ export const EyeDropper: React.FC<{
return;
}
let currentColor: string = COLOR_PALETTE.black;
let isHoldingPointerDown = false;
const ctx = app.canvas.getContext("2d")!;
const getCurrentColor = ({
clientX,
clientY,
}: {
clientX: number;
clientY: number;
}) => {
const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1,
).data;
return rgbToHex(pixel[0], pixel[1], pixel[2]);
};
const mouseMoveListener = ({
clientX,
clientY,
@ -76,14 +87,7 @@ export const EyeDropper: React.FC<{
colorPreviewDiv.style.top = `${clientY + 20}px`;
colorPreviewDiv.style.left = `${clientX + 20}px`;
const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1,
).data;
currentColor = rgbToHex(pixel[0], pixel[1], pixel[2]);
const currentColor = getCurrentColor({ clientX, clientY });
if (isHoldingPointerDown) {
for (const element of metaStuffRef.current.selectedElements) {
@ -125,7 +129,7 @@ export const EyeDropper: React.FC<{
event.stopImmediatePropagation();
event.preventDefault();
onSelect(currentColor, event);
onSelect(getCurrentColor(event), event);
};
const keyDownListener = (event: KeyboardEvent) => {

View file

@ -25,10 +25,10 @@
}
.default-sidebar-trigger .sidebar-trigger__label {
display: none;
display: block;
}
@media screen and (min-width: 1024px) {
display: block;
}
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
display: none;
}
}

View file

@ -6,6 +6,8 @@
--zIndex-interactiveCanvas: 2;
--zIndex-wysiwyg: 3;
--zIndex-layerUI: 4;
--zIndex-eyeDropperBackdrop: 5;
--zIndex-eyeDropperPreview: 6;
--zIndex-modal: 1000;
--zIndex-popup: 1001;

View file

@ -121,6 +121,7 @@ export const isBindableElement = (
element.type === "ellipse" ||
element.type === "image" ||
element.type === "embeddable" ||
element.type === "frame" ||
(element.type === "text" && !element.containerId))
);
};

View file

@ -1,11 +1,13 @@
import ReactDOM from "react-dom";
import {
createPasteEvent,
fireEvent,
GlobalTestState,
render,
screen,
waitFor,
} from "./test-utils";
import { UI, Pointer } from "./helpers/ui";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { API } from "./helpers/api";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
import { getElementAbsoluteCoords } from "../element";
@ -13,6 +15,7 @@ import {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FileId,
} from "../element/types";
import { newLinearElement } from "../element";
@ -22,6 +25,8 @@ import { NormalizedZoomValue } from "../types";
import { ROUNDNESS } from "../constants";
import { vi } from "vitest";
import * as blob from "../data/blob";
import { KEYS } from "../keys";
import { getBoundTextElementPosition } from "../element/textElement";
const { h } = window;
const mouse = new Pointer("mouse");
@ -812,3 +817,69 @@ describe("image", () => {
expect(h.elements[0].angle).toBeCloseTo(0);
});
});
describe("mutliple elements", () => {
it("with bound text flip correctly", async () => {
UI.clickTool("arrow");
fireEvent.click(screen.getByTitle("Architect"));
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
width: 180,
height: 80,
});
Keyboard.keyPress(KEYS.ENTER);
let editor = document.querySelector<HTMLTextAreaElement>(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "arrow" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
const rectangle = UI.createElement("rectangle", {
x: 0,
y: 100,
width: 100,
height: 100,
});
Keyboard.keyPress(KEYS.ENTER);
editor = document.querySelector<HTMLTextAreaElement>(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "rect\ntext" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
mouse.select([arrow, rectangle]);
h.app.actionManager.executeAction(actionFlipHorizontal);
h.app.actionManager.executeAction(actionFlipVertical);
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
const arrowTextPos = getBoundTextElementPosition(arrow.get(), arrowText)!;
const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
expect(arrow.x).toBeCloseTo(180);
expect(arrow.y).toBeCloseTo(200);
expect(arrow.points[1][0]).toBeCloseTo(-180);
expect(arrow.points[1][1]).toBeCloseTo(-80);
expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo(
arrow.x - (arrowTextPos.x + arrowText.width),
);
expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo(
arrow.y - (arrowTextPos.y + arrowText.height),
);
expect(rectangle.x).toBeCloseTo(80);
expect(rectangle.y).toBeCloseTo(0);
expect(rectText.x - rectangle.x).toBeCloseTo(
rectangle.x + rectangle.width - (rectText.x + rectText.width),
);
expect(rectText.y - rectangle.y).toBeCloseTo(
rectangle.y + rectangle.height - (rectText.y + rectText.height),
);
});
});

View file

@ -518,8 +518,9 @@ export type AppProps = Merge<
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
export type AppClassProperties = {
props: AppProps;
canvas: HTMLCanvasElement;
interactiveCanvas: HTMLCanvasElement | null;
/** static canvas */
canvas: HTMLCanvasElement;
focusContainer(): void;
library: Library;
imageCache: Map<