Compare commits

...
10 changed files with 158 additions and 95 deletions
+11 -3
View File
@@ -332,6 +332,7 @@ import {
isMeasureTextSupported, isMeasureTextSupported,
isValidTextContainer, isValidTextContainer,
measureText, measureText,
normalizeText,
wrapText, wrapText,
} from "../element/textElement"; } from "../element/textElement";
import { import {
@@ -3412,7 +3413,7 @@ class App extends React.Component<AppProps, AppState> {
const lines = isPlainPaste ? [text] : text.split("\n"); const lines = isPlainPaste ? [text] : text.split("\n");
const textElements = lines.reduce( const textElements = lines.reduce(
(acc: ExcalidrawTextElement[], line, idx) => { (acc: ExcalidrawTextElement[], line, idx) => {
const originalText = line.trim(); const originalText = normalizeText(line).trim();
if (originalText.length) { if (originalText.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x, x,
@@ -3887,7 +3888,11 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
if (event.key === KEYS.ESCAPE && this.flowChartCreator.isCreatingChart) { if (!isInputLike(event.target)) {
if (
event.key === KEYS.ESCAPE &&
this.flowChartCreator.isCreatingChart
) {
this.flowChartCreator.clear(); this.flowChartCreator.clear();
this.triggerRender(true); this.triggerRender(true);
return; return;
@@ -3943,7 +3948,9 @@ class App extends React.Component<AppProps, AppState> {
), ),
})); }));
const nextNode = this.scene.getNonDeletedElementsMap().get(nextId); const nextNode = this.scene
.getNonDeletedElementsMap()
.get(nextId);
if ( if (
nextNode && nextNode &&
@@ -3970,6 +3977,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
} }
}
if ( if (
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
@@ -23,7 +23,6 @@ const handleDimensionChange: DragInputCallbackType<
> = ({ > = ({
accumulatedChange, accumulatedChange,
originalElements, originalElements,
originalElementsMap,
shouldKeepAspectRatio, shouldKeepAspectRatio,
shouldChangeByStepSize, shouldChangeByStepSize,
nextValue, nextValue,
+20 -11
View File
@@ -41,6 +41,7 @@ import {
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isLinearElement, isLinearElement,
isRectangularElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import type { ElementUpdate } from "./mutateElement"; import type { ElementUpdate } from "./mutateElement";
@@ -751,7 +752,7 @@ export const bindPointToSnapToElementOutline = (
const aabb = bindableElement && aabbForElement(bindableElement); const aabb = bindableElement && aabbForElement(bindableElement);
if (bindableElement && aabb) { if (bindableElement && aabb) {
// TODO: Dirty hack until tangents are properly calculated // TODO: Dirty hacks until tangents are properly calculated
const intersections = [ const intersections = [
...intersectElementWithLine( ...intersectElementWithLine(
bindableElement, bindableElement,
@@ -759,24 +760,32 @@ export const bindPointToSnapToElementOutline = (
[point[0], point[1] + 2 * bindableElement.height], [point[0], point[1] + 2 * bindableElement.height],
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
).map((i) => ).map((i) => {
distanceToBindableElement(bindableElement, i, elementsMap) >= if (!isRectangularElement(bindableElement)) {
bindableElement.height / 2 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], -1 * i[1]] as Point)
: ([point[0], i[1]] as Point), : ([point[0], i[1]] as Point);
), }),
...intersectElementWithLine( ...intersectElementWithLine(
bindableElement, bindableElement,
[point[0] - 2 * bindableElement.width, point[1]], [point[0] - 2 * bindableElement.width, point[1]],
[point[0] + 2 * bindableElement.width, point[1]], [point[0] + 2 * bindableElement.width, point[1]],
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
elementsMap, elementsMap,
).map((i) => ).map((i) => {
distanceToBindableElement(bindableElement, i, elementsMap) >= if (!isRectangularElement(bindableElement)) {
bindableElement.width / 2 return i;
}
const d = distanceToBindableElement(bindableElement, i, elementsMap);
return d >= bindableElement.width / 2 || d < FIXED_BINDING_DISTANCE
? ([-1 * i[0], point[1]] as Point) ? ([-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); const heading = headingForPointFromElement(bindableElement, aabb, point);
@@ -36,7 +36,7 @@ describe("flow chart creation", () => {
height: 100, height: 100,
}); });
h.elements = [rectangle]; API.setElements([rectangle]);
API.setSelectedElements([rectangle]); API.setSelectedElements([rectangle]);
}); });
@@ -166,7 +166,7 @@ describe("flow chart navigation", () => {
height: 100, height: 100,
}); });
h.elements = [rectangle]; API.setElements([rectangle]);
API.setSelectedElements([rectangle]); API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
@@ -234,7 +234,7 @@ describe("flow chart navigation", () => {
height: 100, height: 100,
}); });
h.elements = [rectangle]; API.setElements([rectangle]);
API.setSelectedElements([rectangle]); API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
@@ -325,7 +325,7 @@ describe("flow chart navigation", () => {
height: 100, height: 100,
}); });
h.elements = [rectangle]; API.setElements([rectangle]);
API.setSelectedElements([rectangle]); API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
@@ -741,7 +741,7 @@ export class LinearElementEditor {
} }
if (event.altKey && appState.editingLinearElement) { if (event.altKey && appState.editingLinearElement) {
if ( if (
linearElementEditor.lastUncommittedPoint == null && linearElementEditor.lastUncommittedPoint === null &&
!isElbowArrow(element) !isElbowArrow(element)
) { ) {
mutateElement(element, { mutateElement(element, {
+10 -1
View File
@@ -357,7 +357,16 @@ export const textWysiwyg = ({
}; };
editable.oninput = () => { 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);
}; };
} }
+17
View File
@@ -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 = ( export const isTextBindableContainer = (
element: ExcalidrawElement | null, element: ExcalidrawElement | null,
includeLocked = true, includeLocked = true,
+17 -1
View File
@@ -199,7 +199,7 @@ const generateElementCanvas = (
zoom: Zoom, zoom: Zoom,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
): ExcalidrawElementWithCanvas => { ): ExcalidrawElementWithCanvas | null => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
@@ -210,6 +210,10 @@ const generateElementCanvas = (
zoom, zoom,
); );
if (!width || !height) {
return null;
}
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
@@ -540,6 +544,10 @@ const generateElementWithCanvas = (
appState, appState,
); );
if (!elementWithCanvas) {
return null;
}
elementWithCanvasCache.set(element, elementWithCanvas); elementWithCanvasCache.set(element, elementWithCanvas);
return elementWithCanvas; return elementWithCanvas;
@@ -742,6 +750,10 @@ export const renderElement = (
renderConfig, renderConfig,
appState, appState,
); );
if (!elementWithCanvas) {
return;
}
drawElementFromCanvas( drawElementFromCanvas(
elementWithCanvas, elementWithCanvas,
context, context,
@@ -881,6 +893,10 @@ export const renderElement = (
appState, appState,
); );
if (!elementWithCanvas) {
return;
}
const currentImageSmoothingStatus = context.imageSmoothingEnabled; const currentImageSmoothingStatus = context.imageSmoothingEnabled;
if ( 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`;
+6 -3
View File
@@ -5,6 +5,7 @@ import fs from "fs";
import { vi } from "vitest"; import { vi } from "vitest";
import polyfill from "./packages/excalidraw/polyfill"; import polyfill from "./packages/excalidraw/polyfill";
import { testPolyfills } from "./packages/excalidraw/tests/helpers/polyfills"; import { testPolyfills } from "./packages/excalidraw/tests/helpers/polyfills";
import { yellow } from "./packages/excalidraw/tests/helpers/colorize";
Object.assign(globalThis, testPolyfills); Object.assign(globalThis, testPolyfills);
@@ -98,18 +99,20 @@ const element = document.createElement("div");
element.id = "root"; element.id = "root";
document.body.appendChild(element); document.body.appendChild(element);
const logger = console.error.bind(console); const _consoleError = console.error.bind(console);
console.error = (...args) => { console.error = (...args) => {
// the react's act() warning usually doesn't contain any useful stack trace // 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, // 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 // also stripping the actual component stack trace as it's not useful
if (args[0]?.includes("act(")) { if (args[0]?.includes("act(")) {
logger( _consoleError(
yellow(
`<<< WARNING: test "${ `<<< WARNING: test "${
expect.getState().currentTestName expect.getState().currentTestName
}" does not wrap some state update in act() >>>`, }" does not wrap some state update in act() >>>`,
),
); );
} else { } else {
logger(...args); _consoleError(...args);
} }
}; };