Set Trailing Cmma to (#525)

pull/535/head
Lipis 5 years ago committed by GitHub
parent 25202aec11
commit ee68af0fd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,5 +11,5 @@ module.exports = {
files.filter(file => !cli.isPathIgnored(file)).join(" ") files.filter(file => !cli.isPathIgnored(file)).join(" ")
); );
}, },
"*.{js,css,scss,json,md,ts,tsx,html,yml}": ["prettier --write"] "*.{js,css,scss,json,md,ts,tsx,html,yml}": ["prettier --write"],
}; };

@ -1 +1,3 @@
{} {
"trailingComma": "all"
}

@ -17,8 +17,8 @@ var config = defaults.__get__("config");
config.optimization.runtimeChunk = false; config.optimization.runtimeChunk = false;
config.optimization.splitChunks = { config.optimization.splitChunks = {
cacheGroups: { cacheGroups: {
default: false default: false,
} },
}; };
// Set the filename to be deterministic // Set the filename to be deterministic
config.output.filename = "static/js/build-node.js"; config.output.filename = "static/js/build-node.js";
@ -33,7 +33,7 @@ config.externals = function(context, request, callback) {
if (/\.node$/.test(request)) { if (/\.node$/.test(request)) {
return callback( return callback(
null, null,
"commonjs ../../../node_modules/canvas/build/Release/canvas.node" "commonjs ../../../node_modules/canvas/build/Release/canvas.node",
); );
} }
callback(); callback();

@ -20,7 +20,7 @@ export const actionChangeViewBackgroundColor: Action = {
/> />
</div> </div>
); );
} },
}; };
export const actionClearCanvas: Action = { export const actionClearCanvas: Action = {
@ -28,7 +28,7 @@ export const actionClearCanvas: Action = {
perform: () => { perform: () => {
return { return {
elements: [], elements: [],
appState: getDefaultAppState() appState: getDefaultAppState(),
}; };
}, },
PanelComponent: ({ updateData, t }) => ( PanelComponent: ({ updateData, t }) => (
@ -47,5 +47,5 @@ export const actionClearCanvas: Action = {
} }
}} }}
/> />
) ),
}; };

@ -6,10 +6,10 @@ export const actionDeleteSelected: Action = {
name: "deleteSelectedElements", name: "deleteSelectedElements",
perform: elements => { perform: elements => {
return { return {
elements: deleteSelectedElements(elements) elements: deleteSelectedElements(elements),
}; };
}, },
contextItemLabel: "labels.delete", contextItemLabel: "labels.delete",
contextMenuOrder: 3, contextMenuOrder: 3,
keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE keyTest: event => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
}; };

@ -15,7 +15,7 @@ export const actionChangeProjectName: Action = {
value={appState.name || "Unnamed"} value={appState.name || "Unnamed"}
onChange={(name: string) => updateData(name)} onChange={(name: string) => updateData(name)}
/> />
) ),
}; };
export const actionChangeExportBackground: Action = { export const actionChangeExportBackground: Action = {
@ -34,7 +34,7 @@ export const actionChangeExportBackground: Action = {
/>{" "} />{" "}
{t("labels.withBackground")} {t("labels.withBackground")}
</label> </label>
) ),
}; };
export const actionSaveScene: Action = { export const actionSaveScene: Action = {
@ -51,7 +51,7 @@ export const actionSaveScene: Action = {
aria-label={t("buttons.save")} aria-label={t("buttons.save")}
onClick={() => updateData(null)} onClick={() => updateData(null)}
/> />
) ),
}; };
export const actionLoadScene: Action = { export const actionLoadScene: Action = {
@ -59,7 +59,7 @@ export const actionLoadScene: Action = {
perform: ( perform: (
elements, elements,
appState, appState,
{ elements: loadedElements, appState: loadedAppState } { elements: loadedElements, appState: loadedAppState },
) => { ) => {
return { elements: loadedElements, appState: loadedAppState }; return { elements: loadedElements, appState: loadedAppState };
}, },
@ -77,5 +77,5 @@ export const actionLoadScene: Action = {
.catch(err => console.error(err)); .catch(err => console.error(err));
}} }}
/> />
) ),
}; };

@ -9,7 +9,7 @@ import { AppState } from "../../src/types";
const changeProperty = ( const changeProperty = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
callback: (element: ExcalidrawElement) => ExcalidrawElement callback: (element: ExcalidrawElement) => ExcalidrawElement,
) => { ) => {
return elements.map(element => { return elements.map(element => {
if (element.isSelected) { if (element.isSelected) {
@ -23,7 +23,7 @@ const getFormValue = function<T>(
editingElement: AppState["editingElement"], editingElement: AppState["editingElement"],
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
getAttribute: (element: ExcalidrawElement) => T, getAttribute: (element: ExcalidrawElement) => T,
defaultValue?: T defaultValue?: T,
): T | null { ): T | null {
return ( return (
(editingElement && getAttribute(editingElement)) ?? (editingElement && getAttribute(editingElement)) ??
@ -40,9 +40,9 @@ export const actionChangeStrokeColor: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
strokeColor: value strokeColor: value,
})), })),
appState: { ...appState, currentItemStrokeColor: value } appState: { ...appState, currentItemStrokeColor: value },
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -54,12 +54,12 @@ export const actionChangeStrokeColor: Action = {
appState.editingElement, appState.editingElement,
elements, elements,
element => element.strokeColor, element => element.strokeColor,
appState.currentItemStrokeColor appState.currentItemStrokeColor,
)} )}
onChange={updateData} onChange={updateData}
/> />
</> </>
) ),
}; };
export const actionChangeBackgroundColor: Action = { export const actionChangeBackgroundColor: Action = {
@ -69,9 +69,9 @@ export const actionChangeBackgroundColor: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
backgroundColor: value backgroundColor: value,
})), })),
appState: { ...appState, currentItemBackgroundColor: value } appState: { ...appState, currentItemBackgroundColor: value },
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -83,12 +83,12 @@ export const actionChangeBackgroundColor: Action = {
appState.editingElement, appState.editingElement,
elements, elements,
element => element.backgroundColor, element => element.backgroundColor,
appState.currentItemBackgroundColor appState.currentItemBackgroundColor,
)} )}
onChange={updateData} onChange={updateData}
/> />
</> </>
) ),
}; };
export const actionChangeFillStyle: Action = { export const actionChangeFillStyle: Action = {
@ -98,8 +98,8 @@ export const actionChangeFillStyle: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
fillStyle: value fillStyle: value,
})) })),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -109,19 +109,19 @@ export const actionChangeFillStyle: Action = {
options={[ options={[
{ value: "solid", text: t("labels.solid") }, { value: "solid", text: t("labels.solid") },
{ value: "hachure", text: t("labels.hachure") }, { value: "hachure", text: t("labels.hachure") },
{ value: "cross-hatch", text: t("labels.crossHatch") } { value: "cross-hatch", text: t("labels.crossHatch") },
]} ]}
value={getFormValue( value={getFormValue(
appState.editingElement, appState.editingElement,
elements, elements,
element => element.fillStyle element => element.fillStyle,
)} )}
onChange={value => { onChange={value => {
updateData(value); updateData(value);
}} }}
/> />
</> </>
) ),
}; };
export const actionChangeStrokeWidth: Action = { export const actionChangeStrokeWidth: Action = {
@ -131,8 +131,8 @@ export const actionChangeStrokeWidth: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
strokeWidth: value strokeWidth: value,
})) })),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -142,17 +142,17 @@ export const actionChangeStrokeWidth: Action = {
options={[ options={[
{ value: 1, text: t("labels.thin") }, { value: 1, text: t("labels.thin") },
{ value: 2, text: t("labels.bold") }, { value: 2, text: t("labels.bold") },
{ value: 4, text: t("labels.extraBold") } { value: 4, text: t("labels.extraBold") },
]} ]}
value={getFormValue( value={getFormValue(
appState.editingElement, appState.editingElement,
elements, elements,
element => element.strokeWidth element => element.strokeWidth,
)} )}
onChange={value => updateData(value)} onChange={value => updateData(value)}
/> />
</> </>
) ),
}; };
export const actionChangeSloppiness: Action = { export const actionChangeSloppiness: Action = {
@ -162,8 +162,8 @@ export const actionChangeSloppiness: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
roughness: value roughness: value,
})) })),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -173,17 +173,17 @@ export const actionChangeSloppiness: Action = {
options={[ options={[
{ value: 0, text: t("labels.architect") }, { value: 0, text: t("labels.architect") },
{ value: 1, text: t("labels.artist") }, { value: 1, text: t("labels.artist") },
{ value: 3, text: t("labels.cartoonist") } { value: 3, text: t("labels.cartoonist") },
]} ]}
value={getFormValue( value={getFormValue(
appState.editingElement, appState.editingElement,
elements, elements,
element => element.roughness element => element.roughness,
)} )}
onChange={value => updateData(value)} onChange={value => updateData(value)}
/> />
</> </>
) ),
}; };
export const actionChangeOpacity: Action = { export const actionChangeOpacity: Action = {
@ -193,8 +193,8 @@ export const actionChangeOpacity: Action = {
elements: changeProperty(elements, el => ({ elements: changeProperty(elements, el => ({
...el, ...el,
shape: null, shape: null,
opacity: value opacity: value,
})) })),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -210,12 +210,12 @@ export const actionChangeOpacity: Action = {
appState.editingElement, appState.editingElement,
elements, elements,
element => element.opacity, element => element.opacity,
100 /* default opacity */ 100 /* default opacity */,
) ?? undefined ) ?? undefined
} }
/> />
</> </>
) ),
}; };
export const actionChangeFontSize: Action = { export const actionChangeFontSize: Action = {
@ -227,14 +227,14 @@ export const actionChangeFontSize: Action = {
const element: ExcalidrawTextElement = { const element: ExcalidrawTextElement = {
...el, ...el,
shape: null, shape: null,
font: `${value}px ${el.font.split("px ")[1]}` font: `${value}px ${el.font.split("px ")[1]}`,
}; };
redrawTextBoundingBox(element); redrawTextBoundingBox(element);
return element; return element;
} }
return el; return el;
}) }),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -245,17 +245,17 @@ export const actionChangeFontSize: Action = {
{ value: 16, text: t("labels.small") }, { value: 16, text: t("labels.small") },
{ value: 20, text: t("labels.medium") }, { value: 20, text: t("labels.medium") },
{ value: 28, text: t("labels.large") }, { value: 28, text: t("labels.large") },
{ value: 36, text: t("labels.veryLarge") } { value: 36, text: t("labels.veryLarge") },
]} ]}
value={getFormValue( value={getFormValue(
appState.editingElement, appState.editingElement,
elements, elements,
element => isTextElement(element) && +element.font.split("px ")[0] element => isTextElement(element) && +element.font.split("px ")[0],
)} )}
onChange={value => updateData(value)} onChange={value => updateData(value)}
/> />
</> </>
) ),
}; };
export const actionChangeFontFamily: Action = { export const actionChangeFontFamily: Action = {
@ -267,14 +267,14 @@ export const actionChangeFontFamily: Action = {
const element: ExcalidrawTextElement = { const element: ExcalidrawTextElement = {
...el, ...el,
shape: null, shape: null,
font: `${el.font.split("px ")[0]}px ${value}` font: `${el.font.split("px ")[0]}px ${value}`,
}; };
redrawTextBoundingBox(element); redrawTextBoundingBox(element);
return element; return element;
} }
return el; return el;
}) }),
}; };
}, },
PanelComponent: ({ elements, appState, updateData, t }) => ( PanelComponent: ({ elements, appState, updateData, t }) => (
@ -284,15 +284,15 @@ export const actionChangeFontFamily: Action = {
options={[ options={[
{ value: "Virgil", text: t("labels.handDrawn") }, { value: "Virgil", text: t("labels.handDrawn") },
{ value: "Helvetica", text: t("labels.normal") }, { value: "Helvetica", text: t("labels.normal") },
{ value: "Cascadia", text: t("labels.code") } { value: "Cascadia", text: t("labels.code") },
]} ]}
value={getFormValue( value={getFormValue(
appState.editingElement, appState.editingElement,
elements, elements,
element => isTextElement(element) && element.font.split("px ")[1] element => isTextElement(element) && element.font.split("px ")[1],
)} )}
onChange={value => updateData(value)} onChange={value => updateData(value)}
/> />
</> </>
) ),
}; };

