Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e163402e22 | ||
|
|
2f9526da24 | ||
|
|
1b6e3fe05b | ||
|
|
afe52c89a7 | ||
|
|
be4e127f6c | ||
|
|
ff0b4394b1 | ||
|
|
7d8b7fc14d | ||
|
|
971b4d4ae6 | ||
|
|
cc4c51996c | ||
|
|
79257a1923 | ||
|
|
dc66261c19 | ||
|
|
273ba803d9 | ||
|
|
301e83805d | ||
|
|
ed5ce8d3de |
@@ -126,6 +126,38 @@ polyfill();
|
|||||||
|
|
||||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface BeforeInstallPromptEventChoiceResult {
|
||||||
|
outcome: "accepted" | "dismissed";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeforeInstallPromptEvent extends Event {
|
||||||
|
prompt(): Promise<void>;
|
||||||
|
userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WindowEventMap {
|
||||||
|
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pwaEvent: BeforeInstallPromptEvent | null = null;
|
||||||
|
|
||||||
|
// Adding a listener outside of the component as it may (?) need to be
|
||||||
|
// subscribed early to catch the event.
|
||||||
|
//
|
||||||
|
// Also note that it will fire only if certain heuristics are met (user has
|
||||||
|
// used the app for some time, etc.)
|
||||||
|
window.addEventListener(
|
||||||
|
"beforeinstallprompt",
|
||||||
|
(event: BeforeInstallPromptEvent) => {
|
||||||
|
// prevent Chrome <= 67 from automatically showing the prompt
|
||||||
|
event.preventDefault();
|
||||||
|
// cache for later use
|
||||||
|
pwaEvent = event;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let isSelfEmbedding = false;
|
let isSelfEmbedding = false;
|
||||||
|
|
||||||
if (window.self !== window.top) {
|
if (window.self !== window.top) {
|
||||||
@@ -1100,6 +1132,21 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("labels.installPWA"),
|
||||||
|
category: DEFAULT_CATEGORIES.app,
|
||||||
|
predicate: () => !!pwaEvent,
|
||||||
|
perform: () => {
|
||||||
|
if (pwaEvent) {
|
||||||
|
pwaEvent.prompt();
|
||||||
|
pwaEvent.userChoice.then(() => {
|
||||||
|
// event cannot be reused, but we'll hopefully
|
||||||
|
// grab new one as the event should be fired again
|
||||||
|
pwaEvent = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "excalidraw-monorepo",
|
"name": "excalidraw-monorepo",
|
||||||
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/excalidraw",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
VERTICAL_ALIGN,
|
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { isTextElement, newElement } from "../element";
|
import { isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
@@ -142,6 +142,7 @@ export const actionBindText = register({
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
autoResize: true,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
@@ -296,6 +297,7 @@ export const actionWrapTextInContainer = register({
|
|||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
boundElements: null,
|
boundElements: null,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
autoResize: true,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
|
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
|
||||||
history.onHistoryChangedEmitter,
|
history.onHistoryChangedEmitter,
|
||||||
new HistoryChangedEvent(),
|
new HistoryChangedEvent(
|
||||||
|
history.isUndoStackEmpty,
|
||||||
|
history.isRedoStackEmpty,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -76,6 +79,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
disabled={isUndoStackEmpty}
|
disabled={isUndoStackEmpty}
|
||||||
|
data-testid="button-undo"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -103,7 +107,10 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const { isRedoStackEmpty } = useEmitter(
|
const { isRedoStackEmpty } = useEmitter(
|
||||||
history.onHistoryChangedEmitter,
|
history.onHistoryChangedEmitter,
|
||||||
new HistoryChangedEvent(),
|
new HistoryChangedEvent(
|
||||||
|
history.isUndoStackEmpty,
|
||||||
|
history.isRedoStackEmpty,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,6 +121,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
disabled={isRedoStackEmpty}
|
disabled={isRedoStackEmpty}
|
||||||
|
data-testid="button-redo"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const offsetElementAfterFontResize = (
|
|||||||
prevElement: ExcalidrawTextElement,
|
prevElement: ExcalidrawTextElement,
|
||||||
nextElement: ExcalidrawTextElement,
|
nextElement: ExcalidrawTextElement,
|
||||||
) => {
|
) => {
|
||||||
if (isBoundToContainer(nextElement)) {
|
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||||
return nextElement;
|
return nextElement;
|
||||||
}
|
}
|
||||||
return mutateElement(
|
return mutateElement(
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { isTextElement } from "../element";
|
||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { measureText } from "../element/textElement";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
import { getFontString } from "../utils";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionTextAutoResize = register({
|
||||||
|
name: "autoResize",
|
||||||
|
label: "labels.autoResize",
|
||||||
|
icon: null,
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState, _: unknown, app: AppClassProperties) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
return (
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isTextElement(selectedElements[0]) &&
|
||||||
|
!selectedElements[0].autoResize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (element.id === selectedElements[0].id && isTextElement(element)) {
|
||||||
|
const metrics = measureText(
|
||||||
|
element.originalText,
|
||||||
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newElementWith(element, {
|
||||||
|
autoResize: true,
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
text: element.originalText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}),
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -134,7 +134,8 @@ export type ActionName =
|
|||||||
| "setEmbeddableAsActiveTool"
|
| "setEmbeddableAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer"
|
| "wrapTextInContainer"
|
||||||
| "commandPalette";
|
| "commandPalette"
|
||||||
|
| "autoResize";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
@@ -468,6 +468,7 @@ export const ExitZenModeAction = ({
|
|||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={clsx("disable-zen-mode", {
|
className={clsx("disable-zen-mode", {
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ import {
|
|||||||
newTextElement,
|
newTextElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
transformElements,
|
transformElements,
|
||||||
updateTextElement,
|
refreshTextDimensions,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
@@ -429,6 +429,7 @@ import {
|
|||||||
isPointHittingLinkIcon,
|
isPointHittingLinkIcon,
|
||||||
} from "./hyperlink/helpers";
|
} from "./hyperlink/helpers";
|
||||||
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../actions/shortcuts";
|
||||||
|
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@@ -714,10 +715,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fonts = new Fonts({
|
this.fonts = new Fonts({ scene: this.scene });
|
||||||
scene: this.scene,
|
|
||||||
onSceneUpdated: this.onSceneUpdated,
|
|
||||||
});
|
|
||||||
this.history = new History();
|
this.history = new History();
|
||||||
|
|
||||||
this.actionManager.registerAll(actions);
|
this.actionManager.registerAll(actions);
|
||||||
@@ -940,7 +938,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.scene.informMutation();
|
this.scene.triggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// GC
|
// GC
|
||||||
@@ -1452,10 +1450,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
const { renderTopRightUI, renderCustomStats } = this.props;
|
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||||
|
|
||||||
const versionNonce = this.scene.getVersionNonce();
|
const sceneNonce = this.scene.getSceneNonce();
|
||||||
const { elementsMap, visibleElements } =
|
const { elementsMap, visibleElements } =
|
||||||
this.renderer.getRenderableElements({
|
this.renderer.getRenderableElements({
|
||||||
versionNonce,
|
sceneNonce,
|
||||||
zoom: this.state.zoom,
|
zoom: this.state.zoom,
|
||||||
offsetLeft: this.state.offsetLeft,
|
offsetLeft: this.state.offsetLeft,
|
||||||
offsetTop: this.state.offsetTop,
|
offsetTop: this.state.offsetTop,
|
||||||
@@ -1673,7 +1671,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
allElementsMap={allElementsMap}
|
allElementsMap={allElementsMap}
|
||||||
visibleElements={visibleElements}
|
visibleElements={visibleElements}
|
||||||
versionNonce={versionNonce}
|
sceneNonce={sceneNonce}
|
||||||
selectionNonce={
|
selectionNonce={
|
||||||
this.state.selectionElement?.versionNonce
|
this.state.selectionElement?.versionNonce
|
||||||
}
|
}
|
||||||
@@ -1695,7 +1693,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
visibleElements={visibleElements}
|
visibleElements={visibleElements}
|
||||||
selectedElements={selectedElements}
|
selectedElements={selectedElements}
|
||||||
versionNonce={versionNonce}
|
sceneNonce={sceneNonce}
|
||||||
selectionNonce={
|
selectionNonce={
|
||||||
this.state.selectionElement?.versionNonce
|
this.state.selectionElement?.versionNonce
|
||||||
}
|
}
|
||||||
@@ -1819,7 +1817,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.magicGenerations.set(frameElement.id, data);
|
this.magicGenerations.set(frameElement.id, data);
|
||||||
this.onSceneUpdated();
|
this.triggerRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
private getTextFromElements(elements: readonly ExcalidrawElement[]) {
|
private getTextFromElements(elements: readonly ExcalidrawElement[]) {
|
||||||
@@ -2444,7 +2442,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.history.record(increment.elementsChange, increment.appStateChange);
|
this.history.record(increment.elementsChange, increment.appStateChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.addCallback(this.onSceneUpdated);
|
this.scene.onUpdate(this.triggerRender);
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
|
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
|
||||||
@@ -2489,6 +2487,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.renderer.destroy();
|
this.renderer.destroy();
|
||||||
this.scene = new Scene();
|
this.scene = new Scene();
|
||||||
|
this.fonts = new Fonts({ scene: this.scene });
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
this.files = {};
|
this.files = {};
|
||||||
this.imageCache.clear();
|
this.imageCache.clear();
|
||||||
@@ -2595,6 +2594,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
addEventListener(window, EVENT.FOCUS, () => {
|
addEventListener(window, EVENT.FOCUS, () => {
|
||||||
this.maybeCleanupAfterMissingPointerUp(null);
|
this.maybeCleanupAfterMissingPointerUp(null);
|
||||||
|
// browsers (chrome?) tend to free up memory a lot, which results
|
||||||
|
// in canvas context being cleared. Thus re-render on focus.
|
||||||
|
this.triggerRender(true);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3670,7 +3672,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.scene.informMutation();
|
this.scene.triggerUpdate();
|
||||||
|
|
||||||
this.addNewImagesToImageCache();
|
this.addNewImagesToImageCache();
|
||||||
},
|
},
|
||||||
@@ -3730,8 +3732,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private onSceneUpdated = () => {
|
private triggerRender = (
|
||||||
|
/** force always re-renders canvas even if no change */
|
||||||
|
force?: boolean,
|
||||||
|
) => {
|
||||||
|
if (force === true) {
|
||||||
|
this.scene.triggerUpdate();
|
||||||
|
} else {
|
||||||
this.setState({});
|
this.setState({});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -4300,25 +4309,22 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||||
|
|
||||||
const updateElement = (
|
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
|
||||||
text: string,
|
|
||||||
originalText: string,
|
|
||||||
isDeleted: boolean,
|
|
||||||
) => {
|
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
// Not sure why we include deleted elements as well hence using deleted elements map
|
// Not sure why we include deleted elements as well hence using deleted elements map
|
||||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||||
if (_element.id === element.id && isTextElement(_element)) {
|
if (_element.id === element.id && isTextElement(_element)) {
|
||||||
return updateTextElement(
|
return newElementWith(_element, {
|
||||||
|
originalText: nextOriginalText,
|
||||||
|
isDeleted: isDeleted ?? _element.isDeleted,
|
||||||
|
// returns (wrapped) text and new dimensions
|
||||||
|
...refreshTextDimensions(
|
||||||
_element,
|
_element,
|
||||||
getContainerElement(_element, elementsMap),
|
getContainerElement(_element, elementsMap),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
{
|
nextOriginalText,
|
||||||
text,
|
),
|
||||||
isDeleted,
|
});
|
||||||
originalText,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return _element;
|
return _element;
|
||||||
}),
|
}),
|
||||||
@@ -4341,15 +4347,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
viewportY - this.state.offsetTop,
|
viewportY - this.state.offsetTop,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
onChange: withBatchedUpdates((text) => {
|
onChange: withBatchedUpdates((nextOriginalText) => {
|
||||||
updateElement(text, text, false);
|
updateElement(nextOriginalText, false);
|
||||||
if (isNonDeletedElement(element)) {
|
if (isNonDeletedElement(element)) {
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, elementsMap);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
const isDeleted = !text.trim();
|
const isDeleted = !nextOriginalText.trim();
|
||||||
updateElement(text, originalText, isDeleted);
|
updateElement(nextOriginalText, isDeleted);
|
||||||
// select the created text element only if submitting via keyboard
|
// select the created text element only if submitting via keyboard
|
||||||
// (when submitting via click it should act as signal to deselect)
|
// (when submitting via click it should act as signal to deselect)
|
||||||
if (!isDeleted && viaKeyboard) {
|
if (!isDeleted && viaKeyboard) {
|
||||||
@@ -4394,7 +4400,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// do an initial update to re-initialize element position since we were
|
// do an initial update to re-initialize element position since we were
|
||||||
// modifying element's x/y for sake of editor (case: syncing to remote)
|
// modifying element's x/y for sake of editor (case: syncing to remote)
|
||||||
updateElement(element.text, element.originalText, false);
|
updateElement(element.originalText, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private deselectElements() {
|
private deselectElements() {
|
||||||
@@ -5101,8 +5107,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.translateCanvas({
|
this.translateCanvas({
|
||||||
zoom: zoomState.zoom,
|
zoom: zoomState.zoom,
|
||||||
scrollX: zoomState.scrollX + deltaX / nextZoom,
|
// 2x multiplier is just a magic number that makes this work correctly
|
||||||
scrollY: zoomState.scrollY + deltaY / nextZoom,
|
// on touchscreen devices (note: if we get report that panning is slower/faster
|
||||||
|
// than actual movement, consider swapping with devicePixelRatio)
|
||||||
|
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
|
||||||
|
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
|
||||||
shouldCacheIgnoreZoom: true,
|
shouldCacheIgnoreZoom: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -5577,7 +5586,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
|
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
|
||||||
this.onSceneUpdated();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -8069,7 +8078,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scene.informMutation();
|
this.scene.triggerUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8564,7 +8573,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private restoreReadyToEraseElements = () => {
|
private restoreReadyToEraseElements = () => {
|
||||||
this.elementsPendingErasure = new Set();
|
this.elementsPendingErasure = new Set();
|
||||||
this.onSceneUpdated();
|
this.triggerRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
private eraseElements = () => {
|
private eraseElements = () => {
|
||||||
@@ -8978,7 +8987,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
files,
|
files,
|
||||||
);
|
);
|
||||||
if (updatedFiles.size) {
|
if (updatedFiles.size) {
|
||||||
this.scene.informMutation();
|
this.scene.triggerUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -9633,6 +9642,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionCut,
|
actionCut,
|
||||||
actionCopy,
|
actionCopy,
|
||||||
actionPaste,
|
actionPaste,
|
||||||
@@ -9645,6 +9655,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionPasteStyles,
|
actionPasteStyles,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionGroup,
|
actionGroup,
|
||||||
|
actionTextAutoResize,
|
||||||
actionUnbindText,
|
actionUnbindText,
|
||||||
actionBindText,
|
actionBindText,
|
||||||
actionWrapTextInContainer,
|
actionWrapTextInContainer,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const ButtonIconSelect = <T extends Object>(
|
|||||||
{props.options.map((option) =>
|
{props.options.map((option) =>
|
||||||
props.type === "button" ? (
|
props.type === "button" ? (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
key={option.text}
|
key={option.text}
|
||||||
onClick={(event) => props.onClick(option.value, event)}
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
className={clsx({
|
className={clsx({
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ export const CheckboxItem: React.FC<{
|
|||||||
).focus();
|
).focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="Checkbox-box"
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
>
|
||||||
{checkIcon}
|
{checkIcon}
|
||||||
</button>
|
</button>
|
||||||
<div className="Checkbox-label">{children}</div>
|
<div className="Checkbox-label">{children}</div>
|
||||||
|
|||||||
@@ -540,7 +540,7 @@ function CommandPaletteInner({
|
|||||||
...command,
|
...command,
|
||||||
icon: command.icon || boltIcon,
|
icon: command.icon || boltIcon,
|
||||||
order: command.order ?? getCategoryOrder(command.category),
|
order: command.order ?? getCategoryOrder(command.category),
|
||||||
haystack: `${deburr(command.label)} ${
|
haystack: `${deburr(command.label.toLocaleLowerCase())} ${
|
||||||
command.keywords?.join(" ") || ""
|
command.keywords?.join(" ") || ""
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
@@ -777,7 +777,9 @@ function CommandPaletteInner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _query = deburr(commandSearch.replace(/[<>-_| ]/g, ""));
|
const _query = deburr(
|
||||||
|
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, ""),
|
||||||
|
);
|
||||||
matchingCommands = fuzzy
|
matchingCommands = fuzzy
|
||||||
.filter(_query, matchingCommands, {
|
.filter(_query, matchingCommands, {
|
||||||
extract: (command) => command.haystack,
|
extract: (command) => command.haystack,
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const ContextMenu = React.memo(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={clsx("context-menu-item", {
|
className={clsx("context-menu-item", {
|
||||||
dangerous: actionName === "deleteSelectedElements",
|
dangerous: actionName === "deleteSelectedElements",
|
||||||
checkmark: item.checked?.(appState),
|
checkmark: item.checked?.(appState),
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
title={t("buttons.close")}
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{CloseIcon}
|
{CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ const FollowMode = ({
|
|||||||
{userToFollow.username}
|
{userToFollow.username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onDisconnect}
|
||||||
|
className="follow-mode__disconnect-btn"
|
||||||
|
>
|
||||||
{CloseIcon}
|
{CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ function Picker<T>({
|
|||||||
<div className="picker-content" ref={rGallery}>
|
<div className="picker-content" ref={rGallery}>
|
||||||
{options.map((option, i) => (
|
{options.map((option, i) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={clsx("picker-option", {
|
className={clsx("picker-option", {
|
||||||
active: value === option.value,
|
active: value === option.value,
|
||||||
})}
|
})}
|
||||||
@@ -171,6 +172,7 @@ export function IconPicker<T>({
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
name={group}
|
name={group}
|
||||||
|
type="button"
|
||||||
className={isActive ? "active" : ""}
|
className={isActive ? "active" : ""}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={() => setActive(!isActive)}
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ const LayerUI = ({
|
|||||||
);
|
);
|
||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
Scene.getScene(selectedElements[0])?.informMutation();
|
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
||||||
} else if (colorPickerType === "elementBackground") {
|
} else if (colorPickerType === "elementBackground") {
|
||||||
setAppState({
|
setAppState({
|
||||||
currentItemBackgroundColor: color,
|
currentItemBackgroundColor: color,
|
||||||
@@ -555,6 +555,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const MobileMenu = ({
|
|||||||
!appState.openMenu &&
|
!appState.openMenu &&
|
||||||
!appState.openSidebar && (
|
!appState.openSidebar && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const ChartPreviewBtn = (props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="ChartPreview"
|
className="ChartPreview"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (chartElements) {
|
if (chartElements) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type InteractiveCanvasProps = {
|
|||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
sceneNonce: number | undefined;
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
@@ -206,10 +206,10 @@ const areEqual = (
|
|||||||
// This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
|
// This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
|
||||||
if (
|
if (
|
||||||
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.sceneNonce !== nextProps.sceneNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on elementsMap because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if sceneNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ type StaticCanvasProps = {
|
|||||||
elementsMap: RenderableElementsMap;
|
elementsMap: RenderableElementsMap;
|
||||||
allElementsMap: NonDeletedSceneElementsMap;
|
allElementsMap: NonDeletedSceneElementsMap;
|
||||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
versionNonce: number | undefined;
|
sceneNonce: number | undefined;
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: StaticCanvasAppState;
|
appState: StaticCanvasAppState;
|
||||||
@@ -112,10 +112,10 @@ const areEqual = (
|
|||||||
nextProps: StaticCanvasProps,
|
nextProps: StaticCanvasProps,
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
prevProps.sceneNonce !== nextProps.sceneNonce ||
|
||||||
prevProps.scale !== nextProps.scale ||
|
prevProps.scale !== nextProps.scale ||
|
||||||
// we need to memoize on elementsMap because they may have renewed
|
// we need to memoize on elementsMap because they may have renewed
|
||||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
// even if sceneNonce didn't change (e.g. we filter elements out based
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements
|
prevProps.visibleElements !== nextProps.visibleElements
|
||||||
|
|||||||
@@ -698,14 +698,18 @@ export const BringForwardIcon = createIcon(arrownNarrowUpJSX, tablerIconProps);
|
|||||||
|
|
||||||
export const SendBackwardIcon = createIcon(arrownNarrowUpJSX, {
|
export const SendBackwardIcon = createIcon(arrownNarrowUpJSX, {
|
||||||
...tablerIconProps,
|
...tablerIconProps,
|
||||||
transform: "rotate(180)",
|
style: {
|
||||||
|
transform: "rotate(180deg)",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BringToFrontIcon = createIcon(arrowBarToTopJSX, tablerIconProps);
|
export const BringToFrontIcon = createIcon(arrowBarToTopJSX, tablerIconProps);
|
||||||
|
|
||||||
export const SendToBackIcon = createIcon(arrowBarToTopJSX, {
|
export const SendToBackIcon = createIcon(arrowBarToTopJSX, {
|
||||||
...tablerIconProps,
|
...tablerIconProps,
|
||||||
transform: "rotate(180)",
|
style: {
|
||||||
|
transform: "rotate(180deg)",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
@@ -273,6 +274,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
@@ -378,6 +380,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id48",
|
"containerId": "id48",
|
||||||
@@ -478,6 +481,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id37",
|
"containerId": "id37",
|
||||||
@@ -652,6 +656,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id41",
|
"containerId": "id41",
|
||||||
@@ -692,6 +697,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
@@ -737,6 +743,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
|
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [
|
"boundElements": [
|
||||||
{
|
{
|
||||||
@@ -1194,6 +1201,7 @@ exports[`Test Transform > should transform regular shapes 6`] = `
|
|||||||
exports[`Test Transform > should transform text element 1`] = `
|
exports[`Test Transform > should transform text element 1`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
@@ -1234,6 +1242,7 @@ exports[`Test Transform > should transform text element 1`] = `
|
|||||||
exports[`Test Transform > should transform text element 2`] = `
|
exports[`Test Transform > should transform text element 2`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
@@ -1566,6 +1575,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 7`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 7`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "B",
|
"containerId": "B",
|
||||||
@@ -1608,6 +1618,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 8`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 8`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "A",
|
"containerId": "A",
|
||||||
@@ -1650,6 +1661,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 9`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 9`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "Alice",
|
"containerId": "Alice",
|
||||||
@@ -1692,6 +1704,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 10`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 10`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "Bob",
|
"containerId": "Bob",
|
||||||
@@ -1734,6 +1747,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 11`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 11`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "Bob_Alice",
|
"containerId": "Bob_Alice",
|
||||||
@@ -1774,6 +1788,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
exports[`Test Transform > should transform the elements correctly when linear elements have single point 12`] = `
|
exports[`Test Transform > should transform the elements correctly when linear elements have single point 12`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "Bob_B",
|
"containerId": "Bob_B",
|
||||||
@@ -2022,6 +2037,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 5`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 5`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id25",
|
"containerId": "id25",
|
||||||
@@ -2062,6 +2078,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 6`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 6`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id26",
|
"containerId": "id26",
|
||||||
@@ -2102,6 +2119,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 7`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 7`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id27",
|
"containerId": "id27",
|
||||||
@@ -2143,6 +2161,7 @@ LABELLED ARROW",
|
|||||||
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 8`] = `
|
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 8`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id28",
|
"containerId": "id28",
|
||||||
@@ -2406,6 +2425,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 7`] = `
|
exports[`Test Transform > should transform to text containers when label provided 7`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id13",
|
"containerId": "id13",
|
||||||
@@ -2446,6 +2466,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 8`] = `
|
exports[`Test Transform > should transform to text containers when label provided 8`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id14",
|
"containerId": "id14",
|
||||||
@@ -2487,6 +2508,7 @@ CONTAINER",
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 9`] = `
|
exports[`Test Transform > should transform to text containers when label provided 9`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id15",
|
"containerId": "id15",
|
||||||
@@ -2530,6 +2552,7 @@ CONTAINER",
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 10`] = `
|
exports[`Test Transform > should transform to text containers when label provided 10`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id16",
|
"containerId": "id16",
|
||||||
@@ -2571,6 +2594,7 @@ TEXT CONTAINER",
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 11`] = `
|
exports[`Test Transform > should transform to text containers when label provided 11`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id17",
|
"containerId": "id17",
|
||||||
@@ -2613,6 +2637,7 @@ CONTAINER",
|
|||||||
exports[`Test Transform > should transform to text containers when label provided 12`] = `
|
exports[`Test Transform > should transform to text containers when label provided 12`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"containerId": "id18",
|
"containerId": "id18",
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ const restoreElementWithProperties = <
|
|||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
groupIds: element.groupIds ?? [],
|
groupIds: element.groupIds ?? [],
|
||||||
frameId: element.frameId ?? null,
|
frameId: element.frameId ?? null,
|
||||||
roundness: element.roundness
|
roundness: typeof element.roundness !== "undefined"
|
||||||
? element.roundness
|
? element.roundness
|
||||||
: element.strokeSharpness === "round"
|
: element.strokeSharpness === "round"
|
||||||
? {
|
? {
|
||||||
@@ -208,7 +208,7 @@ const restoreElement = (
|
|||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
originalText: element.originalText || text,
|
originalText: element.originalText || text,
|
||||||
|
autoResize: element.autoResize ?? true,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { isLinearElementType } from "./typeChecks";
|
|||||||
export {
|
export {
|
||||||
newElement,
|
newElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
updateTextElement,
|
|
||||||
refreshTextDimensions,
|
refreshTextDimensions,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
element.updated = getUpdatedTimestamp();
|
element.updated = getUpdatedTimestamp();
|
||||||
|
|
||||||
if (informMutation) {
|
if (informMutation) {
|
||||||
Scene.getScene(element)?.informMutation();
|
Scene.getScene(element)?.triggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
@@ -107,6 +107,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
export const newElementWith = <TElement extends ExcalidrawElement>(
|
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
|
/** pass `true` to always regenerate */
|
||||||
|
force = false,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
@@ -123,7 +125,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!didChange) {
|
if (!didChange && !force) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,8 +240,7 @@ export const newTextElement = (
|
|||||||
metrics,
|
metrics,
|
||||||
);
|
);
|
||||||
|
|
||||||
const textElement = newElementWith(
|
const textElementProps: ExcalidrawTextElement = {
|
||||||
{
|
|
||||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||||
text,
|
text,
|
||||||
fontSize,
|
fontSize,
|
||||||
@@ -254,10 +253,15 @@ export const newTextElement = (
|
|||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
|
autoResize: true,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const textElement: ExcalidrawTextElement = newElementWith(
|
||||||
|
textElementProps,
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return textElement;
|
return textElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,18 +275,25 @@ const getAdjustedDimensions = (
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
} => {
|
} => {
|
||||||
const { width: nextWidth, height: nextHeight } = measureText(
|
let { width: nextWidth, height: nextHeight } = measureText(
|
||||||
nextText,
|
nextText,
|
||||||
getFontString(element),
|
getFontString(element),
|
||||||
element.lineHeight,
|
element.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// wrapped text
|
||||||
|
if (!element.autoResize) {
|
||||||
|
nextWidth = element.width;
|
||||||
|
}
|
||||||
|
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
if (
|
if (
|
||||||
textAlign === "center" &&
|
textAlign === "center" &&
|
||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId &&
|
||||||
|
element.autoResize
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureText(
|
||||||
element.text,
|
element.text,
|
||||||
@@ -343,38 +354,19 @@ export const refreshTextDimensions = (
|
|||||||
if (textElement.isDeleted) {
|
if (textElement.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (container) {
|
if (container || !textElement.autoResize) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
text,
|
text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
getBoundTextMaxWidth(container, textElement),
|
container
|
||||||
|
? getBoundTextMaxWidth(container, textElement)
|
||||||
|
: textElement.width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
|
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
|
||||||
return { text, ...dimensions };
|
return { text, ...dimensions };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateTextElement = (
|
|
||||||
textElement: ExcalidrawTextElement,
|
|
||||||
container: ExcalidrawTextContainer | null,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
{
|
|
||||||
text,
|
|
||||||
isDeleted,
|
|
||||||
originalText,
|
|
||||||
}: {
|
|
||||||
text: string;
|
|
||||||
isDeleted?: boolean;
|
|
||||||
originalText: string;
|
|
||||||
},
|
|
||||||
): ExcalidrawTextElement => {
|
|
||||||
return newElementWith(textElement, {
|
|
||||||
originalText,
|
|
||||||
isDeleted: isDeleted ?? textElement.isDeleted,
|
|
||||||
...refreshTextDimensions(textElement, container, elementsMap, originalText),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newFreeDrawElement = (
|
export const newFreeDrawElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: "freedraw";
|
type: "freedraw";
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import {
|
||||||
|
BOUND_TEXT_PADDING,
|
||||||
|
MIN_FONT_SIZE,
|
||||||
|
SHIFT_LOCKING_ANGLE,
|
||||||
|
} from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import { rotate, centerPoint, rotatePoint } from "../math";
|
import { rotate, centerPoint, rotatePoint } from "../math";
|
||||||
@@ -45,6 +49,9 @@ import {
|
|||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
|
wrapText,
|
||||||
|
measureText,
|
||||||
|
getMinCharWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { isInGroup } from "../groups";
|
import { isInGroup } from "../groups";
|
||||||
@@ -84,14 +91,9 @@ export const transformElements = (
|
|||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, elementsMap);
|
||||||
} else if (
|
} else if (isTextElement(element) && transformHandleType) {
|
||||||
isTextElement(element) &&
|
|
||||||
(transformHandleType === "nw" ||
|
|
||||||
transformHandleType === "ne" ||
|
|
||||||
transformHandleType === "sw" ||
|
|
||||||
transformHandleType === "se")
|
|
||||||
) {
|
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
|
originalElements,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
@@ -223,9 +225,10 @@ const measureFontSizeFromWidth = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
|
originalElements: PointerDownState["originalElements"],
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
@@ -245,6 +248,7 @@ const resizeSingleTextElement = (
|
|||||||
let scaleX = 0;
|
let scaleX = 0;
|
||||||
let scaleY = 0;
|
let scaleY = 0;
|
||||||
|
|
||||||
|
if (transformHandleType !== "e" && transformHandleType !== "w") {
|
||||||
if (transformHandleType.includes("e")) {
|
if (transformHandleType.includes("e")) {
|
||||||
scaleX = (rotatedX - x1) / (x2 - x1);
|
scaleX = (rotatedX - x1) / (x2 - x1);
|
||||||
}
|
}
|
||||||
@@ -257,6 +261,7 @@ const resizeSingleTextElement = (
|
|||||||
if (transformHandleType.includes("s")) {
|
if (transformHandleType.includes("s")) {
|
||||||
scaleY = (rotatedY - y1) / (y2 - y1);
|
scaleY = (rotatedY - y1) / (y2 - y1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const scale = Math.max(scaleX, scaleY);
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
@@ -318,6 +323,102 @@ const resizeSingleTextElement = (
|
|||||||
y: nextY,
|
y: nextY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transformHandleType === "e" || transformHandleType === "w") {
|
||||||
|
const stateAtResizeStart = originalElements.get(element.id)!;
|
||||||
|
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
||||||
|
stateAtResizeStart,
|
||||||
|
stateAtResizeStart.width,
|
||||||
|
stateAtResizeStart.height,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const startTopLeft: Point = [x1, y1];
|
||||||
|
const startBottomRight: Point = [x2, y2];
|
||||||
|
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
|
||||||
|
|
||||||
|
const rotatedPointer = rotatePoint(
|
||||||
|
[pointerX, pointerY],
|
||||||
|
startCenter,
|
||||||
|
-stateAtResizeStart.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [esx1, , esx2] = getResizedElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const boundsCurrentWidth = esx2 - esx1;
|
||||||
|
|
||||||
|
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
|
||||||
|
const minWidth =
|
||||||
|
getMinCharWidth(getFontString(element)) + BOUND_TEXT_PADDING * 2;
|
||||||
|
|
||||||
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
|
|
||||||
|
if (transformHandleType.includes("e")) {
|
||||||
|
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||||
|
}
|
||||||
|
if (transformHandleType.includes("w")) {
|
||||||
|
scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth =
|
||||||
|
element.width * scaleX < minWidth ? minWidth : element.width * scaleX;
|
||||||
|
|
||||||
|
const text = wrapText(
|
||||||
|
element.originalText,
|
||||||
|
getFontString(element),
|
||||||
|
Math.abs(newWidth),
|
||||||
|
);
|
||||||
|
const metrics = measureText(
|
||||||
|
text,
|
||||||
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const eleNewHeight = metrics.height;
|
||||||
|
|
||||||
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||||
|
getResizedElementAbsoluteCoords(
|
||||||
|
stateAtResizeStart,
|
||||||
|
newWidth,
|
||||||
|
eleNewHeight,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||||
|
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||||
|
|
||||||
|
let newTopLeft = [...startTopLeft] as [number, number];
|
||||||
|
if (["n", "w", "nw"].includes(transformHandleType)) {
|
||||||
|
newTopLeft = [
|
||||||
|
startBottomRight[0] - Math.abs(newBoundsWidth),
|
||||||
|
startTopLeft[1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust topLeft to new rotation point
|
||||||
|
const angle = stateAtResizeStart.angle;
|
||||||
|
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
|
||||||
|
const newCenter: Point = [
|
||||||
|
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
|
||||||
|
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
|
||||||
|
];
|
||||||
|
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
|
||||||
|
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
|
||||||
|
|
||||||
|
const resizedElement: Partial<ExcalidrawTextElement> = {
|
||||||
|
width: Math.abs(newWidth),
|
||||||
|
height: Math.abs(metrics.height),
|
||||||
|
x: newTopLeft[0],
|
||||||
|
y: newTopLeft[1],
|
||||||
|
text,
|
||||||
|
autoResize: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
mutateElement(element, resizedElement);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizeSingleElement = (
|
export const resizeSingleElement = (
|
||||||
@@ -876,7 +977,7 @@ export const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Scene.getScene(elementsAndUpdates[0].element)?.informMutation();
|
Scene.getScene(elementsAndUpdates[0].element)?.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
@@ -938,7 +1039,7 @@ const rotateMultipleElements = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Scene.getScene(elements[0])?.informMutation();
|
Scene.getScene(elements[0])?.triggerUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getResizeOffsetXY = (
|
export const getResizeOffsetXY = (
|
||||||
|
|||||||
@@ -87,12 +87,8 @@ export const resizeTest = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note that for a text element, when "resized" from the side
|
// do not resize from the sides for linear elements with only two points
|
||||||
// we should make it wrap/unwrap
|
if (!(isLinearElement(element) && element.points.length <= 2)) {
|
||||||
if (
|
|
||||||
element.type !== "text" &&
|
|
||||||
!(isLinearElement(element) && element.points.length <= 2)
|
|
||||||
) {
|
|
||||||
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
|
||||||
const sides = getSelectionBorders(
|
const sides = getSelectionBorders(
|
||||||
[x1 - SPACING, y1 - SPACING],
|
[x1 - SPACING, y1 - SPACING],
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const redrawTextBoundingBox = (
|
|||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
informMutation: boolean = true,
|
informMutation = true,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
const boundTextUpdates = {
|
const boundTextUpdates = {
|
||||||
@@ -62,21 +62,27 @@ export const redrawTextBoundingBox = (
|
|||||||
|
|
||||||
boundTextUpdates.text = textElement.text;
|
boundTextUpdates.text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container || !textElement.autoResize) {
|
||||||
maxWidth = getBoundTextMaxWidth(container, textElement);
|
maxWidth = container
|
||||||
|
? getBoundTextMaxWidth(container, textElement)
|
||||||
|
: textElement.width;
|
||||||
boundTextUpdates.text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
boundTextUpdates.text,
|
boundTextUpdates.text,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
textElement.lineHeight,
|
textElement.lineHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
|
||||||
|
if (textElement.autoResize) {
|
||||||
boundTextUpdates.width = metrics.width;
|
boundTextUpdates.width = metrics.width;
|
||||||
|
}
|
||||||
boundTextUpdates.height = metrics.height;
|
boundTextUpdates.height = metrics.height;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
|
|||||||
@@ -236,6 +236,117 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Test text wrapping", () => {
|
||||||
|
const { h } = window;
|
||||||
|
const dimensions = { height: 400, width: 800 };
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockBoundingClientRect(dimensions);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
// @ts-ignore
|
||||||
|
h.app.refreshViewportBreakpoints();
|
||||||
|
// @ts-ignore
|
||||||
|
h.app.refreshEditorBreakpoints();
|
||||||
|
|
||||||
|
h.elements = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
restoreOriginalGetBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep width when editing a wrapped text", async () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "Excalidraw\nEditor",
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [text];
|
||||||
|
|
||||||
|
const prevWidth = text.width;
|
||||||
|
const prevHeight = text.height;
|
||||||
|
const prevText = text.text;
|
||||||
|
|
||||||
|
// text is wrapped
|
||||||
|
UI.resize(text, "e", [-20, 0]);
|
||||||
|
expect(text.width).not.toEqual(prevWidth);
|
||||||
|
expect(text.height).not.toEqual(prevHeight);
|
||||||
|
expect(text.text).not.toEqual(prevText);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
const wrappedWidth = text.width;
|
||||||
|
const wrappedHeight = text.height;
|
||||||
|
const wrappedText = text.text;
|
||||||
|
|
||||||
|
// edit text
|
||||||
|
UI.clickTool("selection");
|
||||||
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
|
const editor = await getTextEditor(textEditorSelector);
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
|
const nextText = `${wrappedText} is great!`;
|
||||||
|
updateTextEditor(editor, nextText);
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||||
|
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
|
||||||
|
|
||||||
|
// remove all texts and then add it back editing
|
||||||
|
updateTextEditor(editor, "");
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
updateTextEditor(editor, nextText);
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should restore original text after unwrapping a wrapped text", async () => {
|
||||||
|
const originalText = "Excalidraw\neditor\nis great!";
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: originalText,
|
||||||
|
});
|
||||||
|
h.elements = [text];
|
||||||
|
|
||||||
|
// wrap
|
||||||
|
UI.resize(text, "e", [-40, 0]);
|
||||||
|
// enter text editing mode
|
||||||
|
UI.clickTool("selection");
|
||||||
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
|
const editor = await getTextEditor(textEditorSelector);
|
||||||
|
editor.blur();
|
||||||
|
// restore after unwrapping
|
||||||
|
UI.resize(text, "e", [40, 0]);
|
||||||
|
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
|
||||||
|
|
||||||
|
// wrap again and add a new line
|
||||||
|
UI.resize(text, "e", [-30, 0]);
|
||||||
|
const wrappedText = text.text;
|
||||||
|
UI.clickTool("selection");
|
||||||
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
|
updateTextEditor(editor, `${wrappedText}\nA new line!`);
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
editor.blur();
|
||||||
|
// remove the newly added line
|
||||||
|
UI.clickTool("selection");
|
||||||
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
|
updateTextEditor(editor, wrappedText);
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
editor.blur();
|
||||||
|
// unwrap
|
||||||
|
UI.resize(text, "e", [30, 0]);
|
||||||
|
// expect the text to be restored the same
|
||||||
|
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const dimensions = { height: 400, width: 800 };
|
const dimensions = { height: 400, width: 800 };
|
||||||
@@ -800,26 +911,15 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
||||||
UI.clickTool("text");
|
|
||||||
|
|
||||||
mouse.clickAt(
|
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
);
|
|
||||||
mouse.down();
|
|
||||||
editor = await getTextEditor(textEditorSelector, true);
|
|
||||||
|
|
||||||
editor.select();
|
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
editor.blur();
|
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Cascadia);
|
).toEqual(FONT_FAMILY.Cascadia);
|
||||||
|
|||||||
@@ -79,12 +79,14 @@ export const textWysiwyg = ({
|
|||||||
app,
|
app,
|
||||||
}: {
|
}: {
|
||||||
id: ExcalidrawElement["id"];
|
id: ExcalidrawElement["id"];
|
||||||
onChange?: (text: string) => void;
|
/**
|
||||||
onSubmit: (data: {
|
* textWysiwyg only deals with `originalText`
|
||||||
text: string;
|
*
|
||||||
viaKeyboard: boolean;
|
* Note: `text`, which can be wrapped and therefore different from `originalText`,
|
||||||
originalText: string;
|
* is derived from `originalText`
|
||||||
}) => void;
|
*/
|
||||||
|
onChange?: (nextOriginalText: string) => void;
|
||||||
|
onSubmit: (data: { viaKeyboard: boolean; nextOriginalText: string }) => void;
|
||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawTextElement;
|
element: ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
@@ -129,11 +131,8 @@ export const textWysiwyg = ({
|
|||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
let textElementWidth = updatedTextElement.width;
|
let textElementWidth = updatedTextElement.width;
|
||||||
// Set to element height by default since that's
|
|
||||||
// what is going to be used for unbounded text
|
|
||||||
const textElementHeight = updatedTextElement.height;
|
const textElementHeight = updatedTextElement.height;
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
@@ -226,6 +225,8 @@ export const textWysiwyg = ({
|
|||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
|
} else {
|
||||||
|
textElementWidth += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
@@ -260,6 +261,7 @@ export const textWysiwyg = ({
|
|||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -276,7 +278,7 @@ export const textWysiwyg = ({
|
|||||||
let whiteSpace = "pre";
|
let whiteSpace = "pre";
|
||||||
let wordBreak = "normal";
|
let wordBreak = "normal";
|
||||||
|
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element) || !element.autoResize) {
|
||||||
whiteSpace = "pre-wrap";
|
whiteSpace = "pre-wrap";
|
||||||
wordBreak = "break-word";
|
wordBreak = "break-word";
|
||||||
}
|
}
|
||||||
@@ -499,14 +501,12 @@ export const textWysiwyg = ({
|
|||||||
if (!updateElement) {
|
if (!updateElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let text = editable.value;
|
|
||||||
const container = getContainerElement(
|
const container = getContainerElement(
|
||||||
updateElement,
|
updateElement,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
text = updateElement.text;
|
|
||||||
if (editable.value.trim()) {
|
if (editable.value.trim()) {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||||
@@ -538,9 +538,8 @@ export const textWysiwyg = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
text,
|
|
||||||
viaKeyboard: submittedViaKeyboard,
|
viaKeyboard: submittedViaKeyboard,
|
||||||
originalText: editable.value,
|
nextOriginalText: editable.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -644,7 +643,7 @@ export const textWysiwyg = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// handle updates of textElement properties of editing element
|
// handle updates of textElement properties of editing element
|
||||||
const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
|
const unbindUpdate = Scene.getScene(element)!.onUpdate(() => {
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
const isColorPickerActive = !!document.activeElement?.closest(
|
const isColorPickerActive = !!document.activeElement?.closest(
|
||||||
".color-picker-content",
|
".color-picker-content",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { Bounds } from "./bounds";
|
|||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
|
||||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
@@ -65,13 +64,6 @@ export const OMIT_SIDES_FOR_FRAME = {
|
|||||||
rotation: true,
|
rotation: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
|
|
||||||
e: true,
|
|
||||||
s: true,
|
|
||||||
n: true,
|
|
||||||
w: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const OMIT_SIDES_FOR_LINE_SLASH = {
|
const OMIT_SIDES_FOR_LINE_SLASH = {
|
||||||
e: true,
|
e: true,
|
||||||
s: true,
|
s: true,
|
||||||
@@ -290,8 +282,6 @@ export const getTransformHandles = (
|
|||||||
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isTextElement(element)) {
|
|
||||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
|
||||||
} else if (isFrameLikeElement(element)) {
|
} else if (isFrameLikeElement(element)) {
|
||||||
omitSides = {
|
omitSides = {
|
||||||
...omitSides,
|
...omitSides,
|
||||||
|
|||||||
@@ -193,6 +193,13 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId: ExcalidrawGenericElement["id"] | null;
|
containerId: ExcalidrawGenericElement["id"] | null;
|
||||||
originalText: string;
|
originalText: string;
|
||||||
|
/**
|
||||||
|
* If `true` the width will fit the text. If `false`, the text will
|
||||||
|
* wrap to fit the width.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoResize: boolean;
|
||||||
/**
|
/**
|
||||||
* Unitless line height (aligned to W3C). To get line height in px, multiply
|
* Unitless line height (aligned to W3C). To get line height in px, multiply
|
||||||
* with font size (using `getLineHeightInPx` helper).
|
* with font size (using `getLineHeightInPx` helper).
|
||||||
|
|||||||
@@ -148,7 +148,9 @@
|
|||||||
"discordChat": "Discord chat",
|
"discordChat": "Discord chat",
|
||||||
"zoomToFitViewport": "Zoom to fit in viewport",
|
"zoomToFitViewport": "Zoom to fit in viewport",
|
||||||
"zoomToFitSelection": "Zoom to fit selection",
|
"zoomToFitSelection": "Zoom to fit selection",
|
||||||
"zoomToFit": "Zoom to fit all elements"
|
"zoomToFit": "Zoom to fit all elements",
|
||||||
|
"installPWA": "Install Excalidraw locally (PWA)",
|
||||||
|
"autoResize": "Enable text auto-resizing"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "No items added yet...",
|
"noItems": "No items added yet...",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
"@excalidraw/mermaid-to-excalidraw": "0.3.0",
|
"@excalidraw/mermaid-to-excalidraw": "1.0.0",
|
||||||
"@excalidraw/random-username": "1.1.0",
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { isTextElement, refreshTextDimensions } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getContainerElement } from "../element/textElement";
|
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -12,17 +10,9 @@ import { ShapeCache } from "./ShapeCache";
|
|||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
private scene: Scene;
|
private scene: Scene;
|
||||||
private onSceneUpdated: () => void;
|
|
||||||
|
|
||||||
constructor({
|
constructor({ scene }: { scene: Scene }) {
|
||||||
scene,
|
|
||||||
onSceneUpdated,
|
|
||||||
}: {
|
|
||||||
scene: Scene;
|
|
||||||
onSceneUpdated: () => void;
|
|
||||||
}) {
|
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.onSceneUpdated = onSceneUpdated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
@@ -57,22 +47,16 @@ export class Fonts {
|
|||||||
let didUpdate = false;
|
let didUpdate = false;
|
||||||
|
|
||||||
this.scene.mapElements((element) => {
|
this.scene.mapElements((element) => {
|
||||||
if (isTextElement(element) && !isBoundToContainer(element)) {
|
if (isTextElement(element)) {
|
||||||
ShapeCache.delete(element);
|
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
return newElementWith(element, {
|
ShapeCache.delete(element);
|
||||||
...refreshTextDimensions(
|
return newElementWith(element, {}, true);
|
||||||
element,
|
|
||||||
getContainerElement(element, this.scene.getNonDeletedElementsMap()),
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (didUpdate) {
|
if (didUpdate) {
|
||||||
this.onSceneUpdated();
|
this.scene.triggerUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -107,9 +107,8 @@ export class Renderer {
|
|||||||
width,
|
width,
|
||||||
editingElement,
|
editingElement,
|
||||||
pendingImageElementId,
|
pendingImageElementId,
|
||||||
// unused but serves we cache on it to invalidate elements if they
|
// cache-invalidation nonce
|
||||||
// get mutated
|
sceneNonce: _sceneNonce,
|
||||||
versionNonce: _versionNonce,
|
|
||||||
}: {
|
}: {
|
||||||
zoom: AppState["zoom"];
|
zoom: AppState["zoom"];
|
||||||
offsetLeft: AppState["offsetLeft"];
|
offsetLeft: AppState["offsetLeft"];
|
||||||
@@ -120,7 +119,7 @@ export class Renderer {
|
|||||||
width: AppState["width"];
|
width: AppState["width"];
|
||||||
editingElement: AppState["editingElement"];
|
editingElement: AppState["editingElement"];
|
||||||
pendingImageElementId: AppState["pendingImageElementId"];
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
versionNonce: ReturnType<InstanceType<typeof Scene>["getVersionNonce"]>;
|
sceneNonce: ReturnType<InstanceType<typeof Scene>["getSceneNonce"]>;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,17 @@ class Scene {
|
|||||||
elements: null,
|
elements: null,
|
||||||
cache: new Map(),
|
cache: new Map(),
|
||||||
};
|
};
|
||||||
private versionNonce: number | undefined;
|
/**
|
||||||
|
* Random integer regenerated each scene update.
|
||||||
|
*
|
||||||
|
* Does not relate to elements versions, it's only a renderer
|
||||||
|
* cache-invalidation nonce at the moment.
|
||||||
|
*/
|
||||||
|
private sceneNonce: number | undefined;
|
||||||
|
|
||||||
|
getSceneNonce() {
|
||||||
|
return this.sceneNonce;
|
||||||
|
}
|
||||||
|
|
||||||
getNonDeletedElementsMap() {
|
getNonDeletedElementsMap() {
|
||||||
return this.nonDeletedElementsMap;
|
return this.nonDeletedElementsMap;
|
||||||
@@ -214,10 +224,6 @@ class Scene {
|
|||||||
return (this.elementsMap.get(id) as T | undefined) || null;
|
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersionNonce() {
|
|
||||||
return this.versionNonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNonDeletedElement(
|
getNonDeletedElement(
|
||||||
id: ExcalidrawElement["id"],
|
id: ExcalidrawElement["id"],
|
||||||
): NonDeleted<ExcalidrawElement> | null {
|
): NonDeleted<ExcalidrawElement> | null {
|
||||||
@@ -286,18 +292,18 @@ class Scene {
|
|||||||
this.frames = nextFrameLikes;
|
this.frames = nextFrameLikes;
|
||||||
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
|
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
|
||||||
|
|
||||||
this.informMutation();
|
this.triggerUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
informMutation() {
|
triggerUpdate() {
|
||||||
this.versionNonce = randomInteger();
|
this.sceneNonce = randomInteger();
|
||||||
|
|
||||||
for (const callback of Array.from(this.callbacks)) {
|
for (const callback of Array.from(this.callbacks)) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addCallback(cb: SceneStateCallback): SceneStateCallbackRemover {
|
onUpdate(cb: SceneStateCallback): SceneStateCallbackRemover {
|
||||||
if (this.callbacks.has(cb)) {
|
if (this.callbacks.has(cb)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ export const exportToSvg = async (
|
|||||||
assetPath =
|
assetPath =
|
||||||
window.EXCALIDRAW_ASSET_PATH ||
|
window.EXCALIDRAW_ASSET_PATH ||
|
||||||
`https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
|
`https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
|
||||||
import.meta.env.PKG_VERSION
|
import.meta.env.VITE_PKG_VERSION
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
if (assetPath?.startsWith("/")) {
|
if (assetPath?.startsWith("/")) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -326,6 +327,16 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": null,
|
||||||
|
"label": "labels.autoResize",
|
||||||
|
"name": "autoResize",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "labels.unbindText",
|
"label": "labels.unbindText",
|
||||||
"name": "unbindText",
|
"name": "unbindText",
|
||||||
@@ -387,7 +398,11 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -482,7 +497,11 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -4414,6 +4433,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -4728,6 +4748,16 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": null,
|
||||||
|
"label": "labels.autoResize",
|
||||||
|
"name": "autoResize",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "labels.unbindText",
|
"label": "labels.unbindText",
|
||||||
"name": "unbindText",
|
"name": "unbindText",
|
||||||
@@ -4789,7 +4819,11 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -4884,7 +4918,11 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -5514,6 +5552,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -5828,6 +5867,16 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": null,
|
||||||
|
"label": "labels.autoResize",
|
||||||
|
"name": "autoResize",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "labels.unbindText",
|
"label": "labels.unbindText",
|
||||||
"name": "unbindText",
|
"name": "unbindText",
|
||||||
@@ -5889,7 +5938,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -5984,7 +6037,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -7321,6 +7378,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -7635,6 +7693,16 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": null,
|
||||||
|
"label": "labels.autoResize",
|
||||||
|
"name": "autoResize",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "labels.unbindText",
|
"label": "labels.unbindText",
|
||||||
"name": "unbindText",
|
"name": "unbindText",
|
||||||
@@ -7696,7 +7764,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -7791,7 +7863,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -8188,6 +8264,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"items": [
|
"items": [
|
||||||
|
"separator",
|
||||||
{
|
{
|
||||||
"icon": <svg
|
"icon": <svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -8502,6 +8579,16 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"icon": null,
|
||||||
|
"label": "labels.autoResize",
|
||||||
|
"name": "autoResize",
|
||||||
|
"perform": [Function],
|
||||||
|
"predicate": [Function],
|
||||||
|
"trackEvent": {
|
||||||
|
"category": "element",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "labels.unbindText",
|
"label": "labels.unbindText",
|
||||||
"name": "unbindText",
|
"name": "unbindText",
|
||||||
@@ -8563,7 +8650,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
@@ -8658,7 +8749,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
transform="rotate(180)"
|
style={
|
||||||
|
{
|
||||||
|
"transform": "rotate(180deg)",
|
||||||
|
}
|
||||||
|
}
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10752,7 +10752,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": -2.916666666666668,
|
"scrollX": -6.2500000000000036,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
@@ -13688,8 +13688,8 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"pendingImageElementId": null,
|
"pendingImageElementId": null,
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollX": 10,
|
"scrollX": 20,
|
||||||
"scrollY": -10,
|
"scrollY": -18.535533905932738,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
|
|||||||
exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
|
exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [],
|
"boundElements": [],
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
@@ -344,6 +345,7 @@ exports[`restoreElements > should restore text element correctly passing value f
|
|||||||
exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
|
exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
|
"autoResize": true,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": [],
|
"boundElements": [],
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { Excalidraw } from "../index";
|
|||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { fireEvent, waitFor } from "@testing-library/react";
|
import { fireEvent, queryByTestId, waitFor } from "@testing-library/react";
|
||||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||||
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
|
||||||
import type { AppState, ExcalidrawImperativeAPI } from "../types";
|
import type { AppState, ExcalidrawImperativeAPI } from "../types";
|
||||||
import { arrayToMap, resolvablePromise } from "../utils";
|
import { arrayToMap, resolvablePromise } from "../utils";
|
||||||
@@ -49,7 +50,6 @@ const checkpoint = (name: string) => {
|
|||||||
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
`[${name}] number of renders`,
|
`[${name}] number of renders`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// `scrolledOutside` does not appear to be stable between test runs
|
// `scrolledOutside` does not appear to be stable between test runs
|
||||||
// `selectedLinearElemnt` includes `startBindingElement` containing seed and versionNonce
|
// `selectedLinearElemnt` includes `startBindingElement` containing seed and versionNonce
|
||||||
const {
|
const {
|
||||||
@@ -1688,6 +1688,129 @@ describe("history", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should disable undo/redo buttons when stacks empty", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{
|
||||||
|
elements: [API.createElement({ type: "rectangle", id: "A" })],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const undoAction = createUndoAction(h.history, h.store);
|
||||||
|
const redoAction = createRedoAction(h.history, h.store);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
const undoButton = queryByTestId(container, "button-undo");
|
||||||
|
const redoButton = queryByTestId(container, "button-redo");
|
||||||
|
|
||||||
|
expect(undoButton).toBeDisabled();
|
||||||
|
expect(redoButton).toBeDisabled();
|
||||||
|
|
||||||
|
const rectangle = UI.createElement("rectangle");
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining({ id: "A" }),
|
||||||
|
expect.objectContaining({ id: rectangle.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeFalsy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
expect(undoButton).not.toBeDisabled();
|
||||||
|
expect(redoButton).toBeDisabled();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeFalsy();
|
||||||
|
expect(undoButton).toBeDisabled();
|
||||||
|
expect(redoButton).not.toBeDisabled();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeFalsy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
expect(undoButton).not.toBeDisabled();
|
||||||
|
expect(redoButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remounting undo/redo buttons should initialize undo/redo state correctly", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<Excalidraw
|
||||||
|
initialData={{
|
||||||
|
elements: [API.createElement({ type: "rectangle", id: "A" })],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const undoAction = createUndoAction(h.history, h.store);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId(container, "button-undo")).toBeDisabled();
|
||||||
|
expect(queryByTestId(container, "button-redo")).toBeDisabled();
|
||||||
|
|
||||||
|
// testing undo button
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
const rectangle = UI.createElement("rectangle");
|
||||||
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining({ id: "A" }),
|
||||||
|
expect.objectContaining({ id: rectangle.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeFalsy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||||
|
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
|
||||||
|
expect(queryByTestId(container, "button-redo")).toBeDisabled();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||||
|
expect(h.state.viewModeEnabled).toBe(true);
|
||||||
|
|
||||||
|
expect(queryByTestId(container, "button-undo")).toBeNull();
|
||||||
|
expect(queryByTestId(container, "button-redo")).toBeNull();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||||
|
expect(h.state.viewModeEnabled).toBe(false);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
|
||||||
|
expect(queryByTestId(container, "button-redo")).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// testing redo button
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeFalsy();
|
||||||
|
expect(queryByTestId(container, "button-undo")).toBeDisabled();
|
||||||
|
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||||
|
expect(h.state.viewModeEnabled).toBe(true);
|
||||||
|
|
||||||
|
expect(queryByTestId(container, "button-undo")).toBeNull();
|
||||||
|
expect(queryByTestId(container, "button-redo")).toBeNull();
|
||||||
|
|
||||||
|
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||||
|
expect(h.state.viewModeEnabled).toBe(false);
|
||||||
|
|
||||||
|
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||||
|
expect(h.history.isRedoStackEmpty).toBeFalsy();
|
||||||
|
expect(queryByTestId(container, "button-undo")).toBeDisabled();
|
||||||
|
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("multiplayer undo/redo", () => {
|
describe("multiplayer undo/redo", () => {
|
||||||
|
|||||||
@@ -426,6 +426,112 @@ describe("text element", () => {
|
|||||||
expect(text.fontSize).toBe(fontSize);
|
expect(text.fontSize).toBe(fontSize);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// text can be resized from sides
|
||||||
|
it("can be resized from e", async () => {
|
||||||
|
const text = UI.createElement("text");
|
||||||
|
await UI.editText(text, "Excalidraw\nEditor");
|
||||||
|
|
||||||
|
const width = text.width;
|
||||||
|
const height = text.height;
|
||||||
|
|
||||||
|
UI.resize(text, "e", [30, 0]);
|
||||||
|
expect(text.width).toBe(width + 30);
|
||||||
|
expect(text.height).toBe(height);
|
||||||
|
|
||||||
|
UI.resize(text, "e", [-30, 0]);
|
||||||
|
expect(text.width).toBe(width);
|
||||||
|
expect(text.height).toBe(height);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be resized from w", async () => {
|
||||||
|
const text = UI.createElement("text");
|
||||||
|
await UI.editText(text, "Excalidraw\nEditor");
|
||||||
|
|
||||||
|
const width = text.width;
|
||||||
|
const height = text.height;
|
||||||
|
|
||||||
|
UI.resize(text, "w", [-50, 0]);
|
||||||
|
expect(text.width).toBe(width + 50);
|
||||||
|
expect(text.height).toBe(height);
|
||||||
|
|
||||||
|
UI.resize(text, "w", [50, 0]);
|
||||||
|
expect(text.width).toBe(width);
|
||||||
|
expect(text.height).toBe(height);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wraps when width is narrower than texts inside", async () => {
|
||||||
|
const text = UI.createElement("text");
|
||||||
|
await UI.editText(text, "Excalidraw\nEditor");
|
||||||
|
|
||||||
|
const prevWidth = text.width;
|
||||||
|
const prevHeight = text.height;
|
||||||
|
const prevText = text.text;
|
||||||
|
|
||||||
|
UI.resize(text, "w", [50, 0]);
|
||||||
|
expect(text.width).toBe(prevWidth - 50);
|
||||||
|
expect(text.height).toBeGreaterThan(prevHeight);
|
||||||
|
expect(text.text).not.toEqual(prevText);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
UI.resize(text, "w", [-50, 0]);
|
||||||
|
expect(text.width).toBe(prevWidth);
|
||||||
|
expect(text.height).toEqual(prevHeight);
|
||||||
|
expect(text.text).toEqual(prevText);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
UI.resize(text, "e", [-20, 0]);
|
||||||
|
expect(text.width).toBe(prevWidth - 20);
|
||||||
|
expect(text.height).toBeGreaterThan(prevHeight);
|
||||||
|
expect(text.text).not.toEqual(prevText);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
UI.resize(text, "e", [20, 0]);
|
||||||
|
expect(text.width).toBe(prevWidth);
|
||||||
|
expect(text.height).toEqual(prevHeight);
|
||||||
|
expect(text.text).toEqual(prevText);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps properties when wrapped", async () => {
|
||||||
|
const text = UI.createElement("text");
|
||||||
|
await UI.editText(text, "Excalidraw\nEditor");
|
||||||
|
|
||||||
|
const alignment = text.textAlign;
|
||||||
|
const fontSize = text.fontSize;
|
||||||
|
const fontFamily = text.fontFamily;
|
||||||
|
|
||||||
|
UI.resize(text, "e", [-60, 0]);
|
||||||
|
expect(text.textAlign).toBe(alignment);
|
||||||
|
expect(text.fontSize).toBe(fontSize);
|
||||||
|
expect(text.fontFamily).toBe(fontFamily);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
UI.resize(text, "e", [60, 0]);
|
||||||
|
expect(text.textAlign).toBe(alignment);
|
||||||
|
expect(text.fontSize).toBe(fontSize);
|
||||||
|
expect(text.fontFamily).toBe(fontFamily);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has a minimum width when wrapped", async () => {
|
||||||
|
const text = UI.createElement("text");
|
||||||
|
await UI.editText(text, "Excalidraw\nEditor");
|
||||||
|
|
||||||
|
const width = text.width;
|
||||||
|
|
||||||
|
UI.resize(text, "e", [-width, 0]);
|
||||||
|
expect(text.width).not.toEqual(0);
|
||||||
|
UI.resize(text, "e", [width - text.width, 0]);
|
||||||
|
expect(text.width).toEqual(width);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
|
||||||
|
UI.resize(text, "w", [width, 0]);
|
||||||
|
expect(text.width).not.toEqual(0);
|
||||||
|
UI.resize(text, "w", [text.width - width, 0]);
|
||||||
|
expect(text.width).toEqual(width);
|
||||||
|
expect(text.autoResize).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("image element", () => {
|
describe("image element", () => {
|
||||||
|
|||||||
@@ -1930,10 +1930,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
|
resolved "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
|
||||||
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
|
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
|
||||||
|
|
||||||
"@excalidraw/mermaid-to-excalidraw@0.3.0":
|
"@excalidraw/mermaid-to-excalidraw@1.0.0":
|
||||||
version "0.3.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-0.3.0.tgz#94c438133fc66db6b920e237abda5152b62e6cb0"
|
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.0.0.tgz#8c058d2a43230425cba96d01e4a669a2d7c586a2"
|
||||||
integrity sha512-eyFN8y2ES3HFtETZWZZBakkSB5ROfnHJeCLeBlMgrIk1fxbXpPtxlu2VwGNpqPjDiCfV5FYnx7FaZ4CRiVRVMg==
|
integrity sha512-RGSoJBY2gFag6mQOIwa3OakTrvAZYx0bwvnr5ojuCZInih8Fxhje4X1WZfsaQx+GATEH8Ioq3O3b1FPDg4nKjQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@excalidraw/markdown-to-text" "0.1.2"
|
"@excalidraw/markdown-to-text" "0.1.2"
|
||||||
mermaid "10.9.0"
|
mermaid "10.9.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user