Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17477480f3 | |||
| f7b3befd0a | |||
| 7b2bee9746 | |||
| 88014ace4a | |||
| 87a9430809 | |||
| 99b91c46f7 | |||
| 1ea5b26f25 |
@@ -332,6 +332,7 @@ import {
|
||||
isMeasureTextSupported,
|
||||
isValidTextContainer,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
@@ -3412,7 +3413,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const lines = isPlainPaste ? [text] : text.split("\n");
|
||||
const textElements = lines.reduce(
|
||||
(acc: ExcalidrawTextElement[], line, idx) => {
|
||||
const originalText = line.trim();
|
||||
const originalText = normalizeText(line).trim();
|
||||
if (originalText.length) {
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
|
||||
x,
|
||||
@@ -3887,88 +3888,95 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
if (event.key === KEYS.ESCAPE && this.flowChartCreator.isCreatingChart) {
|
||||
this.flowChartCreator.clear();
|
||||
this.triggerRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const arrowKeyPressed = isArrowKey(event.key);
|
||||
|
||||
if (event[KEYS.CTRL_OR_CMD] && arrowKeyPressed && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (!isInputLike(event.target)) {
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
isFlowchartNodeElement(selectedElements[0])
|
||||
event.key === KEYS.ESCAPE &&
|
||||
this.flowChartCreator.isCreatingChart
|
||||
) {
|
||||
this.flowChartCreator.createNodes(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
getLinkDirectionFromKey(event.key),
|
||||
);
|
||||
this.flowChartCreator.clear();
|
||||
this.triggerRender(true);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const arrowKeyPressed = isArrowKey(event.key);
|
||||
|
||||
if (event.altKey) {
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.length === 1 && arrowKeyPressed) {
|
||||
if (event[KEYS.CTRL_OR_CMD] && arrowKeyPressed && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
||||
const nextId = this.flowChartNavigator.exploreByDirection(
|
||||
selectedElements[0],
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
getLinkDirectionFromKey(event.key),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (nextId) {
|
||||
this.setState((prevState) => ({
|
||||
selectedElementIds: makeNextSelectedElementIds(
|
||||
{
|
||||
[nextId]: true,
|
||||
},
|
||||
prevState,
|
||||
),
|
||||
}));
|
||||
|
||||
const nextNode = this.scene.getNonDeletedElementsMap().get(nextId);
|
||||
|
||||
if (
|
||||
nextNode &&
|
||||
!isElementCompletelyInViewport(
|
||||
nextNode,
|
||||
this.canvas.width / window.devicePixelRatio,
|
||||
this.canvas.height / window.devicePixelRatio,
|
||||
{
|
||||
offsetLeft: this.state.offsetLeft,
|
||||
offsetTop: this.state.offsetTop,
|
||||
scrollX: this.state.scrollX,
|
||||
scrollY: this.state.scrollY,
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
this.scrollToContent(nextNode, {
|
||||
animate: true,
|
||||
duration: 300,
|
||||
});
|
||||
}
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
isFlowchartNodeElement(selectedElements[0])
|
||||
) {
|
||||
this.flowChartCreator.createNodes(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
getLinkDirectionFromKey(event.key),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (selectedElements.length === 1 && arrowKeyPressed) {
|
||||
event.preventDefault();
|
||||
|
||||
const nextId = this.flowChartNavigator.exploreByDirection(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
getLinkDirectionFromKey(event.key),
|
||||
);
|
||||
|
||||
if (nextId) {
|
||||
this.setState((prevState) => ({
|
||||
selectedElementIds: makeNextSelectedElementIds(
|
||||
{
|
||||
[nextId]: true,
|
||||
},
|
||||
prevState,
|
||||
),
|
||||
}));
|
||||
|
||||
const nextNode = this.scene
|
||||
.getNonDeletedElementsMap()
|
||||
.get(nextId);
|
||||
|
||||
if (
|
||||
nextNode &&
|
||||
!isElementCompletelyInViewport(
|
||||
nextNode,
|
||||
this.canvas.width / window.devicePixelRatio,
|
||||
this.canvas.height / window.devicePixelRatio,
|
||||
{
|
||||
offsetLeft: this.state.offsetLeft,
|
||||
offsetTop: this.state.offsetTop,
|
||||
scrollX: this.state.scrollX,
|
||||
scrollY: this.state.scrollY,
|
||||
zoom: this.state.zoom,
|
||||
},
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
this.scrollToContent(nextNode, {
|
||||
animate: true,
|
||||
duration: 300,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -23,7 +23,6 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
> = ({
|
||||
accumulatedChange,
|
||||
originalElements,
|
||||
originalElementsMap,
|
||||
shouldKeepAspectRatio,
|
||||
shouldChangeByStepSize,
|
||||
nextValue,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
isElbowArrow,
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isRectangularElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
@@ -751,7 +752,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
const aabb = bindableElement && aabbForElement(bindableElement);
|
||||
|
||||
if (bindableElement && aabb) {
|
||||
// TODO: Dirty hack until tangents are properly calculated
|
||||
// TODO: Dirty hacks until tangents are properly calculated
|
||||
const intersections = [
|
||||
...intersectElementWithLine(
|
||||
bindableElement,
|
||||
@@ -759,24 +760,32 @@ export const bindPointToSnapToElementOutline = (
|
||||
[point[0], point[1] + 2 * bindableElement.height],
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
).map((i) =>
|
||||
distanceToBindableElement(bindableElement, i, elementsMap) >=
|
||||
bindableElement.height / 2
|
||||
).map((i) => {
|
||||
if (!isRectangularElement(bindableElement)) {
|
||||
return i;
|
||||
}
|
||||
|
||||
const d = distanceToBindableElement(bindableElement, i, elementsMap);
|
||||
return d >= bindableElement.height / 2 || d < FIXED_BINDING_DISTANCE
|
||||
? ([point[0], -1 * i[1]] as Point)
|
||||
: ([point[0], i[1]] as Point),
|
||||
),
|
||||
: ([point[0], i[1]] as Point);
|
||||
}),
|
||||
...intersectElementWithLine(
|
||||
bindableElement,
|
||||
[point[0] - 2 * bindableElement.width, point[1]],
|
||||
[point[0] + 2 * bindableElement.width, point[1]],
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
).map((i) =>
|
||||
distanceToBindableElement(bindableElement, i, elementsMap) >=
|
||||
bindableElement.width / 2
|
||||
).map((i) => {
|
||||
if (!isRectangularElement(bindableElement)) {
|
||||
return i;
|
||||
}
|
||||
|
||||
const d = distanceToBindableElement(bindableElement, i, elementsMap);
|
||||
return d >= bindableElement.width / 2 || d < FIXED_BINDING_DISTANCE
|
||||
? ([-1 * i[0], point[1]] as Point)
|
||||
: ([i[0], point[1]] as Point),
|
||||
),
|
||||
: ([i[0], point[1]] as Point);
|
||||
}),
|
||||
];
|
||||
|
||||
const heading = headingForPointFromElement(bindableElement, aabb, point);
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("flow chart creation", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
h.elements = [rectangle];
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("flow chart navigation", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
h.elements = [rectangle];
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
@@ -234,7 +234,7 @@ describe("flow chart navigation", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
h.elements = [rectangle];
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
@@ -325,7 +325,7 @@ describe("flow chart navigation", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
h.elements = [rectangle];
|
||||
API.setElements([rectangle]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
|
||||
@@ -741,7 +741,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
if (
|
||||
linearElementEditor.lastUncommittedPoint == null &&
|
||||
linearElementEditor.lastUncommittedPoint === null &&
|
||||
!isElbowArrow(element)
|
||||
) {
|
||||
mutateElement(element, {
|
||||
|
||||
@@ -357,7 +357,16 @@ export const textWysiwyg = ({
|
||||
};
|
||||
|
||||
editable.oninput = () => {
|
||||
onChange(normalizeText(editable.value));
|
||||
const normalized = normalizeText(editable.value);
|
||||
if (editable.value !== normalized) {
|
||||
const selectionStart = editable.selectionStart;
|
||||
editable.value = normalized;
|
||||
// put the cursor at some position close to where it was before
|
||||
// normalization (otherwise it'll end up at the end of the text)
|
||||
editable.selectionStart = selectionStart;
|
||||
editable.selectionEnd = selectionStart;
|
||||
}
|
||||
onChange(editable.value);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,23 @@ export const isRectanguloidElement = (
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Remove this when proper distance calculation is introduced
|
||||
// @see binding.ts:distanceToBindableElement()
|
||||
export const isRectangularElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawBindableElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "rectangle" ||
|
||||
element.type === "image" ||
|
||||
element.type === "text" ||
|
||||
element.type === "iframe" ||
|
||||
element.type === "embeddable" ||
|
||||
element.type === "frame" ||
|
||||
element.type === "magicframe")
|
||||
);
|
||||
};
|
||||
|
||||
export const isTextBindableContainer = (
|
||||
element: ExcalidrawElement | null,
|
||||
includeLocked = true,
|
||||
|
||||
@@ -199,7 +199,7 @@ const generateElementCanvas = (
|
||||
zoom: Zoom,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
): ExcalidrawElementWithCanvas => {
|
||||
): ExcalidrawElementWithCanvas | null => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
const padding = getCanvasPadding(element);
|
||||
@@ -210,6 +210,10 @@ const generateElementCanvas = (
|
||||
zoom,
|
||||
);
|
||||
|
||||
if (!width || !height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
@@ -540,6 +544,10 @@ const generateElementWithCanvas = (
|
||||
appState,
|
||||
);
|
||||
|
||||
if (!elementWithCanvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||
|
||||
return elementWithCanvas;
|
||||
@@ -742,6 +750,10 @@ export const renderElement = (
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
if (!elementWithCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawElementFromCanvas(
|
||||
elementWithCanvas,
|
||||
context,
|
||||
@@ -881,6 +893,10 @@ export const renderElement = (
|
||||
appState,
|
||||
);
|
||||
|
||||
if (!elementWithCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const yellow = (str: string) => `\u001b[33m${str}\u001b[0m`;
|
||||
export const red = (str: string) => `\u001b[31m${str}\u001b[0m`;
|
||||
+9
-6
@@ -5,6 +5,7 @@ import fs from "fs";
|
||||
import { vi } from "vitest";
|
||||
import polyfill from "./packages/excalidraw/polyfill";
|
||||
import { testPolyfills } from "./packages/excalidraw/tests/helpers/polyfills";
|
||||
import { yellow } from "./packages/excalidraw/tests/helpers/colorize";
|
||||
|
||||
Object.assign(globalThis, testPolyfills);
|
||||
|
||||
@@ -98,18 +99,20 @@ const element = document.createElement("div");
|
||||
element.id = "root";
|
||||
document.body.appendChild(element);
|
||||
|
||||
const logger = console.error.bind(console);
|
||||
const _consoleError = console.error.bind(console);
|
||||
console.error = (...args) => {
|
||||
// the react's act() warning usually doesn't contain any useful stack trace
|
||||
// so we're catching the log and re-logging the message with the test name,
|
||||
// also stripping the actual component stack trace as it's not useful
|
||||
if (args[0]?.includes("act(")) {
|
||||
logger(
|
||||
`<<< WARNING: test "${
|
||||
expect.getState().currentTestName
|
||||
}" does not wrap some state update in act() >>>`,
|
||||
_consoleError(
|
||||
yellow(
|
||||
`<<< WARNING: test "${
|
||||
expect.getState().currentTestName
|
||||
}" does not wrap some state update in act() >>>`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
logger(...args);
|
||||
_consoleError(...args);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user