@ -5,9 +5,9 @@ export const actionSelectAll: Action = {
name: "selectAll", name: "selectAll",
perform: elements => { perform: elements => {
return { return {
elements: elements.map(elem => ({ ...elem, isSelected: true })) elements: elements.map(elem => ({ ...elem, isSelected: true })),
}; };
}, },
contextItemLabel: "labels.selectAll", contextItemLabel: "labels.selectAll",
keyTest: event => event[KEYS.META] && event.code === "KeyA" keyTest: event => event[KEYS.META] && event.code === "KeyA",
}; };

@ -15,7 +15,7 @@ export const actionCopyStyles: Action = {
}, },
contextItemLabel: "labels.copyStyles", contextItemLabel: "labels.copyStyles",
keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyC", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyC",
contextMenuOrder: 0 contextMenuOrder: 0,
}; };
export const actionPasteStyles: Action = { export const actionPasteStyles: Action = {
@ -33,7 +33,7 @@ export const actionPasteStyles: Action = {
strokeColor: pastedElement?.strokeColor, strokeColor: pastedElement?.strokeColor,
fillStyle: pastedElement?.fillStyle, fillStyle: pastedElement?.fillStyle,
opacity: pastedElement?.opacity, opacity: pastedElement?.opacity,
roughness: pastedElement?.roughness roughness: pastedElement?.roughness,
}; };
if (isTextElement(newElement)) { if (isTextElement(newElement)) {
newElement.font = pastedElement?.font; newElement.font = pastedElement?.font;
@ -42,10 +42,10 @@ export const actionPasteStyles: Action = {
return newElement; return newElement;
} }
return element; return element;
}) }),
}; };
}, },
contextItemLabel: "labels.pasteStyles", contextItemLabel: "labels.pasteStyles",
keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyV", keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyV",
contextMenuOrder: 1 contextMenuOrder: 1,
}; };

