mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-05 21:34:39 +01:00
Compare commits
7 Commits
mtolmacs/f
...
mtolmacs/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad028cf493 | ||
|
|
a55ebda8ad | ||
|
|
b379bcca48 | ||
|
|
4e0441eeb4 | ||
|
|
8013eb5e16 | ||
|
|
725412ebd3 | ||
|
|
7da176ff7d |
@@ -663,8 +663,8 @@ const ExcalidrawWrapper = () => {
|
||||
debugRenderer(
|
||||
debugCanvasRef.current,
|
||||
appState,
|
||||
elements,
|
||||
window.devicePixelRatio,
|
||||
() => forceRefresh((prev) => !prev),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,9 +8,16 @@ import {
|
||||
getNormalizedCanvasDimensions,
|
||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||
import { throttleRAF } from "@excalidraw/common";
|
||||
import { arrayToMap, throttleRAF } from "@excalidraw/common";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isFixedPointBinding,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
isLineSegment,
|
||||
type GlobalPoint,
|
||||
@@ -21,8 +28,15 @@ import { isCurve } from "@excalidraw/math/curve";
|
||||
import React from "react";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
import type { DebugElement } from "@excalidraw/utils/visualdebug";
|
||||
import type { DebugElement } from "@excalidraw/common";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
FixedPointBinding,
|
||||
OrderedExcalidrawElement,
|
||||
PointBinding,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
|
||||
@@ -75,6 +89,180 @@ const renderOrigin = (context: CanvasRenderingContext2D, zoom: number) => {
|
||||
context.save();
|
||||
};
|
||||
|
||||
const _renderBinding = (
|
||||
context: CanvasRenderingContext2D,
|
||||
binding: FixedPointBinding | PointBinding,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: number,
|
||||
width: number,
|
||||
height: number,
|
||||
color: string,
|
||||
) => {
|
||||
if (isFixedPointBinding(binding)) {
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom - width,
|
||||
y * zoom - height,
|
||||
x * zoom - width,
|
||||
y * zoom + height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
};
|
||||
|
||||
const _renderBindableBinding = (
|
||||
binding: FixedPointBinding | PointBinding,
|
||||
context: CanvasRenderingContext2D,
|
||||
elementsMap: ElementsMap,
|
||||
zoom: number,
|
||||
width: number,
|
||||
height: number,
|
||||
color: string,
|
||||
) => {
|
||||
if (isFixedPointBinding(binding)) {
|
||||
const bindable = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (!binding.fixedPoint) {
|
||||
console.warn("Binding must have a fixedPoint");
|
||||
return;
|
||||
}
|
||||
|
||||
const [x, y] = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindable,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.moveTo(x * zoom, y * zoom);
|
||||
context.bezierCurveTo(
|
||||
x * zoom + width,
|
||||
y * zoom + height,
|
||||
x * zoom + width,
|
||||
y * zoom - height,
|
||||
x * zoom,
|
||||
y * zoom,
|
||||
);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
}
|
||||
};
|
||||
|
||||
const renderBindings = (
|
||||
context: CanvasRenderingContext2D,
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
zoom: number,
|
||||
) => {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const dim = 16;
|
||||
elements.forEach((element) => {
|
||||
if (element.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArrowElement(element)) {
|
||||
if (element.startBinding) {
|
||||
if (
|
||||
!elementsMap
|
||||
.get(element.startBinding.elementId)
|
||||
?.boundElements?.find((e) => e.id === element.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
_renderBinding(
|
||||
context,
|
||||
element.startBinding as FixedPointBinding,
|
||||
elementsMap,
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"red",
|
||||
);
|
||||
}
|
||||
|
||||
if (element.endBinding) {
|
||||
if (
|
||||
!elementsMap
|
||||
.get(element.endBinding.elementId)
|
||||
?.boundElements?.find((e) => e.id === element.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
_renderBinding(
|
||||
context,
|
||||
element.endBinding,
|
||||
elementsMap,
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"red",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBindableElement(element) && element.boundElements?.length) {
|
||||
element.boundElements.forEach((boundElement) => {
|
||||
if (boundElement.type !== "arrow") {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrow = elementsMap.get(
|
||||
boundElement.id,
|
||||
) as ExcalidrawArrowElement;
|
||||
|
||||
if (arrow && arrow.startBinding?.elementId === element.id) {
|
||||
_renderBindableBinding(
|
||||
arrow.startBinding,
|
||||
context,
|
||||
elementsMap,
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"green",
|
||||
);
|
||||
}
|
||||
if (arrow && arrow.endBinding?.elementId === element.id) {
|
||||
_renderBindableBinding(
|
||||
arrow.endBinding,
|
||||
context,
|
||||
elementsMap,
|
||||
zoom,
|
||||
dim,
|
||||
dim,
|
||||
"green",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const render = (
|
||||
frame: DebugElement[],
|
||||
context: CanvasRenderingContext2D,
|
||||
@@ -107,8 +295,8 @@ const render = (
|
||||
const _debugRenderer = (
|
||||
canvas: HTMLCanvasElement,
|
||||
appState: AppState,
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
scale: number,
|
||||
refresh: () => void,
|
||||
) => {
|
||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||
canvas,
|
||||
@@ -131,6 +319,7 @@ const _debugRenderer = (
|
||||
);
|
||||
|
||||
renderOrigin(context, appState.zoom.value);
|
||||
renderBindings(context, elements, appState.zoom.value);
|
||||
|
||||
if (
|
||||
window.visualDebug?.currentFrame &&
|
||||
@@ -182,10 +371,10 @@ export const debugRenderer = throttleRAF(
|
||||
(
|
||||
canvas: HTMLCanvasElement,
|
||||
appState: AppState,
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
scale: number,
|
||||
refresh: () => void,
|
||||
) => {
|
||||
_debugRenderer(canvas, appState, scale, refresh);
|
||||
_debugRenderer(canvas, appState, elements, scale);
|
||||
},
|
||||
{ trailing: true },
|
||||
);
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from "./random";
|
||||
export * from "./url";
|
||||
export * from "./utils";
|
||||
export * from "./emitter";
|
||||
export * from "./visualdebug";
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ENV,
|
||||
FONT_FAMILY,
|
||||
getFontFamilyFallbacks,
|
||||
isDarwin,
|
||||
isAndroid,
|
||||
isIOS,
|
||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||
@@ -426,19 +425,6 @@ export const allowFullScreen = () =>
|
||||
|
||||
export const exitFullScreen = () => document.exitFullscreen();
|
||||
|
||||
export const getShortcutKey = (shortcut: string): string => {
|
||||
shortcut = shortcut
|
||||
.replace(/\bAlt\b/i, "Alt")
|
||||
.replace(/\bShift\b/i, "Shift")
|
||||
.replace(/\b(Enter|Return)\b/i, "Enter");
|
||||
if (isDarwin) {
|
||||
return shortcut
|
||||
.replace(/\bCtrlOrCmd\b/gi, "Cmd")
|
||||
.replace(/\bAlt\b/i, "Option");
|
||||
}
|
||||
return shortcut.replace(/\bCtrlOrCmd\b/gi, "Ctrl");
|
||||
};
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
isAndroid,
|
||||
isIOS,
|
||||
isMobileOrTablet,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
@@ -326,7 +327,7 @@ export const getTransformHandles = (
|
||||
);
|
||||
};
|
||||
|
||||
export const shouldShowBoundingBox = (
|
||||
export const hasBoundingBox = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
@@ -345,5 +346,7 @@ export const shouldShowBoundingBox = (
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.points.length > 2;
|
||||
// on mobile/tablet we currently don't show bbox because of resize issues
|
||||
// (also prob best for simplicity's sake)
|
||||
return element.points.length > 2 && !isMobileOrTablet();
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element";
|
||||
|
||||
import { getTransformHandles } from "../src/transformHandles";
|
||||
import {
|
||||
getTextEditor,
|
||||
@@ -413,16 +415,12 @@ describe("element binding", () => {
|
||||
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
|
||||
|
||||
// Drag arrow off of bound rectangle range
|
||||
const handles = getTransformHandles(
|
||||
const [elX, elY] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
h.state.zoom,
|
||||
arrayToMap(h.elements),
|
||||
"mouse",
|
||||
).se!;
|
||||
|
||||
-1,
|
||||
h.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
|
||||
const elX = handles[0] + handles[2] / 2;
|
||||
const elY = handles[1] + handles[3] / 2;
|
||||
mouse.downAt(elX, elY);
|
||||
mouse.moveTo(300, 400);
|
||||
mouse.up();
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isFrameLikeElement } from "@excalidraw/element";
|
||||
|
||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||
|
||||
import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||
import { KEYS, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import { alignElements } from "@excalidraw/element";
|
||||
|
||||
@@ -30,6 +30,8 @@ import { t } from "../i18n";
|
||||
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
MIN_ZOOM,
|
||||
THEME,
|
||||
ZOOM_STEP,
|
||||
getShortcutKey,
|
||||
updateActiveTool,
|
||||
CODES,
|
||||
KEYS,
|
||||
@@ -46,6 +45,7 @@ import { t } from "../i18n";
|
||||
import { getNormalizedZoom } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import { isFrameLikeElement } from "@excalidraw/element";
|
||||
|
||||
import { CODES, KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||
|
||||
@@ -26,6 +26,8 @@ import { t } from "../i18n";
|
||||
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { AppClassProperties, AppState } from "../types";
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
KEYS,
|
||||
MOBILE_ACTION_BUTTON_BG,
|
||||
arrayToMap,
|
||||
getShortcutKey,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
@@ -26,6 +25,7 @@ import { DuplicateIcon } from "../components/icons";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
replaceAllElementsInFrame,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { KEYS, randomId, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||
import { KEYS, randomId, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
getSelectedGroupIds,
|
||||
@@ -43,6 +43,8 @@ import { t } from "../i18n";
|
||||
|
||||
import { isSomeElementSelected } from "../scene";
|
||||
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { AppClassProperties, AppState } from "../types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isEmbeddableElement } from "@excalidraw/element";
|
||||
|
||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { ToolButton } from "../components/ToolButton";
|
||||
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
||||
import { LinkIcon } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
randomInteger,
|
||||
arrayToMap,
|
||||
getFontFamilyString,
|
||||
getShortcutKey,
|
||||
getLineHeight,
|
||||
isTransparent,
|
||||
reduceToCommonValue,
|
||||
@@ -142,6 +141,8 @@ import {
|
||||
restoreCaretPosition,
|
||||
} from "../hooks/useTextEditorFocus";
|
||||
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { AppClassProperties, AppState, Primitive } from "../types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KEYS, CODES, getShortcutKey, isDarwin } from "@excalidraw/common";
|
||||
import { KEYS, CODES, isDarwin } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
moveOneLeft,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SendToBackIcon,
|
||||
} from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { isDarwin, getShortcutKey } from "@excalidraw/common";
|
||||
import { isDarwin } from "@excalidraw/common";
|
||||
|
||||
import type { SubtypeOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import type { ActionName } from "./types";
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@ import {
|
||||
wrapEvent,
|
||||
updateObject,
|
||||
updateActiveTool,
|
||||
getShortcutKey,
|
||||
isTransparent,
|
||||
easeToValuesRAF,
|
||||
muteFSAbortError,
|
||||
@@ -173,7 +172,7 @@ import {
|
||||
getContainerElement,
|
||||
isValidTextContainer,
|
||||
redrawTextBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
hasBoundingBox,
|
||||
getFrameChildren,
|
||||
isCursorInFrame,
|
||||
addElementsToFrame,
|
||||
@@ -406,6 +405,8 @@ import { LassoTrail } from "../lasso";
|
||||
|
||||
import { EraserTrail } from "../eraser";
|
||||
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import ConvertElementTypePopup, {
|
||||
getConversionTypeFromElements,
|
||||
convertElementTypePopupAtom,
|
||||
@@ -1801,7 +1802,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
)}
|
||||
<InteractiveCanvas
|
||||
app={this}
|
||||
containerRef={this.excalidrawContainerRef}
|
||||
canvas={this.interactiveCanvas}
|
||||
elementsMap={elementsMap}
|
||||
@@ -5263,7 +5263,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
considerBoundingBox &&
|
||||
this.state.selectedElementIds[element.id] &&
|
||||
shouldShowBoundingBox([element], this.state)
|
||||
hasBoundingBox([element], this.state)
|
||||
) {
|
||||
// if hitting the bounding box, return early
|
||||
// but if not, we should check for other cases as well (e.g. frame name)
|
||||
@@ -6166,7 +6166,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
(!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement.hoverPointIndex === -1) &&
|
||||
this.state.openDialog?.name !== "elementLinkSelector" &&
|
||||
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))
|
||||
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0])) &&
|
||||
// HACK: Disable transform handles for linear elements on mobile until a
|
||||
// better way of showing them is found
|
||||
!(
|
||||
isLinearElement(selectedElements[0]) &&
|
||||
(isMobileOrTablet() || selectedElements[0].points.length === 2)
|
||||
)
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
getElementWithTransformHandleType(
|
||||
@@ -7286,14 +7292,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!this.state.selectedLinearElement?.isEditing &&
|
||||
!isElbowArrow(selectedElements[0]) &&
|
||||
!(
|
||||
isLineElement(selectedElements[0]) &&
|
||||
LinearElementEditor.getPointIndexUnderCursor(
|
||||
selectedElements[0],
|
||||
elementsMap,
|
||||
this.state.zoom,
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
) !== -1
|
||||
isLinearElement(selectedElements[0]) &&
|
||||
(isMobileOrTablet() || selectedElements[0].points.length === 2)
|
||||
) &&
|
||||
!(
|
||||
this.state.selectedLinearElement &&
|
||||
@@ -11205,6 +11205,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return [actionCopy, ...options];
|
||||
}
|
||||
|
||||
const zIndexActions: ContextMenuItems =
|
||||
this.state.stylesPanelMode === "full"
|
||||
? [
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionSendBackward,
|
||||
actionBringForward,
|
||||
actionSendToBack,
|
||||
actionBringToFront,
|
||||
]
|
||||
: [];
|
||||
|
||||
return [
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionCut,
|
||||
@@ -11230,11 +11241,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
actionUngroup,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionAddToLibrary,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionSendBackward,
|
||||
actionBringForward,
|
||||
actionSendToBack,
|
||||
actionBringToFront,
|
||||
...zIndexActions,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionFlipHorizontal,
|
||||
actionFlipVertical,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { getShortcutKey } from "../..//shortcut";
|
||||
import { useAtom } from "../../editor-jotai";
|
||||
import { t } from "../../i18n";
|
||||
import { useDevice } from "../App";
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
EVENT,
|
||||
KEYS,
|
||||
capitalizeString,
|
||||
getShortcutKey,
|
||||
isWritableElement,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||
|
||||
import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
|
||||
|
||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@import "../css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.context-menu-popover {
|
||||
z-index: var(--zIndex-ui-context-menu);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -64,6 +64,7 @@ export const ContextMenu = React.memo(
|
||||
offsetTop={appState.offsetTop}
|
||||
viewportWidth={appState.width}
|
||||
viewportHeight={appState.height}
|
||||
className="context-menu-popover"
|
||||
>
|
||||
<ul
|
||||
className="context-menu"
|
||||
|
||||
@@ -2,11 +2,12 @@ import React from "react";
|
||||
|
||||
import { isDarwin, isFirefox, isWindows } from "@excalidraw/common";
|
||||
|
||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||
import { probablySupportsClipboardBlob } from "../clipboard";
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
|
||||
import { Dialog } from "./Dialog";
|
||||
import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons";
|
||||
|
||||
@@ -28,11 +28,24 @@ $wide-viewport-width: 1000px;
|
||||
> span {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
margin: 0 1px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--color-gray-40);
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.HintViewer {
|
||||
color: var(--color-gray-60);
|
||||
kbd {
|
||||
border-color: var(--color-gray-60);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ import {
|
||||
isTextElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { getShortcutKey } from "@excalidraw/common";
|
||||
|
||||
import { isNodeInFlowchart } from "@excalidraw/element";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
import { isEraserActive } from "../appState";
|
||||
import { isGridModeEnabled } from "../snapping";
|
||||
|
||||
@@ -28,6 +27,11 @@ interface HintViewerProps {
|
||||
app: AppClassProperties;
|
||||
}
|
||||
|
||||
const getTaggedShortcutKey = (key: string | string[]) =>
|
||||
Array.isArray(key)
|
||||
? `<kbd>${key.map(getShortcutKey).join(" + ")}</kbd>`
|
||||
: `<kbd>${getShortcutKey(key)}</kbd>`;
|
||||
|
||||
const getHints = ({
|
||||
appState,
|
||||
isMobile,
|
||||
@@ -42,7 +46,9 @@ const getHints = ({
|
||||
appState.openSidebar.tab === CANVAS_SEARCH_TAB &&
|
||||
appState.searchMatches?.matches.length
|
||||
) {
|
||||
return t("hints.dismissSearch");
|
||||
return t("hints.dismissSearch", {
|
||||
shortcut: getTaggedShortcutKey("Escape"),
|
||||
});
|
||||
}
|
||||
|
||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||
@@ -50,14 +56,21 @@ const getHints = ({
|
||||
}
|
||||
|
||||
if (isEraserActive(appState)) {
|
||||
return t("hints.eraserRevert");
|
||||
return t("hints.eraserRevert", {
|
||||
shortcut: getTaggedShortcutKey("Alt"),
|
||||
});
|
||||
}
|
||||
if (activeTool.type === "arrow" || activeTool.type === "line") {
|
||||
if (multiMode) {
|
||||
return t("hints.linearElementMulti");
|
||||
return t("hints.linearElementMulti", {
|
||||
shortcut_1: getTaggedShortcutKey("Escape"),
|
||||
shortcut_2: getTaggedShortcutKey("Enter"),
|
||||
});
|
||||
}
|
||||
if (activeTool.type === "arrow") {
|
||||
return t("hints.arrowTool", { arrowShortcut: getShortcutKey("A") });
|
||||
return t("hints.arrowTool", {
|
||||
shortcut: getTaggedShortcutKey("A"),
|
||||
});
|
||||
}
|
||||
return t("hints.linearElement");
|
||||
}
|
||||
@@ -83,31 +96,51 @@ const getHints = ({
|
||||
) {
|
||||
const targetElement = selectedElements[0];
|
||||
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
|
||||
return t("hints.lockAngle");
|
||||
return t("hints.lockAngle", {
|
||||
shortcut: getTaggedShortcutKey("Shift"),
|
||||
});
|
||||
}
|
||||
return isImageElement(targetElement)
|
||||
? t("hints.resizeImage")
|
||||
: t("hints.resize");
|
||||
? t("hints.resizeImage", {
|
||||
shortcut_1: getTaggedShortcutKey("Shift"),
|
||||
shortcut_2: getTaggedShortcutKey("Alt"),
|
||||
})
|
||||
: t("hints.resize", {
|
||||
shortcut_1: getTaggedShortcutKey("Shift"),
|
||||
shortcut_2: getTaggedShortcutKey("Alt"),
|
||||
});
|
||||
}
|
||||
|
||||
if (isRotating && lastPointerDownWith === "mouse") {
|
||||
return t("hints.rotate");
|
||||
return t("hints.rotate", {
|
||||
shortcut: getTaggedShortcutKey("Shift"),
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||
return t("hints.text_selected");
|
||||
return t("hints.text_selected", {
|
||||
shortcut: getTaggedShortcutKey("Enter"),
|
||||
});
|
||||
}
|
||||
|
||||
if (appState.editingTextElement) {
|
||||
return t("hints.text_editing");
|
||||
return t("hints.text_editing", {
|
||||
shortcut_1: getTaggedShortcutKey("Escape"),
|
||||
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "Enter"]),
|
||||
});
|
||||
}
|
||||
|
||||
if (appState.croppingElementId) {
|
||||
return t("hints.leaveCropEditor");
|
||||
return t("hints.leaveCropEditor", {
|
||||
shortcut_1: getTaggedShortcutKey("Enter"),
|
||||
shortcut_2: getTaggedShortcutKey("Escape"),
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
|
||||
return t("hints.enterCropEditor");
|
||||
return t("hints.enterCropEditor", {
|
||||
shortcut: getTaggedShortcutKey("Enter"),
|
||||
});
|
||||
}
|
||||
|
||||
if (activeTool.type === "selection") {
|
||||
@@ -117,33 +150,57 @@ const getHints = ({
|
||||
!appState.editingTextElement &&
|
||||
!appState.selectedLinearElement?.isEditing
|
||||
) {
|
||||
return [t("hints.deepBoxSelect")];
|
||||
return t("hints.deepBoxSelect", {
|
||||
shortcut: getTaggedShortcutKey("CtrlOrCmd"),
|
||||
});
|
||||
}
|
||||
|
||||
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
|
||||
return t("hints.disableSnapping");
|
||||
return t("hints.disableSnapping", {
|
||||
shortcut: getTaggedShortcutKey("CtrlOrCmd"),
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedElements.length && !isMobile) {
|
||||
return [t("hints.canvasPanning")];
|
||||
return t("hints.canvasPanning", {
|
||||
shortcut_1: getTaggedShortcutKey(t("keys.mmb")),
|
||||
shortcut_2: getTaggedShortcutKey("Space"),
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedElements.length === 1) {
|
||||
if (isLinearElement(selectedElements[0])) {
|
||||
if (appState.selectedLinearElement?.isEditing) {
|
||||
return appState.selectedLinearElement.selectedPointsIndices
|
||||
? t("hints.lineEditor_pointSelected")
|
||||
: t("hints.lineEditor_nothingSelected");
|
||||
? t("hints.lineEditor_pointSelected", {
|
||||
shortcut_1: getTaggedShortcutKey("Delete"),
|
||||
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "D"]),
|
||||
})
|
||||
: t("hints.lineEditor_nothingSelected", {
|
||||
shortcut_1: getTaggedShortcutKey("Shift"),
|
||||
shortcut_2: getTaggedShortcutKey("Alt"),
|
||||
});
|
||||
}
|
||||
return isLineElement(selectedElements[0])
|
||||
? t("hints.lineEditor_line_info")
|
||||
: t("hints.lineEditor_info");
|
||||
? t("hints.lineEditor_line_info", {
|
||||
shortcut: getTaggedShortcutKey("Enter"),
|
||||
})
|
||||
: t("hints.lineEditor_info", {
|
||||
shortcut_1: getTaggedShortcutKey("CtrlOrCmd"),
|
||||
shortcut_2: getTaggedShortcutKey(["CtrlOrCmd", "Enter"]),
|
||||
});
|
||||
}
|
||||
if (
|
||||
!appState.newElement &&
|
||||
!appState.selectedElementsAreBeingDragged &&
|
||||
isTextBindableContainer(selectedElements[0])
|
||||
) {
|
||||
const bindTextToElement = t("hints.bindTextToElement", {
|
||||
shortcut: getTaggedShortcutKey("Enter"),
|
||||
});
|
||||
const createFlowchart = t("hints.createFlowchart", {
|
||||
shortcut: getTaggedShortcutKey(["CtrlOrCmd", "↑↓"]),
|
||||
});
|
||||
if (isFlowchartNodeElement(selectedElements[0])) {
|
||||
if (
|
||||
isNodeInFlowchart(
|
||||
@@ -151,13 +208,13 @@ const getHints = ({
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
|
||||
return [bindTextToElement, createFlowchart];
|
||||
}
|
||||
|
||||
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
|
||||
return [bindTextToElement, createFlowchart];
|
||||
}
|
||||
|
||||
return t("hints.bindTextToElement");
|
||||
return bindTextToElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,16 +240,21 @@ export const HintViewer = ({
|
||||
}
|
||||
|
||||
const hint = Array.isArray(hints)
|
||||
? hints
|
||||
.map((hint) => {
|
||||
return getShortcutKey(hint).replace(/\. ?$/, "");
|
||||
})
|
||||
.join(". ")
|
||||
: getShortcutKey(hints);
|
||||
? hints.map((hint) => hint.replace(/\. ?$/, "")).join(", ")
|
||||
: hints;
|
||||
|
||||
const hintJSX = hint.split(/(<kbd>[^<]+<\/kbd>)/g).map((part, index) => {
|
||||
if (index % 2 === 1) {
|
||||
const shortcutMatch =
|
||||
part[0] === "<" && part.match(/^<kbd>([^<]+)<\/kbd>$/);
|
||||
return <kbd key={index}>{shortcutMatch ? shortcutMatch[1] : part}</kbd>;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="HintViewer">
|
||||
<span>{hint}</span>
|
||||
<span>{hintJSX}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { unstable_batchedUpdates } from "react-dom";
|
||||
|
||||
import { KEYS, queryFocusableElements } from "@excalidraw/common";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./Popover.scss";
|
||||
|
||||
type Props = {
|
||||
@@ -15,6 +17,7 @@ type Props = {
|
||||
offsetTop?: number;
|
||||
viewportWidth?: number;
|
||||
viewportHeight?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Popover = ({
|
||||
@@ -27,6 +30,7 @@ export const Popover = ({
|
||||
offsetTop = 0,
|
||||
viewportWidth = window.innerWidth,
|
||||
viewportHeight = window.innerHeight,
|
||||
className,
|
||||
}: Props) => {
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -146,7 +150,7 @@ export const Popover = ({
|
||||
}, [onCloseRequest]);
|
||||
|
||||
return (
|
||||
<div className="popover" ref={popoverRef} tabIndex={-1}>
|
||||
<div className={clsx("popover", className)} ref={popoverRef} tabIndex={-1}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getShortcutKey } from "@excalidraw/common";
|
||||
import { getShortcutKey } from "@excalidraw/excalidraw/shortcut";
|
||||
|
||||
export const TTDDialogSubmitShortcut = () => {
|
||||
return (
|
||||
|
||||
@@ -5,15 +5,6 @@ import {
|
||||
isShallowEqual,
|
||||
sceneCoordsToViewportCoords,
|
||||
} from "@excalidraw/common";
|
||||
import { AnimationController } from "@excalidraw/excalidraw/renderer/animation";
|
||||
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
InteractiveSceneRenderAnimationState,
|
||||
InteractiveSceneRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "@excalidraw/excalidraw/scene/types";
|
||||
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
@@ -21,14 +12,15 @@ import type {
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
Device,
|
||||
InteractiveCanvasAppState,
|
||||
} from "../../types";
|
||||
InteractiveCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "../../scene/types";
|
||||
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
||||
import type { DOMAttributes } from "react";
|
||||
|
||||
type InteractiveCanvasProps = {
|
||||
@@ -44,7 +36,6 @@ type InteractiveCanvasProps = {
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderScrollbars: boolean;
|
||||
device: Device;
|
||||
app: AppClassProperties;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
) => void;
|
||||
@@ -79,11 +70,8 @@ type InteractiveCanvasProps = {
|
||||
>;
|
||||
};
|
||||
|
||||
export const INTERACTIVE_SCENE_ANIMATION_KEY = "animateInteractiveScene";
|
||||
|
||||
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
const isComponentMounted = useRef(false);
|
||||
const rendererParams = useRef(null as InteractiveSceneRenderConfig | null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isComponentMounted.current) {
|
||||
@@ -140,61 +128,29 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
)) ||
|
||||
"#6965db";
|
||||
|
||||
rendererParams.current = {
|
||||
app: props.app,
|
||||
canvas: props.canvas,
|
||||
elementsMap: props.elementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
selectedElements: props.selectedElements,
|
||||
allElementsMap: props.allElementsMap,
|
||||
scale: window.devicePixelRatio,
|
||||
appState: props.appState,
|
||||
renderConfig: {
|
||||
remotePointerViewportCoords,
|
||||
remotePointerButton,
|
||||
remoteSelectedElementIds,
|
||||
remotePointerUsernames,
|
||||
remotePointerUserStates,
|
||||
selectionColor,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
},
|
||||
device: props.device,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
animationState: {
|
||||
bindingHighlight: undefined,
|
||||
},
|
||||
deltaTime: 0,
|
||||
};
|
||||
|
||||
if (!AnimationController.running(INTERACTIVE_SCENE_ANIMATION_KEY)) {
|
||||
AnimationController.start<InteractiveSceneRenderAnimationState>(
|
||||
INTERACTIVE_SCENE_ANIMATION_KEY,
|
||||
({ deltaTime, state }) => {
|
||||
const nextAnimationState = renderInteractiveScene(
|
||||
{
|
||||
...rendererParams.current!,
|
||||
deltaTime,
|
||||
animationState: state,
|
||||
},
|
||||
false,
|
||||
).animationState;
|
||||
|
||||
if (nextAnimationState) {
|
||||
for (const key in nextAnimationState) {
|
||||
if (
|
||||
nextAnimationState[
|
||||
key as keyof InteractiveSceneRenderAnimationState
|
||||
] !== undefined
|
||||
) {
|
||||
return nextAnimationState;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
renderInteractiveScene(
|
||||
{
|
||||
canvas: props.canvas,
|
||||
elementsMap: props.elementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
selectedElements: props.selectedElements,
|
||||
allElementsMap: props.allElementsMap,
|
||||
scale: window.devicePixelRatio,
|
||||
appState: props.appState,
|
||||
renderConfig: {
|
||||
remotePointerViewportCoords,
|
||||
remotePointerButton,
|
||||
remoteSelectedElementIds,
|
||||
remotePointerUsernames,
|
||||
remotePointerUserStates,
|
||||
selectionColor,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
},
|
||||
);
|
||||
}
|
||||
device: props.device,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
},
|
||||
isRenderThrottlingEnabled(),
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
--zIndex-eyeDropperPreview: 6;
|
||||
--zIndex-hyperlinkContainer: 7;
|
||||
|
||||
--zIndex-ui-styles-popup: 40;
|
||||
--zIndex-ui-bottom: 60;
|
||||
--zIndex-ui-library: 80;
|
||||
--zIndex-ui-context-menu: 90;
|
||||
--zIndex-ui-styles-popup: 100;
|
||||
--zIndex-ui-top: 100;
|
||||
|
||||
--zIndex-modal: 1000;
|
||||
|
||||
@@ -337,33 +337,33 @@
|
||||
"shapes": "Shapes"
|
||||
},
|
||||
"hints": {
|
||||
"dismissSearch": "Escape to dismiss search",
|
||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||
"dismissSearch": "{{shortcut}} to dismiss search",
|
||||
"canvasPanning": "To move canvas, hold {{shortcut_1}} or {{shortcut_2}} while dragging, or use the hand tool",
|
||||
"linearElement": "Click to start multiple points, drag for single line",
|
||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||
"arrowTool": "Click to start multiple points, drag for single line. Press {{shortcut}} again to change arrow type.",
|
||||
"freeDraw": "Click and drag, release when you're finished",
|
||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||
"embeddable": "Click-drag to create a website embed",
|
||||
"text_selected": "Double-click or press ENTER to edit text",
|
||||
"text_editing": "Press Escape or CtrlOrCmd+ENTER to finish editing",
|
||||
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
|
||||
"lockAngle": "You can constrain angle by holding SHIFT",
|
||||
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
|
||||
"resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center",
|
||||
"rotate": "You can constrain angles by holding SHIFT while rotating",
|
||||
"lineEditor_info": "Hold CtrlOrCmd and Double-click or press CtrlOrCmd + Enter to edit points",
|
||||
"lineEditor_line_info": "Double-click or press Enter to edit points",
|
||||
"lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move",
|
||||
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
|
||||
"text_selected": "Double-click or press {{shortcut}} to edit text",
|
||||
"text_editing": "Press {{shortcut_1}} or {{shortcut_2}} to finish editing",
|
||||
"linearElementMulti": "Click on last point or press {{shortcut_1}} or {{shortcut_2}} to finish",
|
||||
"lockAngle": "You can constrain angle by holding {{shortcut}}",
|
||||
"resize": "You can constrain proportions by holding {{shortcut_1}} while resizing,\nhold {{shortcut_2}} to resize from the center",
|
||||
"resizeImage": "You can resize freely by holding {{shortcut_1}},\nhold {{shortcut_2}} to resize from the center",
|
||||
"rotate": "You can constrain angles by holding {{shortcut}} while rotating",
|
||||
"lineEditor_info": "Hold {{shortcut_1}} and Double-click or press {{shortcut_2}} to edit points",
|
||||
"lineEditor_line_info": "Double-click or press {{shortcut}} to edit points",
|
||||
"lineEditor_pointSelected": "Press {{shortcut_1}} to remove point(s),\n{{shortcut_2}} to duplicate, or drag to move",
|
||||
"lineEditor_nothingSelected": "Select a point to edit (hold {{shortcut_1}} to select multiple),\nor hold {{shortcut_2}} and click to add new points",
|
||||
"publishLibrary": "Publish your own library",
|
||||
"bindTextToElement": "Press enter to add text",
|
||||
"createFlowchart": "Hold CtrlOrCmd and Arrow key to create a flowchart",
|
||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
||||
"eraserRevert": "Hold Alt to revert the elements marked for deletion",
|
||||
"bindTextToElement": "{{shortcut}} to add text",
|
||||
"createFlowchart": "{{shortcut}} to create a flowchart",
|
||||
"deepBoxSelect": "Hold {{shortcut}} to deep select, and to prevent dragging",
|
||||
"eraserRevert": "Hold {{shortcut}} to revert the elements marked for deletion",
|
||||
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
|
||||
"disableSnapping": "Hold CtrlOrCmd to disable snapping",
|
||||
"enterCropEditor": "Double click the image or press ENTER to crop the image",
|
||||
"leaveCropEditor": "Click outside the image or press ENTER or ESCAPE to finish cropping"
|
||||
"disableSnapping": "Hold {{shortcut}} to disable snapping",
|
||||
"enterCropEditor": "Double click the image or press {{shortcut}} to crop the image",
|
||||
"leaveCropEditor": "Click outside the image or press {{shortcut_1}} or {{shortcut_2}} to finish cropping"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Cannot show preview",
|
||||
@@ -648,5 +648,17 @@
|
||||
},
|
||||
"itemNotAvailable": "Command is not available...",
|
||||
"shortcutHint": "For Command palette, use {{shortcut}}"
|
||||
},
|
||||
"keys": {
|
||||
"ctrl": "Ctrl",
|
||||
"option": "Option",
|
||||
"cmd": "Cmd",
|
||||
"alt": "Alt",
|
||||
"escape": "Esc",
|
||||
"enter": "Enter",
|
||||
"shift": "Shift",
|
||||
"spacebar": "Space",
|
||||
"delete": "Delete",
|
||||
"mmb": "Scroll wheel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { isRenderThrottlingEnabled } from "../reactUtils";
|
||||
|
||||
export type Animation<R extends object> = (params: {
|
||||
deltaTime: number;
|
||||
state?: R;
|
||||
}) => R | null | undefined;
|
||||
|
||||
export class AnimationController {
|
||||
private static isRunning = false;
|
||||
private static animations = new Map<
|
||||
string,
|
||||
{
|
||||
animation: Animation<any>;
|
||||
lastTime: number;
|
||||
state: any;
|
||||
}
|
||||
>();
|
||||
|
||||
static start<R extends object>(key: string, animation: Animation<R>) {
|
||||
const initialState = animation({
|
||||
deltaTime: 0,
|
||||
state: undefined,
|
||||
});
|
||||
|
||||
if (initialState) {
|
||||
AnimationController.animations.set(key, {
|
||||
animation,
|
||||
lastTime: 0,
|
||||
state: initialState,
|
||||
});
|
||||
|
||||
if (!AnimationController.isRunning) {
|
||||
AnimationController.isRunning = true;
|
||||
|
||||
if (isRenderThrottlingEnabled()) {
|
||||
requestAnimationFrame(AnimationController.tick);
|
||||
} else {
|
||||
setTimeout(AnimationController.tick, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static tick() {
|
||||
if (AnimationController.animations.size > 0) {
|
||||
for (const [key, animation] of AnimationController.animations) {
|
||||
const now = performance.now();
|
||||
const deltaTime =
|
||||
animation.lastTime === 0 ? 0 : now - animation.lastTime;
|
||||
|
||||
const state = animation.animation({
|
||||
deltaTime,
|
||||
state: animation.state,
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
AnimationController.animations.delete(key);
|
||||
|
||||
if (AnimationController.animations.size === 0) {
|
||||
AnimationController.isRunning = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
animation.lastTime = now;
|
||||
animation.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRenderThrottlingEnabled()) {
|
||||
requestAnimationFrame(AnimationController.tick);
|
||||
} else {
|
||||
setTimeout(AnimationController.tick, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static running(key: string) {
|
||||
return AnimationController.animations.has(key);
|
||||
}
|
||||
|
||||
static cancel(key: string) {
|
||||
AnimationController.animations.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
getOmitSidesForDevice,
|
||||
getTransformHandles,
|
||||
getTransformHandlesFromCoords,
|
||||
shouldShowBoundingBox,
|
||||
hasBoundingBox,
|
||||
} from "@excalidraw/element";
|
||||
import {
|
||||
isElbowArrow,
|
||||
@@ -735,14 +735,7 @@ const _renderInteractiveScene = ({
|
||||
appState,
|
||||
renderConfig,
|
||||
device,
|
||||
animationState,
|
||||
deltaTime,
|
||||
}: InteractiveSceneRenderConfig): {
|
||||
scrollBars?: ReturnType<typeof getScrollBars>;
|
||||
atLeastOneVisibleElement: boolean;
|
||||
elementsMap: RenderableElementsMap;
|
||||
animationState?: typeof animationState;
|
||||
} => {
|
||||
}: InteractiveSceneRenderConfig) => {
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false, elementsMap };
|
||||
}
|
||||
@@ -752,8 +745,6 @@ const _renderInteractiveScene = ({
|
||||
scale,
|
||||
);
|
||||
|
||||
const nextAnimationState = animationState;
|
||||
|
||||
const context = bootstrapCanvas({
|
||||
canvas,
|
||||
scale,
|
||||
@@ -901,7 +892,7 @@ const _renderInteractiveScene = ({
|
||||
|
||||
// Paint selected elements
|
||||
if (!appState.multiElement && !appState.selectedLinearElement?.isEditing) {
|
||||
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
|
||||
const showBoundingBox = hasBoundingBox(selectedElements, appState);
|
||||
|
||||
const isSingleLinearElementSelected =
|
||||
selectedElements.length === 1 && isLinearElement(selectedElements[0]);
|
||||
@@ -1200,7 +1191,6 @@ const _renderInteractiveScene = ({
|
||||
scrollBars,
|
||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||
elementsMap,
|
||||
animationState: nextAnimationState,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -88,12 +88,7 @@ export type StaticSceneRenderConfig = {
|
||||
renderConfig: StaticCanvasRenderConfig;
|
||||
};
|
||||
|
||||
export type InteractiveSceneRenderAnimationState = {
|
||||
bindingHighlight: { runtime: number } | undefined;
|
||||
};
|
||||
|
||||
export type InteractiveSceneRenderConfig = {
|
||||
app: AppClassProperties;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elementsMap: RenderableElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
@@ -104,8 +99,6 @@ export type InteractiveSceneRenderConfig = {
|
||||
renderConfig: InteractiveCanvasRenderConfig;
|
||||
device: Device;
|
||||
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||
animationState?: InteractiveSceneRenderAnimationState;
|
||||
deltaTime: number;
|
||||
};
|
||||
|
||||
export type NewElementSceneRenderConfig = {
|
||||
|
||||
19
packages/excalidraw/shortcut.ts
Normal file
19
packages/excalidraw/shortcut.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isDarwin } from "@excalidraw/common";
|
||||
|
||||
import { t } from "./i18n";
|
||||
|
||||
export const getShortcutKey = (shortcut: string): string =>
|
||||
shortcut
|
||||
.replace(
|
||||
/\b(Opt(?:ion)?|Alt)\b/i,
|
||||
isDarwin ? t("keys.option") : t("keys.alt"),
|
||||
)
|
||||
.replace(/\bShift\b/i, t("keys.shift"))
|
||||
.replace(/\b(Enter|Return)\b/i, t("keys.enter"))
|
||||
.replace(
|
||||
/\b(Ctrl|Cmd|Command|CtrlOrCmd)\b/gi,
|
||||
isDarwin ? t("keys.cmd") : t("keys.ctrl"),
|
||||
)
|
||||
.replace(/\b(Esc(?:ape)?)\b/i, t("keys.escape"))
|
||||
.replace(/\b(Space(?:bar)?)\b/i, t("keys.spacebar"))
|
||||
.replace(/\b(Del(?:ete)?)\b/i, t("keys.delete"));
|
||||
Reference in New Issue
Block a user