@ -3,7 +3,7 @@ import {
moveOneLeft, moveOneLeft,
moveOneRight, moveOneRight,
moveAllLeft, moveAllLeft,
moveAllRight moveAllRight,
} from "../zindex"; } from "../zindex";
import { getSelectedIndices } from "../scene"; import { getSelectedIndices } from "../scene";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@ -13,13 +13,13 @@ export const actionSendBackward: Action = {
perform: (elements, appState) => { perform: (elements, appState) => {
return { return {
elements: moveOneLeft([...elements], getSelectedIndices(elements)), elements: moveOneLeft([...elements], getSelectedIndices(elements)),
appState appState,
}; };
}, },
contextItemLabel: "labels.sendBackward", contextItemLabel: "labels.sendBackward",
keyPriority: 40, keyPriority: 40,
keyTest: event => keyTest: event =>
event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyB" event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyB",
}; };
export const actionBringForward: Action = { export const actionBringForward: Action = {
@ -27,13 +27,13 @@ export const actionBringForward: Action = {
perform: (elements, appState) => { perform: (elements, appState) => {
return { return {
elements: moveOneRight([...elements], getSelectedIndices(elements)), elements: moveOneRight([...elements], getSelectedIndices(elements)),
appState appState,
}; };
}, },
contextItemLabel: "labels.bringForward", contextItemLabel: "labels.bringForward",
keyPriority: 40, keyPriority: 40,
keyTest: event => keyTest: event =>
event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyF" event[KEYS.META] && event.shiftKey && event.altKey && event.code === "KeyF",
}; };
export const actionSendToBack: Action = { export const actionSendToBack: Action = {
@ -41,11 +41,11 @@ export const actionSendToBack: Action = {
perform: (elements, appState) => { perform: (elements, appState) => {
return { return {
elements: moveAllLeft([...elements], getSelectedIndices(elements)), elements: moveAllLeft([...elements], getSelectedIndices(elements)),
appState appState,
}; };
}, },
contextItemLabel: "labels.sendToBack", contextItemLabel: "labels.sendToBack",
keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyB" keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyB",
}; };
export const actionBringToFront: Action = { export const actionBringToFront: Action = {
@ -53,9 +53,9 @@ export const actionBringToFront: Action = {
perform: (elements, appState) => { perform: (elements, appState) => {
return { return {
elements: moveAllRight([...elements], getSelectedIndices(elements)), elements: moveAllRight([...elements], getSelectedIndices(elements)),
appState appState,
}; };
}, },
contextItemLabel: "labels.bringToFront", contextItemLabel: "labels.bringToFront",
keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyF" keyTest: event => event[KEYS.META] && event.shiftKey && event.code === "KeyF",
}; };

@ -4,7 +4,7 @@ export {
actionBringForward, actionBringForward,
actionBringToFront, actionBringToFront,
actionSendBackward, actionSendBackward,
actionSendToBack actionSendToBack,
} from "./actionZindex"; } from "./actionZindex";
export { actionSelectAll } from "./actionSelectAll"; export { actionSelectAll } from "./actionSelectAll";
export { export {
@ -15,19 +15,19 @@ export {
actionChangeSloppiness, actionChangeSloppiness,
actionChangeOpacity, actionChangeOpacity,
actionChangeFontSize, actionChangeFontSize,
actionChangeFontFamily actionChangeFontFamily,
} from "./actionProperties"; } from "./actionProperties";
export { export {
actionChangeViewBackgroundColor, actionChangeViewBackgroundColor,
actionClearCanvas actionClearCanvas,
} from "./actionCanvas"; } from "./actionCanvas";
export { export {
actionChangeProjectName, actionChangeProjectName,
actionChangeExportBackground, actionChangeExportBackground,
actionSaveScene, actionSaveScene,
actionLoadScene actionLoadScene,
} from "./actionExport"; } from "./actionExport";
export { actionCopyStyles, actionPasteStyles } from "./actionStyles"; export { actionCopyStyles, actionPasteStyles } from "./actionStyles";

@ -3,7 +3,7 @@ import {
Action, Action,
ActionsManagerInterface, ActionsManagerInterface,
UpdaterFn, UpdaterFn,
ActionFilterFn ActionFilterFn,
} from "./types"; } from "./types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
@ -17,7 +17,7 @@ export class ActionManager implements ActionsManagerInterface {
| null = null; | null = null;
setUpdater( setUpdater(
updater: (elements: ExcalidrawElement[], appState: AppState) => void updater: (elements: ExcalidrawElement[], appState: AppState) => void,
) { ) {
this.updater = updater; this.updater = updater;
} }
@ -29,12 +29,12 @@ export class ActionManager implements ActionsManagerInterface {
handleKeyDown( handleKeyDown(
event: KeyboardEvent, event: KeyboardEvent,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState appState: AppState,
) { ) {
const data = Object.values(this.actions) const data = Object.values(this.actions)
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0)) .sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
.filter( .filter(
action => action.keyTest && action.keyTest(event, elements, appState) action => action.keyTest && action.keyTest(event, elements, appState),
); );
if (data.length === 0) return {}; if (data.length === 0) return {};
@ -48,7 +48,7 @@ export class ActionManager implements ActionsManagerInterface {
appState: AppState, appState: AppState,
updater: UpdaterFn, updater: UpdaterFn,
actionFilter: ActionFilterFn = action => action, actionFilter: ActionFilterFn = action => action,
t?: TFunction t?: TFunction,
) { ) {
return Object.values(this.actions) return Object.values(this.actions)
.filter(actionFilter) .filter(actionFilter)
@ -56,7 +56,7 @@ export class ActionManager implements ActionsManagerInterface {
.sort( .sort(
(a, b) => (a, b) =>
(a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) - (a.contextMenuOrder !== undefined ? a.contextMenuOrder : 999) -
(b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999) (b.contextMenuOrder !== undefined ? b.contextMenuOrder : 999),
) )
.map(action => ({ .map(action => ({
label: label:
@ -65,7 +65,7 @@ export class ActionManager implements ActionsManagerInterface {
: action.contextItemLabel!, : action.contextItemLabel!,
action: () => { action: () => {
updater(action.perform(elements, appState, null)); updater(action.perform(elements, appState, null));
} },
})); }));
} }
@ -74,7 +74,7 @@ export class ActionManager implements ActionsManagerInterface {
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
updater: UpdaterFn, updater: UpdaterFn,
t: TFunction t: TFunction,
) { ) {
if (this.actions[name] && "PanelComponent" in this.actions[name]) { if (this.actions[name] && "PanelComponent" in this.actions[name]) {
const action = this.actions[name]; const action = this.actions[name];

@ -11,7 +11,7 @@ export type ActionResult = {
type ActionFn = ( type ActionFn = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
formData: any formData: any,
) => ActionResult; ) => ActionResult;
export type UpdaterFn = (res: ActionResult) => void; export type UpdaterFn = (res: ActionResult) => void;
@ -30,7 +30,7 @@ export interface Action {
keyTest?: ( keyTest?: (
event: KeyboardEvent, event: KeyboardEvent,
elements?: readonly ExcalidrawElement[], elements?: readonly ExcalidrawElement[],
appState?: AppState appState?: AppState,
) => boolean; ) => boolean;
contextItemLabel?: string; contextItemLabel?: string;
contextMenuOrder?: number; contextMenuOrder?: number;
@ -44,19 +44,19 @@ export interface ActionsManagerInterface {
handleKeyDown: ( handleKeyDown: (
event: KeyboardEvent, event: KeyboardEvent,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState appState: AppState,
) => ActionResult | {}; ) => ActionResult | {};
getContextMenuItems: ( getContextMenuItems: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
updater: UpdaterFn, updater: UpdaterFn,
actionFilter: ActionFilterFn actionFilter: ActionFilterFn,
) => { label: string; action: () => void }[]; ) => { label: string; action: () => void }[];
renderAction: ( renderAction: (
name: string, name: string,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
updater: UpdaterFn, updater: UpdaterFn,
t: TFunction t: TFunction,
) => React.ReactElement | null; ) => React.ReactElement | null;
} }

@ -19,6 +19,6 @@ export function getDefaultAppState(): AppState {
scrollY: 0, scrollY: 0,
cursorX: 0, cursorX: 0,
cursorY: 0, cursorY: 0,
name: DEFAULT_PROJECT_NAME name: DEFAULT_PROJECT_NAME,
}; };
} }

@ -3,7 +3,7 @@ import React from "react";
export function ButtonSelect<T>({ export function ButtonSelect<T>({
options, options,
value, value,
onChange onChange,
}: { }: {
options: { value: T; text: string }[]; options: { value: T; text: string }[];
value: T | null; value: T | null;

@ -9,7 +9,7 @@ import "./ColorPicker.css";
const Picker = function({ const Picker = function({
colors, colors,
color, color,
onChange onChange,
}: { }: {
colors: string[]; colors: string[];
color: string | null; color: string | null;
@ -53,7 +53,7 @@ const Picker = function({
function ColorInput({ function ColorInput({
color, color,
onChange onChange,
}: { }: {
color: string | null; color: string | null;
onChange: (color: string) => void; onChange: (color: string) => void;
@ -90,7 +90,7 @@ function ColorInput({
export function ColorPicker({ export function ColorPicker({
type, type,
color, color,
onChange onChange,
}: { }: {
type: "canvasBackground" | "elementBackground" | "elementStroke"; type: "canvasBackground" | "elementBackground" | "elementStroke";
color: string | null; color: string | null;
@ -149,7 +149,7 @@ const colors = {
"#ebfbee", "#ebfbee",
"#f4fce3", "#f4fce3",
"#fff9db", "#fff9db",
"#fff4e6" "#fff4e6",
], ],
// Shade 6 // Shade 6
elementBackground: [ elementBackground: [
@ -167,7 +167,7 @@ const colors = {
"#40c057", "#40c057",
"#82c91e", "#82c91e",
"#fab005", "#fab005",
"#fd7e14" "#fd7e14",
], ],
// Shade 9 // Shade 9
elementStroke: [ elementStroke: [
@ -185,6 +185,6 @@ const colors = {
"#2b8a3e", "#2b8a3e",
"#5c940d", "#5c940d",
"#e67700", "#e67700",
"#d9480f" "#d9480f",
] ],
}; };

@ -83,8 +83,8 @@ export default {
options={options} options={options}
onCloseRequest={handleClose} onCloseRequest={handleClose}
/>, />,
getContextMenuNode() getContextMenuNode(),
); );
} }
} },
}; };

@ -25,7 +25,7 @@ const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
type ExportCB = ( type ExportCB = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
scale?: number scale?: number,
) => void; ) => void;
export function ExportDialog({ export function ExportDialog({
@ -36,7 +36,7 @@ export function ExportDialog({
syncActionResult, syncActionResult,
onExportToPng, onExportToPng,
onExportToClipboard, onExportToClipboard,
onExportToBackend onExportToBackend,
}: { }: {
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
@ -69,7 +69,7 @@ export function ExportDialog({
exportBackground, exportBackground,
viewBackgroundColor, viewBackgroundColor,
exportPadding, exportPadding,
scale scale,
}); });
previewNode?.appendChild(canvas); previewNode?.appendChild(canvas);
return () => { return () => {
@ -81,7 +81,7 @@ export function ExportDialog({
exportBackground, exportBackground,
exportPadding, exportPadding,
viewBackgroundColor, viewBackgroundColor,
scale scale,
]); ]);
function handleClose() { function handleClose() {
@ -141,7 +141,7 @@ export function ExportDialog({
elements, elements,
appState, appState,
syncActionResult, syncActionResult,
t t,
)} )}
<Stack.Col gap={1}> <Stack.Col gap={1}>
<div className="ExportDialog__scales"> <div className="ExportDialog__scales">
@ -165,7 +165,7 @@ export function ExportDialog({
elements, elements,
appState, appState,
syncActionResult, syncActionResult,
t t,
)} )}
{someElementIsSelected && ( {someElementIsSelected && (
<div> <div>

@ -9,7 +9,7 @@ type FixedSideContainerProps = {
export function FixedSideContainer({ export function FixedSideContainer({
children, children,
side side,
}: FixedSideContainerProps) { }: FixedSideContainerProps) {
return ( return (
<div className={"FixedSideContainer FixedSideContainer_side_" + side}> <div className={"FixedSideContainer FixedSideContainer_side_" + side}>

@ -3,7 +3,7 @@ import React from "react";
export function LanguageList<T>({ export function LanguageList<T>({
onClick, onClick,
languages, languages,
currentLanguage currentLanguage,
}: { }: {
languages: { lng: string; label: string }[]; languages: { lng: string; label: string }[];
onClick: (value: string) => void; onClick: (value: string) => void;

@ -35,7 +35,7 @@ const ICONS = {
> >
<path d="M1728 576v256q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45v-256q0-106-75-181t-181-75-181 75-75 181v192h96q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h672v-192q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5z" /> <path d="M1728 576v256q0 26-19 45t-45 19h-64q-26 0-45-19t-19-45v-256q0-106-75-181t-181-75-181 75-75 181v192h96q40 0 68 28t28 68v576q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-576q0-40 28-68t68-28h672v-192q0-185 131.5-316.5t316.5-131.5 316.5 131.5 131.5 316.5z" />
</svg> </svg>
) ),
}; };
export function LockIcon(props: LockIconProps) { export function LockIcon(props: LockIconProps) {

@ -16,7 +16,7 @@ export function Modal(props: {
{props.children} {props.children}
</div> </div>
</div>, </div>,
modalRoot modalRoot,
); );
} }

@ -14,7 +14,7 @@ export function Popover({
left, left,
top, top,
onCloseRequest, onCloseRequest,
fitInViewport = false fitInViewport = false,
}: Props) { }: Props) {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);

@ -17,7 +17,7 @@ function RowStack({ children, gap, align, justifyContent }: StackProps) {
{ {
"--gap": gap, "--gap": gap,
alignItems: align, alignItems: align,
justifyContent justifyContent,
} as React.CSSProperties } as React.CSSProperties
} }
> >
@ -34,7 +34,7 @@ function ColStack({ children, gap, align, justifyContent }: StackProps) {
{ {
"--gap": gap, "--gap": gap,
justifyItems: align, justifyItems: align,
justifyContent justifyContent,
} as React.CSSProperties } as React.CSSProperties
} }
> >
@ -45,5 +45,5 @@ function ColStack({ children, gap, align, justifyContent }: StackProps) {
export default { export default {
Row: RowStack, Row: RowStack,
Col: ColStack Col: ColStack,
}; };

@ -13,7 +13,7 @@ const _ce = ({ x, y, w, h }: { x: number; y: number; w: number; h: number }) =>
x, x,
y, y,
width: w, width: w,
height: h height: h,
} as ExcalidrawElement); } as ExcalidrawElement);
describe("getElementAbsoluteCoords", () => { describe("getElementAbsoluteCoords", () => {
@ -29,14 +29,14 @@ describe("getElementAbsoluteCoords", () => {
it("test x2 coordinate if width is positive or zero", () => { it("test x2 coordinate if width is positive or zero", () => {
const [, , x2] = getElementAbsoluteCoords( const [, , x2] = getElementAbsoluteCoords(
_ce({ x: 10, y: 0, w: 10, h: 0 }) _ce({ x: 10, y: 0, w: 10, h: 0 }),
); );
expect(x2).toEqual(20); expect(x2).toEqual(20);
}); });
it("test x2 coordinate if width is negative", () => { it("test x2 coordinate if width is negative", () => {
const [, , x2] = getElementAbsoluteCoords( const [, , x2] = getElementAbsoluteCoords(
_ce({ x: 10, y: 0, w: -10, h: 0 }) _ce({ x: 10, y: 0, w: -10, h: 0 }),
); );
expect(x2).toEqual(10); expect(x2).toEqual(10);
}); });
@ -53,14 +53,14 @@ describe("getElementAbsoluteCoords", () => {
it("test y2 coordinate if height is positive or zero", () => { it("test y2 coordinate if height is positive or zero", () => {
const [, , , y2] = getElementAbsoluteCoords( const [, , , y2] = getElementAbsoluteCoords(
_ce({ x: 0, y: 10, w: 0, h: 10 }) _ce({ x: 0, y: 10, w: 0, h: 10 }),
); );
expect(y2).toEqual(20); expect(y2).toEqual(20);
}); });
it("test y2 coordinate if height is negative", () => { it("test y2 coordinate if height is negative", () => {
const [, , , y2] = getElementAbsoluteCoords( const [, , , y2] = getElementAbsoluteCoords(
_ce({ x: 0, y: 10, w: 0, h: -10 }) _ce({ x: 0, y: 10, w: 0, h: -10 }),
); );
expect(y2).toEqual(10); expect(y2).toEqual(10);
}); });

@ -10,7 +10,7 @@ export function getElementAbsoluteCoords(element: ExcalidrawElement) {
element.width >= 0 ? element.x : element.x + element.width, // x1 element.width >= 0 ? element.x : element.x + element.width, // x1
element.height >= 0 ? element.y : element.y + element.height, // y1 element.height >= 0 ? element.y : element.y + element.height, // y1
element.width >= 0 ? element.x + element.width : element.x, // x2 element.width >= 0 ? element.x + element.width : element.x, // x2
element.height >= 0 ? element.y + element.height : element.y // y2 element.height >= 0 ? element.y + element.height : element.y, // y2
]; ];
} }

@ -5,7 +5,7 @@ import {
getArrowPoints, getArrowPoints,
getDiamondPoints, getDiamondPoints,
getElementAbsoluteCoords, getElementAbsoluteCoords,
getLinePoints getLinePoints,
} from "./bounds"; } from "./bounds";
function isElementDraggableFromInside(element: ExcalidrawElement): boolean { function isElementDraggableFromInside(element: ExcalidrawElement): boolean {
@ -15,7 +15,7 @@ function isElementDraggableFromInside(element: ExcalidrawElement): boolean {
export function hitTest( export function hitTest(
element: ExcalidrawElement, element: ExcalidrawElement,
x: number, x: number,
y: number y: number,
): boolean { ): boolean {
// For shapes that are composed of lines, we only enable point-selection when the distance // For shapes that are composed of lines, we only enable point-selection when the distance
// of the click is less than x pixels of any of the lines that the shape is composed of // of the click is less than x pixels of any of the lines that the shape is composed of
@ -95,7 +95,7 @@ export function hitTest(
bottomX, bottomX,
bottomY, bottomY,
leftX, leftX,
leftY leftY,
] = getDiamondPoints(element); ] = getDiamondPoints(element);
if (isElementDraggableFromInside(element)) { if (isElementDraggableFromInside(element)) {

@ -5,7 +5,7 @@ type Sides = "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se";
export function handlerRectangles( export function handlerRectangles(
element: ExcalidrawElement, element: ExcalidrawElement,
{ scrollX, scrollY }: SceneScroll { scrollX, scrollY }: SceneScroll,
) { ) {
const elementX1 = element.x; const elementX1 = element.x;
const elementX2 = element.x + element.width; const elementX2 = element.x + element.width;
@ -24,14 +24,14 @@ export function handlerRectangles(
elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4, elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4,
elementY1 - margin + scrollY + marginY, elementY1 - margin + scrollY + marginY,
8, 8,
8 8,
]; ];
handlers["s"] = [ handlers["s"] = [
elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4, elementX1 + (elementX2 - elementX1) / 2 + scrollX - 4,
elementY2 - margin + scrollY - marginY, elementY2 - margin + scrollY - marginY,
8, 8,
8 8,
]; ];
} }
@ -40,14 +40,14 @@ export function handlerRectangles(
elementX1 - margin + scrollX + marginX, elementX1 - margin + scrollX + marginX,
elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4, elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4,
8, 8,
8 8,
]; ];
handlers["e"] = [ handlers["e"] = [
elementX2 - margin + scrollX - marginX, elementX2 - margin + scrollX - marginX,
elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4, elementY1 + (elementY2 - elementY1) / 2 + scrollY - 4,
8, 8,
8 8,
]; ];
} }
@ -55,31 +55,31 @@ export function handlerRectangles(
elementX1 - margin + scrollX + marginX, elementX1 - margin + scrollX + marginX,
elementY1 - margin + scrollY + marginY, elementY1 - margin + scrollY + marginY,
8, 8,
8 8,
]; // nw ]; // nw
handlers["ne"] = [ handlers["ne"] = [
elementX2 - margin + scrollX - marginX, elementX2 - margin + scrollX - marginX,
elementY1 - margin + scrollY + marginY, elementY1 - margin + scrollY + marginY,
8, 8,
8 8,
]; // ne ]; // ne
handlers["sw"] = [ handlers["sw"] = [
elementX1 - margin + scrollX + marginX, elementX1 - margin + scrollX + marginX,
elementY2 - margin + scrollY - marginY, elementY2 - margin + scrollY - marginY,
8, 8,
8 8,
]; // sw ]; // sw
handlers["se"] = [ handlers["se"] = [
elementX2 - margin + scrollX - marginX, elementX2 - margin + scrollX - marginX,
elementY2 - margin + scrollY - marginY, elementY2 - margin + scrollY - marginY,
8, 8,
8 8,
]; // se ]; // se
if (element.type === "arrow" || element.type === "line") { if (element.type === "arrow" || element.type === "line") {
return { return {
nw: handlers.nw, nw: handlers.nw,
se: handlers.se se: handlers.se,
} as typeof handlers; } as typeof handlers;
} }

@ -3,7 +3,7 @@ export {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getDiamondPoints, getDiamondPoints,
getArrowPoints, getArrowPoints,
getLinePoints getLinePoints,
} from "./bounds"; } from "./bounds";
export { handlerRectangles } from "./handlerRectangles"; export { handlerRectangles } from "./handlerRectangles";
@ -15,5 +15,5 @@ export { redrawTextBoundingBox } from "./textElement";
export { export {
getPerfectElementSize, getPerfectElementSize,
isInvisiblySmallElement, isInvisiblySmallElement,
resizePerfectLineForNWHandler resizePerfectLineForNWHandler,
} from "./sizeHelpers"; } from "./sizeHelpers";

@ -16,7 +16,7 @@ export function newElement(
roughness: number, roughness: number,
opacity: number, opacity: number,
width = 0, width = 0,
height = 0 height = 0,
) { ) {
const element = { const element = {
id: nanoid(), id: nanoid(),
@ -33,7 +33,7 @@ export function newElement(
opacity, opacity,
isSelected: false, isSelected: false,
seed: randomSeed(), seed: randomSeed(),
shape: null as Drawable | Drawable[] | null shape: null as Drawable | Drawable[] | null,
}; };
return element; return element;
} }
@ -41,7 +41,7 @@ export function newElement(
export function newTextElement( export function newTextElement(
element: ExcalidrawElement, element: ExcalidrawElement,
text: string, text: string,
font: string font: string,
) { ) {
const metrics = measureText(text, font); const metrics = measureText(text, font);
const textElement: ExcalidrawTextElement = { const textElement: ExcalidrawTextElement = {
@ -54,7 +54,7 @@ export function newTextElement(
y: element.y - metrics.height / 2, y: element.y - metrics.height / 2,
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline baseline: metrics.baseline,
}; };
return textElement; return textElement;

@ -9,7 +9,7 @@ export function resizeTest(
element: ExcalidrawElement, element: ExcalidrawElement,
x: number, x: number,
y: number, y: number,
{ scrollX, scrollY }: SceneScroll { scrollX, scrollY }: SceneScroll,
): HandlerRectanglesRet | false { ): HandlerRectanglesRet | false {
if (!element.isSelected || element.type === "text") return false; if (!element.isSelected || element.type === "text") return false;
@ -36,7 +36,7 @@ export function resizeTest(
export function getElementWithResizeHandler( export function getElementWithResizeHandler(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
{ x, y }: { x: number; y: number }, { x, y }: { x: number; y: number },
{ scrollX, scrollY }: SceneScroll { scrollX, scrollY }: SceneScroll,
) { ) {
return elements.reduce((result, element) => { return elements.reduce((result, element) => {
if (result) { if (result) {
@ -44,7 +44,7 @@ export function getElementWithResizeHandler(
} }
const resizeHandle = resizeTest(element, x, y, { const resizeHandle = resizeTest(element, x, y, {
scrollX, scrollX,
scrollY scrollY,
}); });
return resizeHandle ? { element, resizeHandle } : null; return resizeHandle ? { element, resizeHandle } : null;
}, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null); }, null as { element: ExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);

@ -10,7 +10,7 @@ export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
export function getPerfectElementSize( export function getPerfectElementSize(
elementType: string, elementType: string,
width: number, width: number,
height: number height: number,
): { width: number; height: number } { ): { width: number; height: number } {
const absWidth = Math.abs(width); const absWidth = Math.abs(width);
const absHeight = Math.abs(height); const absHeight = Math.abs(height);
@ -33,7 +33,7 @@ export function getPerfectElementSize(
export function resizePerfectLineForNWHandler( export function resizePerfectLineForNWHandler(
element: ExcalidrawElement, element: ExcalidrawElement,
x: number, x: number,
y: number y: number,
) { ) {
const anchorX = element.x + element.width; const anchorX = element.x + element.width;
const anchorY = element.y + element.height; const anchorY = element.y + element.height;

@ -22,7 +22,7 @@ export function textWysiwyg({
y, y,
strokeColor, strokeColor,
font, font,
onSubmit onSubmit,
}: TextWysiwygParams) { }: TextWysiwygParams) {
// Using contenteditable here as it has dynamic width. // Using contenteditable here as it has dynamic width.
// But this solution has an issue — it allows to paste // But this solution has an issue — it allows to paste
@ -45,7 +45,7 @@ export function textWysiwyg({
padding: "4px", padding: "4px",
outline: "transparent", outline: "transparent",
whiteSpace: "nowrap", whiteSpace: "nowrap",
minHeight: "1em" minHeight: "1em",
}); });
editable.onkeydown = ev => { editable.onkeydown = ev => {

@ -1,7 +1,7 @@
import { ExcalidrawElement, ExcalidrawTextElement } from "./types"; import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
export function isTextElement( export function isTextElement(
element: ExcalidrawElement element: ExcalidrawElement,
): element is ExcalidrawTextElement { ): element is ExcalidrawTextElement {
return element.type === "text"; return element.type === "text";
} }

@ -8,14 +8,14 @@ class SceneHistory {
generateCurrentEntry( generateCurrentEntry(
appState: Partial<AppState>, appState: Partial<AppState>,
elements: readonly ExcalidrawElement[] elements: readonly ExcalidrawElement[],
) { ) {
return JSON.stringify({ return JSON.stringify({
appState, appState,
elements: elements.map(({ shape, ...element }) => ({ elements: elements.map(({ shape, ...element }) => ({
...element, ...element,
isSelected: false isSelected: false,
})) })),
}); });
} }

@ -18,7 +18,7 @@ export const languages = [
{ lng: "en", label: "English" }, { lng: "en", label: "English" },
{ lng: "es", label: "Español" }, { lng: "es", label: "Español" },
{ lng: "fr", label: "Français" }, { lng: "fr", label: "Français" },
{ lng: "pt", label: "Português" } { lng: "pt", label: "Português" },
]; ];
i18n i18n
@ -28,7 +28,7 @@ i18n
.init({ .init({
fallbackLng, fallbackLng,
react: { useSuspense: false }, react: { useSuspense: false },
load: "languageOnly" load: "languageOnly",
}); });
export default i18n; export default i18n;

@ -17,7 +17,7 @@ const elements = [
roughness: 1, roughness: 1,
opacity: 100, opacity: 100,
isSelected: false, isSelected: false,
seed: 749612521 seed: 749612521,
}, },
{ {
id: "7W-iw5pEBPTU3eaCaLtFo", id: "7W-iw5pEBPTU3eaCaLtFo",
@ -33,7 +33,7 @@ const elements = [
roughness: 1, roughness: 1,
opacity: 100, opacity: 100,
isSelected: false, isSelected: false,
seed: 952056308 seed: 952056308,
}, },
{ {
id: "kqKI231mvTrcsYo2DkUsR", id: "kqKI231mvTrcsYo2DkUsR",
@ -52,8 +52,8 @@ const elements = [
seed: 1683771448, seed: 1683771448,
text: "test", text: "test",
font: "20px Virgil", font: "20px Virgil",
baseline: 22 baseline: 22,
} },
]; ];
registerFont("./public/FG_Virgil.ttf", { family: "Virgil" }); registerFont("./public/FG_Virgil.ttf", { family: "Virgil" });
@ -62,9 +62,9 @@ const canvas = getExportCanvasPreview(
{ {
exportBackground: true, exportBackground: true,
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
scale: 1 scale: 1,
}, },
createCanvas createCanvas,
); );
const fs = require("fs"); const fs = require("fs");

@ -15,7 +15,7 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getCursorForResizingElement, getCursorForResizingElement,
getPerfectElementSize, getPerfectElementSize,
resizePerfectLineForNWHandler resizePerfectLineForNWHandler,
} from "./element"; } from "./element";
import { import {
clearSelection, clearSelection,
@ -31,7 +31,7 @@ import {
hasStroke, hasStroke,
hasText, hasText,
exportCanvas, exportCanvas,
importFromBackend importFromBackend,
} from "./scene"; } from "./scene";
import { renderScene } from "./renderer"; import { renderScene } from "./renderer";
@ -71,7 +71,7 @@ import {
actionLoadScene, actionLoadScene,
actionSaveScene, actionSaveScene,
actionCopyStyles, actionCopyStyles,
actionPasteStyles actionPasteStyles,
} from "./actions"; } from "./actions";
import { Action, ActionResult } from "./actions/types"; import { Action, ActionResult } from "./actions/types";
import { getDefaultAppState } from "./appState"; import { getDefaultAppState } from "./appState";
@ -100,7 +100,7 @@ const ELEMENT_TRANSLATE_AMOUNT = 1;
const TEXT_TO_CENTER_SNAP_THRESHOLD = 30; const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
const CURSOR_TYPE = { const CURSOR_TYPE = {
TEXT: "text", TEXT: "text",
CROSSHAIR: "crosshair" CROSSHAIR: "crosshair",
}; };
let lastCanvasWidth = -1; let lastCanvasWidth = -1;
@ -110,7 +110,7 @@ let lastMouseUp: ((e: any) => void) | null = null;
export function viewportCoordsToSceneCoords( export function viewportCoordsToSceneCoords(
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
{ scrollX, scrollY }: { scrollX: number; scrollY: number } { scrollX, scrollY }: { scrollX: number; scrollY: number },
) { ) {
const x = clientX - CANVAS_WINDOW_OFFSET_LEFT - scrollX; const x = clientX - CANVAS_WINDOW_OFFSET_LEFT - scrollX;
const y = clientY - CANVAS_WINDOW_OFFSET_TOP - scrollY; const y = clientY - CANVAS_WINDOW_OFFSET_TOP - scrollY;
@ -118,7 +118,7 @@ export function viewportCoordsToSceneCoords(
} }
function pickAppStatePropertiesForHistory( function pickAppStatePropertiesForHistory(
appState: AppState appState: AppState,
): Partial<AppState> { ): Partial<AppState> {
return { return {
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
@ -126,7 +126,7 @@ function pickAppStatePropertiesForHistory(
currentItemBackgroundColor: appState.currentItemBackgroundColor, currentItemBackgroundColor: appState.currentItemBackgroundColor,
currentItemFont: appState.currentItemFont, currentItemFont: appState.currentItemFont,
viewBackgroundColor: appState.viewBackgroundColor, viewBackgroundColor: appState.viewBackgroundColor,
name: appState.name name: appState.name,
}; };
} }
@ -186,8 +186,8 @@ export class App extends React.Component<any, AppState> {
JSON.stringify( JSON.stringify(
elements elements
.filter(element => element.isSelected) .filter(element => element.isSelected)
.map(({ shape, ...el }) => el) .map(({ shape, ...el }) => el),
) ),
); );
elements = deleteSelectedElements(elements); elements = deleteSelectedElements(elements);
this.forceUpdate(); this.forceUpdate();
@ -200,8 +200,8 @@ export class App extends React.Component<any, AppState> {
JSON.stringify( JSON.stringify(
elements elements
.filter(element => element.isSelected) .filter(element => element.isSelected)
.map(({ shape, ...el }) => el) .map(({ shape, ...el }) => el),
) ),
); );
e.preventDefault(); e.preventDefault();
}; };
@ -257,7 +257,7 @@ export class App extends React.Component<any, AppState> {
document.removeEventListener( document.removeEventListener(
"mousemove", "mousemove",
this.getCurrentCursorPosition, this.getCurrentCursorPosition,
false false,
); );
window.removeEventListener("resize", this.onResize, false); window.removeEventListener("resize", this.onResize, false);
window.removeEventListener("unload", this.onUnload, false); window.removeEventListener("unload", this.onUnload, false);
@ -354,7 +354,7 @@ export class App extends React.Component<any, AppState> {
const text = JSON.stringify( const text = JSON.stringify(
elements elements
.filter(element => element.isSelected) .filter(element => element.isSelected)
.map(({ shape, ...el }) => el) .map(({ shape, ...el }) => el),
); );
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
} }
@ -393,7 +393,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{(hasBackground(elements) || {(hasBackground(elements) ||
@ -404,7 +404,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{this.actionManager.renderAction( {this.actionManager.renderAction(
@ -412,7 +412,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
<hr /> <hr />
</> </>
@ -426,7 +426,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{this.actionManager.renderAction( {this.actionManager.renderAction(
@ -434,7 +434,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
<hr /> <hr />
</> </>
@ -447,7 +447,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{this.actionManager.renderAction( {this.actionManager.renderAction(
@ -455,7 +455,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
<hr /> <hr />
</> </>
@ -466,7 +466,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{this.actionManager.renderAction( {this.actionManager.renderAction(
@ -474,7 +474,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
</div> </div>
</Island> </Island>
@ -533,14 +533,14 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
{this.actionManager.renderAction( {this.actionManager.renderAction(
"saveScene", "saveScene",
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
<ExportDialog <ExportDialog
elements={elements} elements={elements}
@ -553,7 +553,7 @@ export class App extends React.Component<any, AppState> {
exportBackground: this.state.exportBackground, exportBackground: this.state.exportBackground,
name: this.state.name, name: this.state.name,
viewBackgroundColor: this.state.viewBackgroundColor, viewBackgroundColor: this.state.viewBackgroundColor,
scale scale,
}); });
}} }}
onExportToClipboard={(exportedElements, scale) => { onExportToClipboard={(exportedElements, scale) => {
@ -562,7 +562,7 @@ export class App extends React.Component<any, AppState> {
exportBackground: this.state.exportBackground, exportBackground: this.state.exportBackground,
name: this.state.name, name: this.state.name,
viewBackgroundColor: this.state.viewBackgroundColor, viewBackgroundColor: this.state.viewBackgroundColor,
scale scale,
}); });
}} }}
onExportToBackend={exportedElements => { onExportToBackend={exportedElements => {
@ -571,10 +571,10 @@ export class App extends React.Component<any, AppState> {
"backend", "backend",
exportedElements.map(element => ({ exportedElements.map(element => ({
...element, ...element,
isSelected: false isSelected: false,
})), })),
this.canvas, this.canvas,
this.state this.state,
); );
}} }}
/> />
@ -583,7 +583,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
</Stack.Row> </Stack.Row>
{this.actionManager.renderAction( {this.actionManager.renderAction(
@ -591,7 +591,7 @@ export class App extends React.Component<any, AppState> {
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
t t,
)} )}
</Stack.Col> </Stack.Col>
); );
@ -626,7 +626,7 @@ export class App extends React.Component<any, AppState> {
id="canvas" id="canvas"
style={{ style={{
width: canvasWidth, width: canvasWidth,
height: canvasHeight height: canvasHeight,
}} }}
width={canvasWidth * window.devicePixelRatio} width={canvasWidth * window.devicePixelRatio}
height={canvasHeight * window.devicePixelRatio} height={canvasHeight * window.devicePixelRatio}
@ -641,7 +641,7 @@ export class App extends React.Component<any, AppState> {
} }
if (canvas) { if (canvas) {
canvas.addEventListener("wheel", this.handleWheel, { canvas.addEventListener("wheel", this.handleWheel, {
passive: false passive: false,
}); });
this.removeWheelEventListener = () => this.removeWheelEventListener = () =>
canvas.removeEventListener("wheel", this.handleWheel); canvas.removeEventListener("wheel", this.handleWheel);
@ -670,18 +670,18 @@ export class App extends React.Component<any, AppState> {
options: [ options: [
navigator.clipboard && { navigator.clipboard && {
label: t("labels.paste"), label: t("labels.paste"),
action: () => this.pasteFromClipboard() action: () => this.pasteFromClipboard(),
}, },
...this.actionManager.getContextMenuItems( ...this.actionManager.getContextMenuItems(
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
action => this.canvasOnlyActions.includes(action), action => this.canvasOnlyActions.includes(action),
t t,
) ),
], ],
top: e.clientY, top: e.clientY,
left: e.clientX left: e.clientX,
}); });
return; return;
} }
@ -696,22 +696,22 @@ export class App extends React.Component<any, AppState> {
options: [ options: [
navigator.clipboard && { navigator.clipboard && {
label: t("labels.copy"), label: t("labels.copy"),
action: this.copyToClipboard action: this.copyToClipboard,
}, },
navigator.clipboard && { navigator.clipboard && {
label: t("labels.paste"), label: t("labels.paste"),
action: () => this.pasteFromClipboard() action: () => this.pasteFromClipboard(),
}, },
...this.actionManager.getContextMenuItems( ...this.actionManager.getContextMenuItems(
elements, elements,
this.state, this.state,
this.syncActionResult, this.syncActionResult,
action => !this.canvasOnlyActions.includes(action), action => !this.canvasOnlyActions.includes(action),
t t,
) ),
], ],
top: e.clientY, top: e.clientY,
left: e.clientX left: e.clientX,
}); });
}} }}
onMouseDown={e => { onMouseDown={e => {
@ -733,7 +733,7 @@ export class App extends React.Component<any, AppState> {
lastY = e.clientY; lastY = e.clientY;
this.setState(state => ({ this.setState(state => ({
scrollX: state.scrollX - deltaX, scrollX: state.scrollX - deltaX,
scrollY: state.scrollY - deltaY scrollY: state.scrollY - deltaY,
})); }));
}; };
const onMouseUp = (lastMouseUp = (e: MouseEvent) => { const onMouseUp = (lastMouseUp = (e: MouseEvent) => {
@ -743,7 +743,7 @@ export class App extends React.Component<any, AppState> {
window.removeEventListener("mouseup", onMouseUp); window.removeEventListener("mouseup", onMouseUp);
}); });
window.addEventListener("mousemove", onMouseMove, { window.addEventListener("mousemove", onMouseMove, {
passive: true passive: true,
}); });
window.addEventListener("mouseup", onMouseUp); window.addEventListener("mouseup", onMouseUp);
return; return;
@ -763,7 +763,7 @@ export class App extends React.Component<any, AppState> {
// Handle scrollbars dragging // Handle scrollbars dragging
const { const {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar isOverVerticalScrollBar,
} = isOverScrollBars( } = isOverScrollBars(
elements, elements,
e.clientX - CANVAS_WINDOW_OFFSET_LEFT, e.clientX - CANVAS_WINDOW_OFFSET_LEFT,
@ -771,7 +771,7 @@ export class App extends React.Component<any, AppState> {
canvasWidth, canvasWidth,
canvasHeight, canvasHeight,
this.state.scrollX, this.state.scrollX,
this.state.scrollY this.state.scrollY,
); );
const { x, y } = viewportCoordsToSceneCoords(e, this.state); const { x, y } = viewportCoordsToSceneCoords(e, this.state);
@ -785,7 +785,7 @@ export class App extends React.Component<any, AppState> {
"hachure", "hachure",
1, 1,
1, 1,
100 100,
); );
if (isTextElement(element)) { if (isTextElement(element)) {
@ -802,16 +802,16 @@ export class App extends React.Component<any, AppState> {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, elements,
{ x, y }, { x, y },
this.state this.state,
); );
this.setState({ this.setState({
resizingElement: resizeElement ? resizeElement.element : null resizingElement: resizeElement ? resizeElement.element : null,
}); });
if (resizeElement) { if (resizeElement) {
resizeHandle = resizeElement.resizeHandle; resizeHandle = resizeElement.resizeHandle;
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement(
resizeElement resizeElement,
); );
isResizingElements = true; isResizingElements = true;
} else { } else {
@ -837,7 +837,7 @@ export class App extends React.Component<any, AppState> {
elements = [ elements = [
...elements.map(element => ({ ...elements.map(element => ({
...element, ...element,
isSelected: false isSelected: false,
})), })),
...elements ...elements
.filter(element => element.isSelected) .filter(element => element.isSelected)
@ -845,7 +845,7 @@ export class App extends React.Component<any, AppState> {
const newElement = duplicateElement(element); const newElement = duplicateElement(element);
newElement.isSelected = true; newElement.isSelected = true;
return newElement; return newElement;
}) }),
]; ];
} }
} }
@ -860,7 +860,7 @@ export class App extends React.Component<any, AppState> {
if (!e.altKey) { if (!e.altKey) {
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition( const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
x, x,
y y,
); );
if (snappedToCenterPosition) { if (snappedToCenterPosition) {
element.x = snappedToCenterPosition.elementCenterX; element.x = snappedToCenterPosition.elementCenterX;
@ -884,22 +884,22 @@ export class App extends React.Component<any, AppState> {
...newTextElement( ...newTextElement(
element, element,
text, text,
this.state.currentItemFont this.state.currentItemFont,
), ),
isSelected: true isSelected: true,
} },
]; ];
} }
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
editingElement: null, editingElement: null,
elementType: "selection" elementType: "selection",
}); });
} },
}); });
this.setState({ this.setState({
elementType: "selection", elementType: "selection",
editingElement: element editingElement: element,
}); });
return; return;
} }
@ -993,7 +993,7 @@ export class App extends React.Component<any, AppState> {
const { width, height } = getPerfectElementSize( const { width, height } = getPerfectElementSize(
element.type, element.type,
x - element.x, x - element.x,
y - element.y y - element.y,
); );
element.width = width; element.width = width;
element.height = height; element.height = height;
@ -1023,7 +1023,7 @@ export class App extends React.Component<any, AppState> {
} }
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement(
{ element, resizeHandle } { element, resizeHandle },
); );
el.x = element.x; el.x = element.x;
el.y = element.y; el.y = element.y;
@ -1078,11 +1078,11 @@ export class App extends React.Component<any, AppState> {
if (e.shiftKey) { if (e.shiftKey) {
let { let {
width: newWidth, width: newWidth,
height: newHeight height: newHeight,
} = getPerfectElementSize( } = getPerfectElementSize(
this.state.elementType, this.state.elementType,
width, width,
height height,
); );
draggingElement.width = newWidth; draggingElement.width = newWidth;
draggingElement.height = newHeight; draggingElement.height = newHeight;
@ -1099,7 +1099,7 @@ export class App extends React.Component<any, AppState> {
} }
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
elements, elements,
draggingElement draggingElement,
); );
elementsWithinSelection.forEach(element => { elementsWithinSelection.forEach(element => {
element.isSelected = true; element.isSelected = true;
@ -1115,7 +1115,7 @@ export class App extends React.Component<any, AppState> {
draggingElement, draggingElement,
resizingElement, resizingElement,
elementType, elementType,
elementLocked elementLocked,
} = this.state; } = this.state;
lastMouseUp = null; lastMouseUp = null;
@ -1130,7 +1130,7 @@ export class App extends React.Component<any, AppState> {
// remove invisible element which was added in onMouseDown // remove invisible element which was added in onMouseDown
elements = elements.slice(0, -1); elements = elements.slice(0, -1);
this.setState({ this.setState({
draggingElement: null draggingElement: null,
}); });
this.forceUpdate(); this.forceUpdate();
return; return;
@ -1179,7 +1179,7 @@ export class App extends React.Component<any, AppState> {
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
elementType: "selection" elementType: "selection",
}); });
} }
@ -1214,10 +1214,10 @@ export class App extends React.Component<any, AppState> {
"hachure", "hachure",
1, 1,
1, 1,
100 100,
), ),
"", // default text "", // default text
this.state.currentItemFont // default font this.state.currentItemFont, // default font
); );
this.setState({ editingElement: element }); this.setState({ editingElement: element });
@ -1227,7 +1227,7 @@ export class App extends React.Component<any, AppState> {
if (elementAtPosition && isTextElement(elementAtPosition)) { if (elementAtPosition && isTextElement(elementAtPosition)) {
elements = elements.filter( elements = elements.filter(
element => element.id !== elementAtPosition.id element => element.id !== elementAtPosition.id,
); );
this.forceUpdate(); this.forceUpdate();
@ -1248,7 +1248,7 @@ export class App extends React.Component<any, AppState> {
} else if (!e.altKey) { } else if (!e.altKey) {
const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition( const snappedToCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
x, x,
y y,
); );
if (snappedToCenterPosition) { if (snappedToCenterPosition) {
@ -1273,16 +1273,16 @@ export class App extends React.Component<any, AppState> {
// we need to recreate the element to update dimensions & // we need to recreate the element to update dimensions &
// position // position
...newTextElement(element, text, element.font), ...newTextElement(element, text, element.font),
isSelected: true isSelected: true,
} },
]; ];
} }
this.setState({ this.setState({
draggingElement: null, draggingElement: null,
editingElement: null, editingElement: null,
elementType: "selection" elementType: "selection",
}); });
} },
}); });
}} }}
onMouseMove={e => { onMouseMove={e => {
@ -1296,11 +1296,11 @@ export class App extends React.Component<any, AppState> {
const resizeElement = getElementWithResizeHandler( const resizeElement = getElementWithResizeHandler(
elements, elements,
{ x, y }, { x, y },
this.state this.state,
); );
if (resizeElement && resizeElement.resizeHandle) { if (resizeElement && resizeElement.resizeHandle) {
document.documentElement.style.cursor = getCursorForResizingElement( document.documentElement.style.cursor = getCursorForResizingElement(
resizeElement resizeElement,
); );
return; return;
} }
@ -1327,7 +1327,7 @@ export class App extends React.Component<any, AppState> {
const { deltaX, deltaY } = e; const { deltaX, deltaY } = e;
this.setState(state => ({ this.setState(state => ({
scrollX: state.scrollX - deltaX, scrollX: state.scrollX - deltaX,
scrollY: state.scrollY - deltaY scrollY: state.scrollY - deltaY,
})); }));
}; };
@ -1384,7 +1384,7 @@ export class App extends React.Component<any, AppState> {
duplicate.x += dx - minX; duplicate.x += dx - minX;
duplicate.y += dy - minY; duplicate.y += dy - minY;
return duplicate; return duplicate;
}) }),
]; ];
this.forceUpdate(); this.forceUpdate();
} }
@ -1399,7 +1399,7 @@ export class App extends React.Component<any, AppState> {
elementClickedInside.y + elementClickedInside.height / 2; elementClickedInside.y + elementClickedInside.height / 2;
const distanceToCenter = Math.hypot( const distanceToCenter = Math.hypot(
x - elementCenterX, x - elementCenterX,
y - elementCenterY y - elementCenterY,
); );
const isSnappedToCenter = const isSnappedToCenter =
distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD; distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD;
@ -1427,15 +1427,15 @@ export class App extends React.Component<any, AppState> {
renderScene(elements, this.rc!, this.canvas!, { renderScene(elements, this.rc!, this.canvas!, {
scrollX: this.state.scrollX, scrollX: this.state.scrollX,
scrollY: this.state.scrollY, scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor viewBackgroundColor: this.state.viewBackgroundColor,
}); });
this.saveDebounced(); this.saveDebounced();
if (history.isRecording()) { if (history.isRecording()) {
history.pushEntry( history.pushEntry(
history.generateCurrentEntry( history.generateCurrentEntry(
pickAppStatePropertiesForHistory(this.state), pickAppStatePropertiesForHistory(this.state),
elements elements,
) ),
); );
} }
} }
@ -1453,7 +1453,7 @@ class TopErrorBoundary extends React.Component {
return { return {
hasError: true, hasError: true,
localStorage: JSON.stringify({ ...localStorage }), localStorage: JSON.stringify({ ...localStorage }),
stack: error.stack stack: error.stack,
}; };
} }
@ -1471,7 +1471,7 @@ class TopErrorBoundary extends React.Component {
} catch {} } catch {}
window.open( window.open(
`https://github.com/excalidraw/excalidraw/issues/new?body=${body}` `https://github.com/excalidraw/excalidraw/issues/new?body=${body}`,
); );
} }
@ -1539,5 +1539,5 @@ ReactDOM.render(
<TopErrorBoundary> <TopErrorBoundary>
<AppWithTrans /> <AppWithTrans />
</TopErrorBoundary>, </TopErrorBoundary>,
rootElement rootElement,
); );

@ -11,7 +11,7 @@ export const KEYS = {
return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform) return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
? "metaKey" ? "metaKey"
: "ctrlKey"; : "ctrlKey";
} },
}; };
export function isArrowKey(keyCode: string) { export function isArrowKey(keyCode: string) {

@ -5,7 +5,7 @@ export function distanceBetweenPointAndSegment(
x1: number, x1: number,
y1: number, y1: number,
x2: number, x2: number,
y2: number y2: number,
) { ) {
const A = x - x1; const A = x - x1;
const B = y - y1; const B = y - y1;
@ -42,13 +42,13 @@ export function rotate(
y1: number, y1: number,
x2: number, x2: number,
y2: number, y2: number,
angle: number angle: number,
) { ) {
// 𝑎𝑥=(𝑎𝑥𝑐𝑥)cos𝜃(𝑎𝑦𝑐𝑦)sin𝜃+𝑐𝑥 // 𝑎𝑥=(𝑎𝑥𝑐𝑥)cos𝜃(𝑎𝑦𝑐𝑦)sin𝜃+𝑐𝑥
// 𝑎𝑦=(𝑎𝑥𝑐𝑥)sin𝜃+(𝑎𝑦𝑐𝑦)cos𝜃+𝑐𝑦. // 𝑎𝑦=(𝑎𝑥𝑐𝑥)sin𝜃+(𝑎𝑦𝑐𝑦)cos𝜃+𝑐𝑦.
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line // https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
return [ return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2, (x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2 (x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
]; ];
} }

@ -3,7 +3,7 @@ import { isTextElement } from "../element/typeChecks";
import { import {
getDiamondPoints, getDiamondPoints,
getArrowPoints, getArrowPoints,
getLinePoints getLinePoints,
} from "../element/bounds"; } from "../element/bounds";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core"; import { Drawable } from "roughjs/bin/core";
@ -11,7 +11,7 @@ import { Drawable } from "roughjs/bin/core";
export function renderElement( export function renderElement(
element: ExcalidrawElement, element: ExcalidrawElement,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D context: CanvasRenderingContext2D,
) { ) {
const generator = rc.generator; const generator = rc.generator;
if (element.type === "selection") { if (element.type === "selection") {
@ -30,7 +30,7 @@ export function renderElement(
fillStyle: element.fillStyle, fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth, strokeWidth: element.strokeWidth,
roughness: element.roughness, roughness: element.roughness,
seed: element.seed seed: element.seed,
}); });
} }
@ -47,14 +47,14 @@ export function renderElement(
bottomX, bottomX,
bottomY, bottomY,
leftX, leftX,
leftY leftY,
] = getDiamondPoints(element); ] = getDiamondPoints(element);
element.shape = generator.polygon( element.shape = generator.polygon(
[ [
[topX, topY], [topX, topY],
[rightX, rightY], [rightX, rightY],
[bottomX, bottomY], [bottomX, bottomY],
[leftX, leftY] [leftX, leftY],
], ],
{ {
stroke: element.strokeColor, stroke: element.strokeColor,
@ -65,8 +65,8 @@ export function renderElement(
fillStyle: element.fillStyle, fillStyle: element.fillStyle,
strokeWidth: element.strokeWidth, strokeWidth: element.strokeWidth,
roughness: element.roughness, roughness: element.roughness,
seed: element.seed seed: element.seed,
} },
); );
} }
@ -90,8 +90,8 @@ export function renderElement(
strokeWidth: element.strokeWidth, strokeWidth: element.strokeWidth,
roughness: element.roughness, roughness: element.roughness,
seed: element.seed, seed: element.seed,
curveFitting: 1 curveFitting: 1,
} },
); );
} }
@ -104,7 +104,7 @@ export function renderElement(
stroke: element.strokeColor, stroke: element.strokeColor,
strokeWidth: element.strokeWidth, strokeWidth: element.strokeWidth,
roughness: element.roughness, roughness: element.roughness,
seed: element.seed seed: element.seed,
}; };
if (!element.shape) { if (!element.shape) {
@ -114,7 +114,7 @@ export function renderElement(
// ----- // -----
generator.line(x1, y1, x2, y2, options), generator.line(x1, y1, x2, y2, options),
// / // /
generator.line(x4, y4, x2, y2, options) generator.line(x4, y4, x2, y2, options),
]; ];
} }
@ -128,7 +128,7 @@ export function renderElement(
stroke: element.strokeColor, stroke: element.strokeColor,
strokeWidth: element.strokeWidth, strokeWidth: element.strokeWidth,
roughness: element.roughness, roughness: element.roughness,
seed: element.seed seed: element.seed,
}; };
if (!element.shape) { if (!element.shape) {
@ -147,7 +147,7 @@ export function renderElement(
context.fillText( context.fillText(
element.text, element.text,
0, 0,
element.baseline || element.actualBoundingBoxAscent || 0 element.baseline || element.actualBoundingBoxAscent || 0,
); );
context.fillStyle = fillStyle; context.fillStyle = fillStyle;
context.font = font; context.font = font;

@ -8,7 +8,7 @@ import { SceneState } from "../scene/types";
import { import {
getScrollBars, getScrollBars,
SCROLLBAR_COLOR, SCROLLBAR_COLOR,
SCROLLBAR_WIDTH SCROLLBAR_WIDTH,
} from "../scene/scrollbars"; } from "../scene/scrollbars";
import { renderElement } from "./renderElement"; import { renderElement } from "./renderElement";
@ -23,13 +23,13 @@ export function renderScene(
offsetX, offsetX,
offsetY, offsetY,
renderScrollbars = true, renderScrollbars = true,
renderSelection = true renderSelection = true,
}: { }: {
offsetX?: number; offsetX?: number;
offsetY?: number; offsetY?: number;
renderScrollbars?: boolean; renderScrollbars?: boolean;
renderSelection?: boolean; renderSelection?: boolean;
} = {} } = {},
) { ) {
if (!canvas) return; if (!canvas) return;
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
@ -53,7 +53,7 @@ export function renderScene(
sceneState = { sceneState = {
...sceneState, ...sceneState,
scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX, scrollX: typeof offsetX === "number" ? offsetX : sceneState.scrollX,
scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY scrollY: typeof offsetY === "number" ? offsetY : sceneState.scrollY,
}; };
elements.forEach(element => { elements.forEach(element => {
@ -65,19 +65,19 @@ export function renderScene(
// If canvas is scaled for high pixelDeviceRatio width and height // If canvas is scaled for high pixelDeviceRatio width and height
// setted in the `style` attribute // setted in the `style` attribute
parseInt(canvas.style.width) || canvas.width, parseInt(canvas.style.width) || canvas.width,
parseInt(canvas.style.height) || canvas.height parseInt(canvas.style.height) || canvas.height,
) )
) { ) {
return; return;
} }
context.translate( context.translate(
element.x + sceneState.scrollX, element.x + sceneState.scrollX,
element.y + sceneState.scrollY element.y + sceneState.scrollY,
); );
renderElement(element, rc, context); renderElement(element, rc, context);
context.translate( context.translate(
-element.x - sceneState.scrollX, -element.x - sceneState.scrollX,
-element.y - sceneState.scrollY -element.y - sceneState.scrollY,
); );
}); });
@ -91,7 +91,7 @@ export function renderScene(
elementX1, elementX1,
elementY1, elementY1,
elementX2, elementX2,
elementY2 elementY2,
] = getElementAbsoluteCoords(element); ] = getElementAbsoluteCoords(element);
const lineDash = context.getLineDash(); const lineDash = context.getLineDash();
context.setLineDash([8, 4]); context.setLineDash([8, 4]);
@ -99,7 +99,7 @@ export function renderScene(
elementX1 - margin + sceneState.scrollX, elementX1 - margin + sceneState.scrollX,
elementY1 - margin + sceneState.scrollY, elementY1 - margin + sceneState.scrollY,
elementX2 - elementX1 + margin * 2, elementX2 - elementX1 + margin * 2,
elementY2 - elementY1 + margin * 2 elementY2 - elementY1 + margin * 2,
); );
context.setLineDash(lineDash); context.setLineDash(lineDash);
}); });
@ -118,7 +118,7 @@ export function renderScene(
context.canvas.width / window.devicePixelRatio, context.canvas.width / window.devicePixelRatio,
context.canvas.height / window.devicePixelRatio, context.canvas.height / window.devicePixelRatio,
sceneState.scrollX, sceneState.scrollX,
sceneState.scrollY sceneState.scrollY,
); );
const strokeStyle = context.strokeStyle; const strokeStyle = context.strokeStyle;
@ -132,7 +132,7 @@ export function renderScene(
scrollBar.y, scrollBar.y,
scrollBar.width, scrollBar.width,
scrollBar.height, scrollBar.height,
SCROLLBAR_WIDTH / 2 SCROLLBAR_WIDTH / 2,
); );
}); });
context.strokeStyle = strokeStyle; context.strokeStyle = strokeStyle;
@ -145,7 +145,7 @@ function isVisibleElement(
scrollX: number, scrollX: number,
scrollY: number, scrollY: number,
canvasWidth: number, canvasWidth: number,
canvasHeight: number canvasHeight: number,
) { ) {
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element); let [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
x1 += scrollX; x1 += scrollX;

@ -14,7 +14,7 @@ export function roundRect(
y: number, y: number,
width: number, width: number,
height: number, height: number,
radius: number radius: number,
) { ) {
context.beginPath(); context.beginPath();
context.moveTo(x + radius, y); context.moveTo(x + radius, y);
@ -25,7 +25,7 @@ export function roundRect(
x + width, x + width,
y + height, y + height,
x + width - radius, x + width - radius,
y + height y + height,
); );
context.lineTo(x + radius, y + height); context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - radius); context.quadraticCurveTo(x, y + height, x, y + height - radius);

@ -8,7 +8,7 @@ export const hasBackground = (elements: readonly ExcalidrawElement[]) =>
element.isSelected && element.isSelected &&
(element.type === "rectangle" || (element.type === "rectangle" ||
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "diamond") element.type === "diamond"),
); );
export const hasStroke = (elements: readonly ExcalidrawElement[]) => export const hasStroke = (elements: readonly ExcalidrawElement[]) =>
@ -19,7 +19,7 @@ export const hasStroke = (elements: readonly ExcalidrawElement[]) =>
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "diamond" || element.type === "diamond" ||
element.type === "arrow" || element.type === "arrow" ||
element.type === "line") element.type === "line"),
); );
export const hasText = (elements: readonly ExcalidrawElement[]) => export const hasText = (elements: readonly ExcalidrawElement[]) =>
@ -28,7 +28,7 @@ export const hasText = (elements: readonly ExcalidrawElement[]) =>
export function getElementAtPosition( export function getElementAtPosition(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
x: number, x: number,
y: number y: number,
) { ) {
let hitElement = null; let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array) // We need to to hit testing from front (end of the array) to back (beginning of the array)
@ -45,7 +45,7 @@ export function getElementAtPosition(
export function getElementContainingPosition( export function getElementContainingPosition(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
x: number, x: number,
y: number y: number,
) { ) {
let hitElement = null; let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array) // We need to to hit testing from front (end of the array) to back (beginning of the array)

@ -27,19 +27,19 @@ interface DataState {
export function serializeAsJSON( export function serializeAsJSON(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState?: AppState appState?: AppState,
): string { ): string {
return JSON.stringify({ return JSON.stringify({
version: 1, version: 1,
source: window.location.origin, source: window.location.origin,
elements: elements.map(({ shape, ...el }) => el), elements: elements.map(({ shape, ...el }) => el),
appState: appState || getDefaultAppState() appState: appState || getDefaultAppState(),
}); });
} }
export async function saveAsJSON( export async function saveAsJSON(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState appState: AppState,
) { ) {
const serialized = serializeAsJSON(elements, appState); const serialized = serializeAsJSON(elements, appState);
@ -48,9 +48,9 @@ export async function saveAsJSON(
new Blob([serialized], { type: "application/json" }), new Blob([serialized], { type: "application/json" }),
{ {
fileName: name, fileName: name,
description: "Excalidraw file" description: "Excalidraw file",
}, },
(window as any).handle (window as any).handle,
); );
} }
@ -72,7 +72,7 @@ export async function loadFromJSON() {
const blob = await fileOpen({ const blob = await fileOpen({
description: "Excalidraw files", description: "Excalidraw files",
extensions: ["json"], extensions: ["json"],
mimeTypes: ["application/json"] mimeTypes: ["application/json"],
}); });
if (blob.handle) { if (blob.handle) {
(window as any).handle = blob.handle; (window as any).handle = blob.handle;
@ -101,12 +101,12 @@ export async function loadFromJSON() {
export async function exportToBackend( export async function exportToBackend(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState appState: AppState,
) { ) {
const response = await fetch(BACKEND_POST, { const response = await fetch(BACKEND_POST, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: serializeAsJSON(elements, appState) body: serializeAsJSON(elements, appState),
}); });
const json = await response.json(); const json = await response.json();
if (json.id) { if (json.id) {
@ -117,8 +117,8 @@ export async function exportToBackend(
window.alert( window.alert(
i18n.t("alerts.copiedToClipboard", { i18n.t("alerts.copiedToClipboard", {
url: url.toString(), url: url.toString(),
interpolation: { escapeValue: false } interpolation: { escapeValue: false },
}) }),
); );
} else { } else {
window.alert(i18n.t("alerts.couldNotCreateShareableLink")); window.alert(i18n.t("alerts.couldNotCreateShareableLink"));
@ -129,7 +129,7 @@ export async function importFromBackend(id: string | null) {
let elements: readonly ExcalidrawElement[] = []; let elements: readonly ExcalidrawElement[] = [];
let appState: AppState = getDefaultAppState(); let appState: AppState = getDefaultAppState();
const response = await fetch(`${BACKEND_GET}${id}.json`).then(data => const response = await fetch(`${BACKEND_GET}${id}.json`).then(data =>
data.clone().json() data.clone().json(),
); );
if (response != null) { if (response != null) {
try { try {
@ -152,14 +152,14 @@ export async function exportCanvas(
exportPadding = 10, exportPadding = 10,
viewBackgroundColor, viewBackgroundColor,
name, name,
scale = 1 scale = 1,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
name: string; name: string;
scale?: number; scale?: number;
} },
) { ) {
if (!elements.length) if (!elements.length)
return window.alert(i18n.t("alerts.cannotExportEmptyCanvas")); return window.alert(i18n.t("alerts.cannotExportEmptyCanvas"));
@ -169,7 +169,7 @@ export async function exportCanvas(
exportBackground, exportBackground,
viewBackgroundColor, viewBackgroundColor,
exportPadding, exportPadding,
scale scale,
}); });
tempCanvas.style.display = "none"; tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas); document.body.appendChild(tempCanvas);
@ -180,7 +180,7 @@ export async function exportCanvas(
if (blob) { if (blob) {
await fileSave(blob, { await fileSave(blob, {
fileName: fileName, fileName: fileName,
description: "Excalidraw image" description: "Excalidraw image",
}); });
} }
}); });
@ -190,7 +190,7 @@ export async function exportCanvas(
tempCanvas.toBlob(async function(blob: any) { tempCanvas.toBlob(async function(blob: any) {
try { try {
await navigator.clipboard.write([ await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }) new window.ClipboardItem({ "image/png": blob }),
]); ]);
} catch (err) { } catch (err) {
window.alert(errorMsg); window.alert(errorMsg);
@ -213,7 +213,7 @@ export async function exportCanvas(
function restore( function restore(
savedElements: readonly ExcalidrawElement[], savedElements: readonly ExcalidrawElement[],
savedState: AppState savedState: AppState,
): DataState { ): DataState {
return { return {
elements: savedElements.map(element => ({ elements: savedElements.map(element => ({
@ -225,9 +225,9 @@ function restore(
opacity: opacity:
element.opacity === null || element.opacity === undefined element.opacity === null || element.opacity === undefined
? 100 ? 100
: element.opacity : element.opacity,
})), })),
appState: savedState appState: savedState,
}; };
} }
@ -239,7 +239,7 @@ export function restoreFromLocalStorage() {
if (savedElements) { if (savedElements) {
try { try {
elements = JSON.parse(savedElements).map( elements = JSON.parse(savedElements).map(
({ shape, ...element }: ExcalidrawElement) => element ({ shape, ...element }: ExcalidrawElement) => element,
); );
} catch (e) { } catch (e) {
// Do nothing because elements array is already empty // Do nothing because elements array is already empty
@ -260,7 +260,7 @@ export function restoreFromLocalStorage() {
export function saveToLocalStorage( export function saveToLocalStorage(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
state: AppState state: AppState,
) { ) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements)); localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state)); localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state));

@ -9,7 +9,7 @@ export function getExportCanvasPreview(
exportBackground, exportBackground,
exportPadding = 10, exportPadding = 10,
viewBackgroundColor, viewBackgroundColor,
scale = 1 scale = 1,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
@ -18,13 +18,13 @@ export function getExportCanvasPreview(
}, },
createCanvas: (width: number, height: number) => any = function( createCanvas: (width: number, height: number) => any = function(
width, width,
height height,
) { ) {
const tempCanvas = document.createElement("canvas"); const tempCanvas = document.createElement("canvas");
tempCanvas.width = width * scale; tempCanvas.width = width * scale;
tempCanvas.height = height * scale; tempCanvas.height = height * scale;
return tempCanvas; return tempCanvas;
} },
) { ) {
// calculate smallest area to fit the contents in // calculate smallest area to fit the contents in
let subCanvasX1 = Infinity; let subCanvasX1 = Infinity;
@ -56,14 +56,14 @@ export function getExportCanvasPreview(
{ {
viewBackgroundColor: exportBackground ? viewBackgroundColor : null, viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: 0, scrollX: 0,
scrollY: 0 scrollY: 0,
}, },
{ {
offsetX: -subCanvasX1 + exportPadding, offsetX: -subCanvasX1 + exportPadding,
offsetY: -subCanvasY1 + exportPadding, offsetY: -subCanvasY1 + exportPadding,
renderScrollbars: false, renderScrollbars: false,
renderSelection: false renderSelection: false,
} },
); );
return tempCanvas; return tempCanvas;
} }

@ -5,7 +5,7 @@ export {
deleteSelectedElements, deleteSelectedElements,
someElementIsSelected, someElementIsSelected,
getElementsWithinSelection, getElementsWithinSelection,
getCommonAttributeOfSelectedElements getCommonAttributeOfSelectedElements,
} from "./selection"; } from "./selection";
export { export {
exportCanvas, exportCanvas,
@ -14,13 +14,13 @@ export {
restoreFromLocalStorage, restoreFromLocalStorage,
saveToLocalStorage, saveToLocalStorage,
exportToBackend, exportToBackend,
importFromBackend importFromBackend,
} from "./data"; } from "./data";
export { export {
hasBackground, hasBackground,
hasStroke, hasStroke,
getElementAtPosition, getElementAtPosition,
getElementContainingPosition, getElementContainingPosition,
hasText hasText,
} from "./comparisons"; } from "./comparisons";
export { createScene } from "./createScene"; export { createScene } from "./createScene";

@ -11,7 +11,7 @@ export function getScrollBars(
canvasWidth: number, canvasWidth: number,
canvasHeight: number, canvasHeight: number,
scrollX: number, scrollX: number,
scrollY: number scrollY: number,
) { ) {
let minX = Infinity; let minX = Infinity;
let maxX = 0; let maxX = 0;
@ -41,14 +41,14 @@ export function getScrollBars(
horizontalScrollBar = { horizontalScrollBar = {
x: Math.min( x: Math.min(
leftOverflow + SCROLLBAR_MARGIN, leftOverflow + SCROLLBAR_MARGIN,
canvasWidth - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN canvasWidth - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN,
), ),
y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, y: canvasHeight - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
width: Math.max( width: Math.max(
canvasWidth - rightOverflow - leftOverflow - SCROLLBAR_MARGIN * 2, canvasWidth - rightOverflow - leftOverflow - SCROLLBAR_MARGIN * 2,
SCROLLBAR_MIN_SIZE SCROLLBAR_MIN_SIZE,
), ),
height: SCROLLBAR_WIDTH height: SCROLLBAR_WIDTH,
}; };
} }
@ -59,19 +59,19 @@ export function getScrollBars(
x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN, x: canvasWidth - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
y: Math.min( y: Math.min(
topOverflow + SCROLLBAR_MARGIN, topOverflow + SCROLLBAR_MARGIN,
canvasHeight - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN canvasHeight - SCROLLBAR_MIN_SIZE - SCROLLBAR_MARGIN,
), ),
width: SCROLLBAR_WIDTH, width: SCROLLBAR_WIDTH,
height: Math.max( height: Math.max(
canvasHeight - bottomOverflow - topOverflow - SCROLLBAR_WIDTH * 2, canvasHeight - bottomOverflow - topOverflow - SCROLLBAR_WIDTH * 2,
SCROLLBAR_MIN_SIZE SCROLLBAR_MIN_SIZE,
) ),
}; };
} }
return { return {
horizontal: horizontalScrollBar, horizontal: horizontalScrollBar,
vertical: verticalScrollBar vertical: verticalScrollBar,
}; };
} }
@ -82,30 +82,30 @@ export function isOverScrollBars(
canvasWidth: number, canvasWidth: number,
canvasHeight: number, canvasHeight: number,
scrollX: number, scrollX: number,
scrollY: number scrollY: number,
) { ) {
const scrollBars = getScrollBars( const scrollBars = getScrollBars(
elements, elements,
canvasWidth, canvasWidth,
canvasHeight, canvasHeight,
scrollX, scrollX,
scrollY scrollY,
); );
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [ const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal, scrollBars.horizontal,
scrollBars.vertical scrollBars.vertical,
].map( ].map(
scrollBar => scrollBar =>
scrollBar && scrollBar &&
scrollBar.x <= x && scrollBar.x <= x &&
x <= scrollBar.x + scrollBar.width && x <= scrollBar.x + scrollBar.width &&
scrollBar.y <= y && scrollBar.y <= y &&
y <= scrollBar.y + scrollBar.height y <= scrollBar.y + scrollBar.height,
); );
return { return {
isOverHorizontalScrollBar, isOverHorizontalScrollBar,
isOverVerticalScrollBar isOverVerticalScrollBar,
}; };
} }

@ -3,20 +3,20 @@ import { getElementAbsoluteCoords } from "../element";
export function getElementsWithinSelection( export function getElementsWithinSelection(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
selection: ExcalidrawElement selection: ExcalidrawElement,
) { ) {
const [ const [
selectionX1, selectionX1,
selectionY1, selectionY1,
selectionX2, selectionX2,
selectionY2 selectionY2,
] = getElementAbsoluteCoords(selection); ] = getElementAbsoluteCoords(selection);
return elements.filter(element => { return elements.filter(element => {
const [ const [
elementX1, elementX1,
elementY1, elementY1,
elementX2, elementX2,
elementY2 elementY2,
] = getElementAbsoluteCoords(element); ] = getElementAbsoluteCoords(element);
return ( return (
@ -62,14 +62,14 @@ export const someElementIsSelected = (elements: readonly ExcalidrawElement[]) =>
*/ */
export function getCommonAttributeOfSelectedElements<T>( export function getCommonAttributeOfSelectedElements<T>(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
getAttribute: (element: ExcalidrawElement) => T getAttribute: (element: ExcalidrawElement) => T,
): T | null { ): T | null {
const attributes = Array.from( const attributes = Array.from(
new Set( new Set(
elements elements
.filter(element => element.isSelected) .filter(element => element.isSelected)
.map(element => getAttribute(element)) .map(element => getAttribute(element)),
) ),
); );
return attributes.length === 1 ? attributes[0] : null; return attributes.length === 1 ? attributes[0] : null;
} }

@ -9,7 +9,7 @@ export const SHAPES = [
<path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z" /> <path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z" />
</svg> </svg>
), ),
value: "selection" value: "selection",
}, },
{ {
icon: ( icon: (
@ -18,7 +18,7 @@ export const SHAPES = [
<path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" /> <path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z" />
</svg> </svg>
), ),
value: "rectangle" value: "rectangle",
}, },
{ {
icon: ( icon: (
@ -27,7 +27,7 @@ export const SHAPES = [
<path d="M111.823 0L16.622 111.823 111.823 223.646 207.025 111.823z" /> <path d="M111.823 0L16.622 111.823 111.823 223.646 207.025 111.823z" />
</svg> </svg>
), ),
value: "diamond" value: "diamond",
}, },
{ {
icon: ( icon: (
@ -36,7 +36,7 @@ export const SHAPES = [
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" /> <path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z" />
</svg> </svg>
), ),
value: "ellipse" value: "ellipse",
}, },
{ {
icon: ( icon: (
@ -45,7 +45,7 @@ export const SHAPES = [
<path d="M313.941 216H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h301.941v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z" /> <path d="M313.941 216H12c-6.627 0-12 5.373-12 12v56c0 6.627 5.373 12 12 12h301.941v46.059c0 21.382 25.851 32.09 40.971 16.971l86.059-86.059c9.373-9.373 9.373-24.569 0-33.941l-86.059-86.059c-15.119-15.119-40.971-4.411-40.971 16.971V216z" />
</svg> </svg>
), ),
value: "arrow" value: "arrow",
}, },
{ {
icon: ( icon: (
@ -54,7 +54,7 @@ export const SHAPES = [
<line x1="0" y1="3" x2="6" y2="3" stroke="#000" strokeLinecap="round" /> <line x1="0" y1="3" x2="6" y2="3" stroke="#000" strokeLinecap="round" />
</svg> </svg>
), ),
value: "line" value: "line",
}, },
{ {
icon: ( icon: (
@ -63,13 +63,13 @@ export const SHAPES = [
<path d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" /> <path d="M432 416h-23.41L277.88 53.69A32 32 0 0 0 247.58 32h-47.16a32 32 0 0 0-30.3 21.69L39.41 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16h-19.58l23.3-64h152.56l23.3 64H304a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h128a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM176.85 272L224 142.51 271.15 272z" />
</svg> </svg>
), ),
value: "text" value: "text",
} },
]; ];
export const shapesShortcutKeys = SHAPES.map((shape, index) => [ export const shapesShortcutKeys = SHAPES.map((shape, index) => [
shape.value[0], shape.value[0],
(index + 1).toString() (index + 1).toString(),
]).flat(1); ]).flat(1);
export function findShapeByKey(key: string) { export function findShapeByKey(key: string) {

@ -15,7 +15,7 @@ export function capitalizeString(str: string) {
} }
export function isInputLike( export function isInputLike(
target: Element | EventTarget | null target: Element | EventTarget | null,
): target is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement { ): target is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement {
return ( return (
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") || (target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
@ -54,7 +54,7 @@ export function measureText(text: string, font: string) {
export function debounce<T extends any[]>( export function debounce<T extends any[]>(
fn: (...args: T) => void, fn: (...args: T) => void,
timeout: number timeout: number,
) { ) {
let handle = 0; let handle = 0;
let lastArgs: T; let lastArgs: T;

@ -4,7 +4,7 @@ function expectMove<T>(
fn: (elements: T[], indicesToMove: number[]) => void, fn: (elements: T[], indicesToMove: number[]) => void,
elems: T[], elems: T[],
indices: number[], indices: number[],
equal: T[] equal: T[],
) { ) {
fn(elems, indices); fn(elems, indices);
expect(elems).toEqual(equal); expect(elems).toEqual(equal);
@ -17,7 +17,7 @@ it("should moveOneLeft", () => {
moveOneLeft, moveOneLeft,
["a", "b", "c", "d"], ["a", "b", "c", "d"],
[0, 1, 2, 3], [0, 1, 2, 3],
["a", "b", "c", "d"] ["a", "b", "c", "d"],
); );
expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 3], ["b", "a", "d", "c"]); expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 3], ["b", "a", "d", "c"]);
}); });
@ -29,7 +29,7 @@ it("should moveOneRight", () => {
moveOneRight, moveOneRight,
["a", "b", "c", "d"], ["a", "b", "c", "d"],
[0, 1, 2, 3], [0, 1, 2, 3],
["a", "b", "c", "d"] ["a", "b", "c", "d"],
); );
expectMove(moveOneRight, ["a", "b", "c", "d"], [0, 2], ["b", "a", "d", "c"]); expectMove(moveOneRight, ["a", "b", "c", "d"], [0, 2], ["b", "a", "d", "c"]);
}); });
@ -39,31 +39,31 @@ it("should moveAllLeft", () => {
moveAllLeft, moveAllLeft,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[2, 5], [2, 5],
["c", "f", "a", "b", "d", "e", "g"] ["c", "f", "a", "b", "d", "e", "g"],
); );
expectMove( expectMove(
moveAllLeft, moveAllLeft,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[5], [5],
["f", "a", "b", "c", "d", "e", "g"] ["f", "a", "b", "c", "d", "e", "g"],
); );
expectMove( expectMove(
moveAllLeft, moveAllLeft,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6],
["a", "b", "c", "d", "e", "f", "g"] ["a", "b", "c", "d", "e", "f", "g"],
); );
expectMove( expectMove(
moveAllLeft, moveAllLeft,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[0, 1, 2], [0, 1, 2],
["a", "b", "c", "d", "e", "f", "g"] ["a", "b", "c", "d", "e", "f", "g"],
); );
expectMove( expectMove(
moveAllLeft, moveAllLeft,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[4, 5, 6], [4, 5, 6],
["e", "f", "g", "a", "b", "c", "d"] ["e", "f", "g", "a", "b", "c", "d"],
); );
}); });
@ -72,30 +72,30 @@ it("should moveAllRight", () => {
moveAllRight, moveAllRight,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[2, 5], [2, 5],
["a", "b", "d", "e", "g", "c", "f"] ["a", "b", "d", "e", "g", "c", "f"],
); );
expectMove( expectMove(
moveAllRight, moveAllRight,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[5], [5],
["a", "b", "c", "d", "e", "g", "f"] ["a", "b", "c", "d", "e", "g", "f"],
); );
expectMove( expectMove(
moveAllRight, moveAllRight,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 6],
["a", "b", "c", "d", "e", "f", "g"] ["a", "b", "c", "d", "e", "f", "g"],
); );
expectMove( expectMove(
moveAllRight, moveAllRight,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[0, 1, 2], [0, 1, 2],
["d", "e", "f", "g", "a", "b", "c"] ["d", "e", "f", "g", "a", "b", "c"],
); );
expectMove( expectMove(
moveAllRight, moveAllRight,
["a", "b", "c", "d", "e", "f", "g"], ["a", "b", "c", "d", "e", "f", "g"],
[4, 5, 6], [4, 5, 6],
["a", "b", "c", "d", "e", "f", "g"] ["a", "b", "c", "d", "e", "f", "g"],
); );
}); });

@ -23,7 +23,7 @@ export function moveOneLeft<T>(elements: T[], indicesToMove: number[]) {
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) { export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
const reversedIndicesToMove = indicesToMove.sort( const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a (a: number, b: number) => b - a,
); );
let isSorted = true; let isSorted = true;
@ -166,7 +166,7 @@ export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
// And we are done! // And we are done!
export function moveAllRight<T>(elements: T[], indicesToMove: number[]) { export function moveAllRight<T>(elements: T[], indicesToMove: number[]) {
const reversedIndicesToMove = indicesToMove.sort( const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a (a: number, b: number) => b - a,
); );
// Copy the elements to move // Copy the elements to move

Loading…
Cancel
Save