Prefer arrow functions and callbacks (#1210)

pull/1620/head^2
Lipis 5 years ago committed by GitHub
parent 33fe223b5d
commit c427aa3cce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,15 +18,15 @@ import {
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
function getElementIndices(
const getElementIndices = (
direction: "left" | "right",
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
const selectedIndices: number[] = [];
let deletedIndicesCache: number[] = [];
function cb(element: ExcalidrawElement, index: number) {
const cb = (element: ExcalidrawElement, index: number) => {
if (element.isDeleted) {
// we want to build an array of deleted elements that are preceeding
// a selected element so that we move them together
@ -39,7 +39,7 @@ function getElementIndices(
// of selected/deleted elements, of after encountering non-deleted elem
deletedIndicesCache = [];
}
}
};
// sending back → select contiguous deleted elements that are to the left of
// selected element(s)
@ -59,19 +59,19 @@ function getElementIndices(
}
// sort in case we were gathering indexes from right to left
return selectedIndices.sort();
}
};
function moveElements(
const moveElements = (
func: typeof moveOneLeft,
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
const _elements = elements.slice();
const direction =
func === moveOneLeft || func === moveAllLeft ? "left" : "right";
const indices = getElementIndices(direction, _elements, appState);
return func(_elements, indices);
}
};
export const actionSendBackward = register({
name: "sendBackward",

@ -2,7 +2,7 @@ import { Action } from "./types";
export let actions: readonly Action[] = [];
export function register(action: Action): Action {
export const register = (action: Action): Action => {
actions = actions.concat(action);
return action;
}
};

@ -6,7 +6,7 @@ import { t } from "./i18n";
export const DEFAULT_FONT = "20px Virgil";
export const DEFAULT_TEXT_ALIGN = "left";
export function getDefaultAppState(): AppState {
export const getDefaultAppState = (): AppState => {
return {
isLoading: false,
errorMessage: null,
@ -49,9 +49,9 @@ export function getDefaultAppState(): AppState {
showShortcutsDialog: false,
zenModeEnabled: false,
};
}
};
export function clearAppStateForLocalStorage(appState: AppState) {
export const clearAppStateForLocalStorage = (appState: AppState) => {
const {
draggingElement,
resizingElement,
@ -68,11 +68,11 @@ export function clearAppStateForLocalStorage(appState: AppState) {
...exportedState
} = appState;
return exportedState;
}
};
export function clearAppStatePropertiesForHistory(
export const clearAppStatePropertiesForHistory = (
appState: AppState,
): Partial<AppState> {
): Partial<AppState> => {
return {
selectedElementIds: appState.selectedElementIds,
exportBackground: appState.exportBackground,
@ -88,10 +88,10 @@ export function clearAppStatePropertiesForHistory(
viewBackgroundColor: appState.viewBackgroundColor,
name: appState.name,
};
}
};
export function cleanAppStateForExport(appState: AppState) {
export const cleanAppStateForExport = (appState: AppState) => {
return {
viewBackgroundColor: appState.viewBackgroundColor,
};
}
};

@ -21,10 +21,10 @@ export const probablySupportsClipboardBlob =
"ClipboardItem" in window &&
"toBlob" in HTMLCanvasElement.prototype;
export async function copyToAppClipboard(
export const copyToAppClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) {
) => {
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
try {
// when copying to in-app clipboard, clear system clipboard so that if
@ -38,11 +38,11 @@ export async function copyToAppClipboard(
// we can't be sure of the order of copy operations
PREFER_APP_CLIPBOARD = true;
}
}
};
export function getAppClipboard(): {
export const getAppClipboard = (): {
elements?: readonly ExcalidrawElement[];
} {
} => {
if (!CLIPBOARD) {
return {};
}
@ -62,14 +62,14 @@ export function getAppClipboard(): {
}
return {};
}
};
export async function getClipboardContent(
export const getClipboardContent = async (
event: ClipboardEvent | null,
): Promise<{
text?: string;
elements?: readonly ExcalidrawElement[];
}> {
}> => {
try {
const text = event
? event.clipboardData?.getData("text/plain").trim()
@ -84,12 +84,12 @@ export async function getClipboardContent(
}
return getAppClipboard();
}
};
export async function copyCanvasToClipboardAsPng(canvas: HTMLCanvasElement) {
return new Promise((resolve, reject) => {
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
new Promise((resolve, reject) => {
try {
canvas.toBlob(async function (blob: any) {
canvas.toBlob(async (blob: any) => {
try {
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }),
@ -103,17 +103,16 @@ export async function copyCanvasToClipboardAsPng(canvas: HTMLCanvasElement) {
reject(error);
}
});
}
export async function copyCanvasToClipboardAsSvg(svgroot: SVGSVGElement) {
export const copyCanvasToClipboardAsSvg = async (svgroot: SVGSVGElement) => {
try {
await navigator.clipboard.writeText(svgroot.outerHTML);
} catch (error) {
console.error(error);
}
}
};
export async function copyTextToSystemClipboard(text: string | null) {
export const copyTextToSystemClipboard = async (text: string | null) => {
let copied = false;
if (probablySupportsClipboardWriteText) {
try {
@ -131,10 +130,10 @@ export async function copyTextToSystemClipboard(text: string | null) {
if (!copied && !copyTextViaExecCommand(text || " ")) {
throw new Error("couldn't copy");
}
}
};
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
function copyTextViaExecCommand(text: string) {
const copyTextViaExecCommand = (text: string) => {
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const textarea = document.createElement("textarea");
@ -168,4 +167,4 @@ function copyTextViaExecCommand(text: string) {
textarea.remove();
return success;
}
};

@ -11,7 +11,7 @@ import Stack from "./Stack";
import useIsMobile from "../is-mobile";
import { getNonDeletedElements } from "../element";
export function SelectedShapeActions({
export const SelectedShapeActions = ({
appState,
elements,
renderAction,
@ -21,7 +21,7 @@ export function SelectedShapeActions({
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
elementType: ExcalidrawElement["type"];
}) {
}) => {
const targetElements = getTargetElement(
getNonDeletedElements(elements),
appState,
@ -83,65 +83,61 @@ export function SelectedShapeActions({
)}
</div>
);
}
};
export function ShapesSwitcher({
export const ShapesSwitcher = ({
elementType,
setAppState,
}: {
elementType: ExcalidrawElement["type"];
setAppState: any;
}) {
return (
<>
{SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`);
const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
index + 1
}`;
return (
<ToolButton
key={value}
type="radio"
icon={icon}
checked={elementType === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${index + 1}`}
aria-label={capitalizeString(label)}
aria-keyshortcuts={`${key} ${index + 1}`}
data-testid={value}
onChange={() => {
setAppState({
elementType: value,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(value);
setAppState({});
}}
></ToolButton>
);
})}
</>
);
}
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`);
const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
index + 1
}`;
return (
<ToolButton
key={value}
type="radio"
icon={icon}
checked={elementType === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={`${index + 1}`}
aria-label={capitalizeString(label)}
aria-keyshortcuts={`${key} ${index + 1}`}
data-testid={value}
onChange={() => {
setAppState({
elementType: value,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(value);
setAppState({});
}}
></ToolButton>
);
})}
</>
);
export function ZoomActions({
export const ZoomActions = ({
renderAction,
zoom,
}: {
renderAction: ActionManager["renderAction"];
zoom: number;
}) {
return (
<Stack.Col gap={1}>
<Stack.Row gap={1} align="center">
{renderAction("zoomIn")}
{renderAction("zoomOut")}
{renderAction("resetZoom")}
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
</Stack.Row>
</Stack.Col>
);
}
}) => (
<Stack.Col gap={1}>
<Stack.Row gap={1} align="center">
{renderAction("zoomIn")}
{renderAction("zoomOut")}
{renderAction("resetZoom")}
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
</Stack.Row>
</Stack.Col>
);

@ -136,13 +136,14 @@ import throttle from "lodash.throttle";
/**
* @param func handler taking at most single parameter (event).
*/
function withBatchedUpdates<
const withBatchedUpdates = <
TFunction extends ((event: any) => void) | (() => void)
>(func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never) {
return ((event) => {
>(
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
) =>
((event) => {
unstable_batchedUpdates(func as TFunction, event);
}) as TFunction;
}
const { history } = createHistory();
@ -2748,9 +2749,7 @@ if (
},
},
history: {
get() {
return history;
},
get: () => history,
},
});
}

@ -1,6 +1,6 @@
import React from "react";
export function ButtonSelect<T>({
export const ButtonSelect = <T extends Object>({
options,
value,
onChange,
@ -10,23 +10,21 @@ export function ButtonSelect<T>({
value: T | null;
onChange: (value: T) => void;
group: string;
}) {
return (
<div className="buttonList">
{options.map((option) => (
<label
key={option.text}
className={value === option.value ? "active" : ""}
>
<input
type="radio"
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value ? true : false}
/>
{option.text}
</label>
))}
</div>
);
}
}) => (
<div className="buttonList">
{options.map((option) => (
<label
key={option.text}
className={value === option.value ? "active" : ""}
>
<input
type="radio"
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value ? true : false}
/>
{option.text}
</label>
))}
</div>
);

@ -7,11 +7,11 @@ import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils";
import colors from "../colors";
function isValidColor(color: string) {
const isValidColor = (color: string) => {
const style = new Option().style;
style.color = color;
return !!style.color;
}
};
const getColor = (color: string): string | null => {
if (color === "transparent") {
@ -36,7 +36,7 @@ const keyBindings = [
["a", "s", "d", "f", "g"],
].flat();
const Picker = function ({
const Picker = ({
colors,
color,
onChange,
@ -50,7 +50,7 @@ const Picker = function ({
onClose: () => void;
label: string;
showInput: boolean;
}) {
}) => {
const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>();
const gallery = React.useRef<HTMLDivElement>();
@ -235,7 +235,7 @@ const ColorInput = React.forwardRef(
},
);
export function ColorPicker({
export const ColorPicker = ({
type,
color,
onChange,
@ -245,7 +245,7 @@ export function ColorPicker({
color: string | null;
onChange: (color: string) => void;
label: string;
}) {
}) => {
const [isActive, setActive] = React.useState(false);
const pickerButton = React.useRef<HTMLButtonElement>(null);
@ -296,4 +296,4 @@ export function ColorPicker({
</React.Suspense>
</div>
);
}
};

@ -16,45 +16,41 @@ type Props = {
left: number;
};
function ContextMenu({ options, onCloseRequest, top, left }: Props) {
return (
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => (
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
>
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
{options.map((option, idx) => (
<li key={idx} onClick={onCloseRequest}>
<ContextMenuOption {...option} />
</li>
))}
</ul>
</Popover>
);
}
{options.map((option, idx) => (
<li key={idx} onClick={onCloseRequest}>
<ContextMenuOption {...option} />
</li>
))}
</ul>
</Popover>
);
function ContextMenuOption({ label, action }: ContextMenuOption) {
return (
<button className="context-menu-option" onClick={action}>
{label}
</button>
);
}
const ContextMenuOption = ({ label, action }: ContextMenuOption) => (
<button className="context-menu-option" onClick={action}>
{label}
</button>
);
let contextMenuNode: HTMLDivElement;
function getContextMenuNode(): HTMLDivElement {
const getContextMenuNode = (): HTMLDivElement => {
if (contextMenuNode) {
return contextMenuNode;
}
const div = document.createElement("div");
document.body.appendChild(div);
return (contextMenuNode = div);
}
};
type ContextMenuParams = {
options: (ContextMenuOption | false | null | undefined)[];
@ -62,9 +58,9 @@ type ContextMenuParams = {
left: number;
};
function handleClose() {
const handleClose = () => {
unmountComponentAtNode(getContextMenuNode());
}
};
export default {
push(params: ContextMenuParams) {

@ -8,13 +8,13 @@ import { KEYS } from "../keys";
import "./Dialog.scss";
export function Dialog(props: {
export const Dialog = (props: {
children: React.ReactNode;
className?: string;
maxWidth?: number;
onCloseRequest(): void;
title: React.ReactNode;
}) {
}) => {
const islandRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -31,7 +31,7 @@ export function Dialog(props: {
return;
}
function handleKeyDown(event: KeyboardEvent) {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.TAB) {
const focusableElements = queryFocusableElements();
const { activeElement } = document;
@ -50,7 +50,7 @@ export function Dialog(props: {
event.preventDefault();
}
}
}
};
const node = islandRef.current;
node.addEventListener("keydown", handleKeyDown);
@ -58,13 +58,13 @@ export function Dialog(props: {
return () => node.removeEventListener("keydown", handleKeyDown);
}, []);
function queryFocusableElements() {
const queryFocusableElements = () => {
const focusableElements = islandRef.current?.querySelectorAll<HTMLElement>(
"button, a, input, select, textarea, div[tabindex]",
);
return focusableElements ? Array.from(focusableElements) : [];
}
};
return (
<Modal
@ -88,4 +88,4 @@ export function Dialog(props: {
</Island>
</Modal>
);
}
};

@ -3,13 +3,13 @@ import { t } from "../i18n";
import { Dialog } from "./Dialog";
export function ErrorDialog({
export const ErrorDialog = ({
message,
onClose,
}: {
message: string;
onClose?: () => void;
}) {
}) => {
const [modalIsShown, setModalIsShown] = useState(!!message);
const handleClose = React.useCallback(() => {
@ -33,4 +33,4 @@ export function ErrorDialog({
)}
</>
);
}
};

@ -24,7 +24,7 @@ export type ExportCB = (
scale?: number,
) => void;
function ExportModal({
const ExportModal = ({
elements,
appState,
exportPadding = 10,
@ -43,7 +43,7 @@ function ExportModal({
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
onCloseRequest: () => void;
}) {
}) => {
const someElementIsSelected = isSomeElementSelected(elements, appState);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
@ -160,9 +160,9 @@ function ExportModal({
</Stack.Col>
</div>
);
}
};
export function ExportDialog({
export const ExportDialog = ({
elements,
appState,
exportPadding = 10,
@ -180,7 +180,7 @@ export function ExportDialog({
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend: ExportCB;
}) {
}) => {
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);
@ -221,4 +221,4 @@ export function ExportDialog({
)}
</>
);
}
};

@ -8,16 +8,14 @@ type FixedSideContainerProps = {
className?: string;
};
export function FixedSideContainer({
export const FixedSideContainer = ({
children,
side,
className,
}: FixedSideContainerProps) {
return (
<div
className={`FixedSideContainer FixedSideContainer_side_${side} ${className}`}
>
{children}
</div>
);
}
}: FixedSideContainerProps) => (
<div
className={`FixedSideContainer FixedSideContainer_side_${side} ${className}`}
>
{children}
</div>
);

@ -18,10 +18,8 @@ const ICON = (
</svg>
);
export function HelpIcon(props: HelpIconProps) {
return (
<label title={`${props.title} — ?`} className="help-icon">
<div onClick={props.onClick}>{ICON}</div>
</label>
);
}
export const HelpIcon = (props: HelpIconProps) => (
<label title={`${props.title} — ?`} className="help-icon">
<div onClick={props.onClick}>{ICON}</div>
</label>
);

@ -1,7 +1,7 @@
import React from "react";
import * as i18n from "../i18n";
export function LanguageList({
export const LanguageList = ({
onChange,
languages = i18n.languages,
currentLanguage = i18n.getLanguage().lng,
@ -11,23 +11,21 @@ export function LanguageList({
onChange: (value: string) => void;
currentLanguage?: string;
floating?: boolean;
}) {
return (
<React.Fragment>
<select
className={`dropdown-select dropdown-select__language${
floating ? " dropdown-select--floating" : ""
}`}
onChange={({ target }) => onChange(target.value)}
value={currentLanguage}
aria-label={i18n.t("buttons.selectLanguage")}
>
{languages.map((language) => (
<option key={language.lng} value={language.lng}>
{language.label}
</option>
))}
</select>
</React.Fragment>
);
}
}) => (
<React.Fragment>
<select
className={`dropdown-select dropdown-select__language${
floating ? " dropdown-select--floating" : ""
}`}
onChange={({ target }) => onChange(target.value)}
value={currentLanguage}
aria-label={i18n.t("buttons.selectLanguage")}
>
{languages.map((language) => (
<option key={language.lng} value={language.lng}>
{language.label}
</option>
))}
</select>
</React.Fragment>
);

@ -40,7 +40,7 @@ const ICONS = {
),
};
export function LockIcon(props: LockIconProps) {
export const LockIcon = (props: LockIconProps) => {
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
return (
@ -64,4 +64,4 @@ export function LockIcon(props: LockIconProps) {
</div>
</label>
);
}
};

@ -29,7 +29,7 @@ type MobileMenuProps = {
onLockToggle: () => void;
};
export function MobileMenu({
export const MobileMenu = ({
appState,
elements,
actionManager,
@ -39,108 +39,106 @@ export function MobileMenu({
onUsernameChange,
onRoomDestroy,
onLockToggle,
}: MobileMenuProps) {
return (
<>
{appState.isLoading && <LoadingMessage />}
<FixedSideContainer side="top">
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1}>
<Island padding={1}>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
/>
</Stack.Row>
</Island>
<LockIcon
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Stack.Row>
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
</FixedSideContainer>
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}}
>
<Island padding={3}>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn">
<Stack.Col gap={4}>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
<RoomDialog
isCollaborating={appState.isCollaborating}
collaboratorCount={appState.collaborators.size}
username={appState.username}
onUsernameChange={onUsernameChange}
onRoomCreate={onRoomCreate}
onRoomDestroy={onRoomDestroy}
}: MobileMenuProps) => (
<>
{appState.isLoading && <LoadingMessage />}
<FixedSideContainer side="top">
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1}>
<Island padding={1}>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
elementType={appState.elementType}
setAppState={setAppState}
/>
{actionManager.renderAction("changeViewBackgroundColor")}
<fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={(lng) => {
setLanguage(lng);
setAppState({});
}}
/>
</fieldset>
</Stack.Col>
</div>
</Section>
) : appState.openMenu === "shape" &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
</Stack.Row>
</Island>
<LockIcon
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
</Section>
) : null}
<footer className="App-toolbar">
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
{actionManager.renderAction("deleteSelectedElements")}
</Stack.Row>
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
</FixedSideContainer>
<div
className="App-bottom-bar"
style={{
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
}}
>
<Island padding={3}>
{appState.openMenu === "canvas" ? (
<Section className="App-mobile-menu" heading="canvasActions">
<div className="panelColumn">
<Stack.Col gap={4}>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
<RoomDialog
isCollaborating={appState.isCollaborating}
collaboratorCount={appState.collaborators.size}
username={appState.username}
onUsernameChange={onUsernameChange}
onRoomCreate={onRoomCreate}
onRoomDestroy={onRoomDestroy}
/>
{actionManager.renderAction("changeViewBackgroundColor")}
<fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={(lng) => {
setLanguage(lng);
setAppState({});
}}
/>
</fieldset>
</Stack.Col>
</div>
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({ ...calculateScrollCenter(elements) });
}}
>
{t("buttons.scrollBackToContent")}
</button>
</Section>
) : appState.openMenu === "shape" &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
appState={appState}
elements={elements}
renderAction={actionManager.renderAction}
elementType={appState.elementType}
/>
</Section>
) : null}
<footer className="App-toolbar">
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
</footer>
</Island>
</div>
</>
);
}
{actionManager.renderAction("deleteSelectedElements")}
</div>
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({ ...calculateScrollCenter(elements) });
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>
</>
);

@ -4,13 +4,13 @@ import React, { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { KEYS } from "../keys";
export function Modal(props: {
export const Modal = (props: {
className?: string;
children: React.ReactNode;
maxWidth?: number;
onCloseRequest(): void;
labelledBy: string;
}) {
}) => {
const modalRoot = useBodyRoot();
const handleKeydown = (event: React.KeyboardEvent) => {
@ -44,14 +44,14 @@ export function Modal(props: {
</div>,
modalRoot,
);
}
};
function useBodyRoot() {
function createDiv() {
const useBodyRoot = () => {
const createDiv = () => {
const div = document.createElement("div");
document.body.appendChild(div);
return div;
}
};
const [div] = useState(createDiv);
useEffect(() => {
return () => {
@ -59,4 +59,4 @@ function useBodyRoot() {
};
}, [div]);
return div;
}
};

@ -10,13 +10,13 @@ type Props = {
fitInViewport?: boolean;
};
export function Popover({
export const Popover = ({
children,
left,
top,
onCloseRequest,
fitInViewport = false,
}: Props) {
}: Props) => {
const popoverRef = useRef<HTMLDivElement>(null);
// ensure the popover doesn't overflow the viewport
@ -53,4 +53,4 @@ export function Popover({
{children}
</div>
);
}
};

@ -9,7 +9,7 @@ import { copyTextToSystemClipboard } from "../clipboard";
import { Dialog } from "./Dialog";
import { AppState } from "../types";
function RoomModal({
const RoomModal = ({
activeRoomLink,
username,
onUsernameChange,
@ -23,21 +23,21 @@ function RoomModal({
onRoomCreate: () => void;
onRoomDestroy: () => void;
onPressingEnter: () => void;
}) {
}) => {
const roomLinkInput = useRef<HTMLInputElement>(null);
function copyRoomLink() {
const copyRoomLink = () => {
copyTextToSystemClipboard(activeRoomLink);
if (roomLinkInput.current) {
roomLinkInput.current.select();
}
}
function selectInput(event: React.MouseEvent<HTMLInputElement>) {
};
const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target !== document.activeElement) {
event.preventDefault();
(event.target as HTMLInputElement).select();
}
}
};
return (
<div className="RoomDialog-modal">
@ -113,9 +113,9 @@ function RoomModal({
)}
</div>
);
}
};
export function RoomDialog({
export const RoomDialog = ({
isCollaborating,
collaboratorCount,
username,
@ -129,7 +129,7 @@ export function RoomDialog({
onUsernameChange: (username: string) => void;
onRoomCreate: () => void;
onRoomDestroy: () => void;
}) {
}) => {
const [modalIsShown, setModalIsShown] = useState(false);
const [activeRoomLink, setActiveRoomLink] = useState("");
@ -182,4 +182,4 @@ export function RoomDialog({
)}
</>
);
}
};

@ -6,7 +6,7 @@ interface SectionProps extends React.HTMLProps<HTMLElement> {
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
}
export function Section({ heading, children, ...props }: SectionProps) {
export const Section = ({ heading, children, ...props }: SectionProps) => {
const header = (
<h2 className="visually-hidden" id={`${heading}-title`}>
{t(`headings.${heading}`)}
@ -24,4 +24,4 @@ export function Section({ heading, children, ...props }: SectionProps) {
)}
</section>
);
}
};

@ -10,13 +10,13 @@ type StackProps = {
className?: string | boolean;
};
function RowStack({
const RowStack = ({
children,
gap,
align,
justifyContent,
className,
}: StackProps) {
}: StackProps) => {
return (
<div
className={`Stack Stack_horizontal ${className || ""}`}
@ -31,15 +31,15 @@ function RowStack({
{children}
</div>
);
}
};
function ColStack({
const ColStack = ({
children,
gap,
align,
justifyContent,
className,
}: StackProps) {
}: StackProps) => {
return (
<div
className={`Stack Stack_vertical ${className || ""}`}
@ -54,7 +54,7 @@ function ColStack({
{children}
</div>
);
}
};
export default {
Row: RowStack,

@ -36,10 +36,7 @@ type ToolButtonProps =
const DEFAULT_SIZE: ToolIconSize = "m";
export const ToolButton = React.forwardRef(function (
props: ToolButtonProps,
ref,
) {
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
const innerRef = React.useRef(null);
React.useImperativeHandle(ref, () => innerRef.current);
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;

@ -2,7 +2,7 @@ import { getDefaultAppState } from "../appState";
import { restore } from "./restore";
import { t } from "../i18n";
export async function loadFromBlob(blob: any) {
export const loadFromBlob = async (blob: any) => {
const updateAppState = (contents: string) => {
const defaultAppState = getDefaultAppState();
let elements = [];
@ -40,4 +40,4 @@ export async function loadFromBlob(blob: any) {
const { elements, appState } = updateAppState(contents);
return restore(elements, appState, { scrollToContent: true });
}
};

@ -72,17 +72,15 @@ export type SocketUpdateDataIncoming =
// part of `AppState`.
(window as any).handle = null;
function byteToHex(byte: number): string {
return `0${byte.toString(16)}`.slice(-2);
}
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
async function generateRandomID() {
const generateRandomID = async () => {
const arr = new Uint8Array(10);
window.crypto.getRandomValues(arr);
return Array.from(arr, byteToHex).join("");
}
};
async function generateEncryptionKey() {
const generateEncryptionKey = async () => {
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
@ -92,29 +90,29 @@ async function generateEncryptionKey() {
["encrypt", "decrypt"],
);
return (await window.crypto.subtle.exportKey("jwk", key)).k;
}
};
function createIV() {
const createIV = () => {
const arr = new Uint8Array(12);
return window.crypto.getRandomValues(arr);
}
};
export function getCollaborationLinkData(link: string) {
export const getCollaborationLinkData = (link: string) => {
if (link.length === 0) {
return;
}
const hash = new URL(link).hash;
return hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
}
};
export async function generateCollaborationLink() {
export const generateCollaborationLink = async () => {
const id = await generateRandomID();
const key = await generateEncryptionKey();
return `${window.location.origin}${window.location.pathname}#room=${id},${key}`;
}
};
function getImportedKey(key: string, usage: string) {
return window.crypto.subtle.importKey(
const getImportedKey = (key: string, usage: string) =>
window.crypto.subtle.importKey(
"jwk",
{
alg: "A128GCM",
@ -130,12 +128,11 @@ function getImportedKey(key: string, usage: string) {
false, // extractable
[usage],
);
}
export async function encryptAESGEM(
export const encryptAESGEM = async (
data: Uint8Array,
key: string,
): Promise<EncryptedData> {
): Promise<EncryptedData> => {
const importedKey = await getImportedKey(key, "encrypt");
const iv = createIV();
return {
@ -149,13 +146,13 @@ export async function encryptAESGEM(
),
iv,
};
}
};
export async function decryptAESGEM(
export const decryptAESGEM = async (
data: ArrayBuffer,
key: string,
iv: Uint8Array,
): Promise<SocketUpdateDataIncoming> {
): Promise<SocketUpdateDataIncoming> => {
try {
const importedKey = await getImportedKey(key, "decrypt");
const decrypted = await window.crypto.subtle.decrypt(
@ -178,12 +175,12 @@ export async function decryptAESGEM(
return {
type: "INVALID_RESPONSE",
};
}
};
export async function exportToBackend(
export const exportToBackend = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
const json = serializeAsJSON(elements, appState);
const encoded = new TextEncoder().encode(json);
@ -233,12 +230,12 @@ export async function exportToBackend(
console.error(error);
window.alert(t("alerts.couldNotCreateShareableLink"));
}
}
};
export async function importFromBackend(
export const importFromBackend = async (
id: string | null,
privateKey: string | undefined,
) {
) => {
let elements: readonly ExcalidrawElement[] = [];
let appState: AppState = getDefaultAppState();
@ -281,9 +278,9 @@ export async function importFromBackend(
} finally {
return restore(elements, appState, { scrollToContent: true });
}
}
};
export async function exportCanvas(
export const exportCanvas = async (
type: ExportType,
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
@ -303,7 +300,7 @@ export async function exportCanvas(
scale?: number;
shouldAddWatermark: boolean;
},
) {
) => {
if (elements.length === 0) {
return window.alert(t("alerts.cannotExportEmptyCanvas"));
}
@ -362,9 +359,9 @@ export async function exportCanvas(
if (tempCanvas !== canvas) {
tempCanvas.remove();
}
}
};
export async function loadScene(id: string | null, privateKey?: string) {
export const loadScene = async (id: string | null, privateKey?: string) => {
let data;
if (id != null) {
// the private key is used to decrypt the content from the server, take
@ -380,4 +377,4 @@ export async function loadScene(id: string | null, privateKey?: string) {
appState: data.appState && { ...data.appState },
commitToHistory: false,
};
}
};

@ -5,11 +5,11 @@ import { cleanAppStateForExport } from "../appState";
import { fileOpen, fileSave } from "browser-nativefs";
import { loadFromBlob } from "./blob";
export function serializeAsJSON(
export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: AppState,
): string {
return JSON.stringify(
): string =>
JSON.stringify(
{
type: "excalidraw",
version: 1,
@ -20,12 +20,11 @@ export function serializeAsJSON(
null,
2,
);
}
export async function saveAsJSON(
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
const serialized = serializeAsJSON(elements, appState);
const name = `${appState.name}.excalidraw`;
@ -41,12 +40,12 @@ export async function saveAsJSON(
},
(window as any).handle,
);
}
export async function loadFromJSON() {
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json", "application/vnd.excalidraw+json"],
});
return loadFromBlob(blob);
}
};

@ -7,7 +7,7 @@ const LOCAL_STORAGE_KEY = "excalidraw";
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
const LOCAL_STORAGE_KEY_COLLAB = "excalidraw-collab";
export function saveUsernameToLocalStorage(username: string) {
export const saveUsernameToLocalStorage = (username: string) => {
try {
localStorage.setItem(
LOCAL_STORAGE_KEY_COLLAB,
@ -17,9 +17,9 @@ export function saveUsernameToLocalStorage(username: string) {
// Unable to access window.localStorage
console.error(error);
}
}
};
export function restoreUsernameFromLocalStorage(): string | null {
export const restoreUsernameFromLocalStorage = (): string | null => {
try {
const data = localStorage.getItem(LOCAL_STORAGE_KEY_COLLAB);
if (data) {
@ -31,12 +31,12 @@ export function restoreUsernameFromLocalStorage(): string | null {
}
return null;
}
};
export function saveToLocalStorage(
export const saveToLocalStorage = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
try {
localStorage.setItem(
LOCAL_STORAGE_KEY,
@ -50,9 +50,9 @@ export function saveToLocalStorage(
// Unable to access window.localStorage
console.error(error);
}
}
};
export function restoreFromLocalStorage() {
export const restoreFromLocalStorage = () => {
let savedElements = null;
let savedState = null;
@ -86,4 +86,4 @@ export function restoreFromLocalStorage() {
}
return restore(elements, appState);
}
};

@ -12,13 +12,13 @@ import { calculateScrollCenter } from "../scene";
import { randomId } from "../random";
import { DEFAULT_TEXT_ALIGN } from "../appState";
export function restore(
export const restore = (
// we're making the elements mutable for this API because we want to
// efficiently remove/tweak properties on them (to migrate old scenes)
savedElements: readonly Mutable<ExcalidrawElement>[],
savedState: AppState | null,
opts?: { scrollToContent: boolean },
): DataState {
): DataState => {
const elements = savedElements
.filter((el) => {
// filtering out selection, which is legacy, no longer kept in elements,
@ -94,4 +94,4 @@ export function restore(
elements: elements,
appState: savedState,
};
}
};

@ -12,9 +12,9 @@ import { rescalePoints } from "../points";
// If the element is created from right to left, the width is going to be negative
// This set of functions retrieves the absolute position of the 4 points.
export function getElementAbsoluteCoords(
export const getElementAbsoluteCoords = (
element: ExcalidrawElement,
): [number, number, number, number] {
): [number, number, number, number] => {
if (isLinearElement(element)) {
return getLinearElementAbsoluteCoords(element);
}
@ -24,9 +24,9 @@ export function getElementAbsoluteCoords(
element.x + element.width,
element.y + element.height,
];
}
};
export function getDiamondPoints(element: ExcalidrawElement) {
export const getDiamondPoints = (element: ExcalidrawElement) => {
// Here we add +1 to avoid these numbers to be 0
// otherwise rough.js will throw an error complaining about it
const topX = Math.floor(element.width / 2) + 1;
@ -39,16 +39,16 @@ export function getDiamondPoints(element: ExcalidrawElement) {
const leftY = rightY;
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
}
};
export function getCurvePathOps(shape: Drawable): Op[] {
export const getCurvePathOps = (shape: Drawable): Op[] => {
for (const set of shape.sets) {
if (set.type === "path") {
return set.ops;
}
}
return shape.sets[0].ops;
}
};
const getMinMaxXYFromCurvePathOps = (
ops: Op[],
@ -150,10 +150,10 @@ const getLinearElementAbsoluteCoords = (
];
};
export function getArrowPoints(
export const getArrowPoints = (
element: ExcalidrawLinearElement,
shape: Drawable[],
) {
) => {
const ops = getCurvePathOps(shape[0]);
const data = ops[ops.length - 1].data;
@ -212,7 +212,7 @@ export function getArrowPoints(
const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
return [x2, y2, x3, y3, x4, y4];
}
};
const getLinearElementRotatedBounds = (
element: ExcalidrawLinearElement,

@ -19,10 +19,10 @@ import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
import { isLinearElement } from "./typeChecks";
function isElementDraggableFromInside(
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
appState: AppState,
): boolean {
): boolean => {
const dragFromInside =
element.backgroundColor !== "transparent" ||
appState.selectedElementIds[element.id];
@ -30,15 +30,15 @@ function isElementDraggableFromInside(
return dragFromInside && isPathALoop(element.points);
}
return dragFromInside;
}
};
export function hitTest(
export const hitTest = (
element: NonDeletedExcalidrawElement,
appState: AppState,
x: number,
y: number,
zoom: number,
): boolean {
): boolean => {
// 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
const lineThreshold = 10 / zoom;
@ -210,7 +210,7 @@ export function hitTest(
return false;
}
throw new Error(`Unimplemented type ${element.type}`);
}
};
const pointInBezierEquation = (
p0: Point,

@ -21,7 +21,7 @@ export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
rotation: true,
};
function generateHandler(
const generateHandler = (
x: number,
y: number,
width: number,
@ -29,18 +29,18 @@ function generateHandler(
cx: number,
cy: number,
angle: number,
): [number, number, number, number] {
): [number, number, number, number] => {
const [xx, yy] = rotate(x + width / 2, y + height / 2, cx, cy, angle);
return [xx - width / 2, yy - height / 2, width, height];
}
};
export function handlerRectanglesFromCoords(
export const handlerRectanglesFromCoords = (
[x1, y1, x2, y2]: [number, number, number, number],
angle: number,
zoom: number,
pointerType: PointerType = "mouse",
omitSides: { [T in Sides]?: boolean } = {},
): Partial<{ [T in Sides]: [number, number, number, number] }> {
): Partial<{ [T in Sides]: [number, number, number, number] }> => {
const size = handleSizes[pointerType];
const handlerWidth = size / zoom;
const handlerHeight = size / zoom;
@ -173,13 +173,13 @@ export function handlerRectanglesFromCoords(
}
return handlers;
}
};
export function handlerRectangles(
export const handlerRectangles = (
element: ExcalidrawElement,
zoom: number,
pointerType: PointerType = "mouse",
) {
) => {
const handlers = handlerRectanglesFromCoords(
getElementAbsoluteCoords(element),
element.angle,
@ -234,4 +234,4 @@ export function handlerRectangles(
}
return handlers;
}
};

@ -49,35 +49,30 @@ export {
} from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions";
export function getSyncableElements(elements: readonly ExcalidrawElement[]) {
// There are places in Excalidraw where synthetic invisibly small elements are added and removed.
export const getSyncableElements = (
elements: readonly ExcalidrawElement[], // There are places in Excalidraw where synthetic invisibly small elements are added and removed.
) =>
// It's probably best to keep those local otherwise there might be a race condition that
// gets the app into an invalid state. I've never seen it happen but I'm worried about it :)
return elements.filter((el) => el.isDeleted || !isInvisiblySmallElement(el));
}
elements.filter((el) => el.isDeleted || !isInvisiblySmallElement(el));
export function getElementMap(elements: readonly ExcalidrawElement[]) {
return elements.reduce(
export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
elements.reduce(
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
acc[element.id] = element;
return acc;
},
{},
);
}
export function getDrawingVersion(elements: readonly ExcalidrawElement[]) {
return elements.reduce((acc, el) => acc + el.version, 0);
}
export const getDrawingVersion = (elements: readonly ExcalidrawElement[]) =>
elements.reduce((acc, el) => acc + el.version, 0);
export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) {
return elements.filter(
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
elements.filter(
(element) => !element.isDeleted,
) as readonly NonDeletedExcalidrawElement[];
}
export function isNonDeletedElement<T extends ExcalidrawElement>(
export const isNonDeletedElement = <T extends ExcalidrawElement>(
element: T,
): element is NonDeleted<T> {
return !element.isDeleted;
}
): element is NonDeleted<T> => !element.isDeleted;

@ -13,10 +13,10 @@ type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
// The version is used to compare updates when more than one user is working in
// the same drawing. Note: this will trigger the component to update. Make sure you
// are calling it either from a React event handler or within unstable_batchedUpdates().
export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
updates: ElementUpdate<TElement>,
) {
) => {
// casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732)
const { points } = updates as any;
@ -45,16 +45,14 @@ export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
element.versionNonce = randomInteger();
globalSceneState.informMutation();
}
};
export function newElementWith<TElement extends ExcalidrawElement>(
export const newElementWith = <TElement extends ExcalidrawElement>(
element: TElement,
updates: ElementUpdate<TElement>,
): TElement {
return {
...element,
version: element.version + 1,
versionNonce: randomInteger(),
...updates,
};
}
): TElement => ({
...element,
version: element.version + 1,
versionNonce: randomInteger(),
...updates,
});

@ -5,12 +5,12 @@ import {
} from "./newElement";
import { mutateElement } from "./mutateElement";
function isPrimitive(val: any) {
const isPrimitive = (val: any) => {
const type = typeof val;
return val == null || (type !== "object" && type !== "function");
}
};
function assertCloneObjects(source: any, clone: any) {
const assertCloneObjects = (source: any, clone: any) => {
for (const key in clone) {
if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
expect(clone[key]).not.toBe(source[key]);
@ -19,7 +19,7 @@ function assertCloneObjects(source: any, clone: any) {
}
}
}
}
};
it("clones arrow element", () => {
const element = newLinearElement({

@ -25,7 +25,7 @@ type ElementConstructorOpts = {
angle?: ExcalidrawGenericElement["angle"];
};
function _newElementBase<T extends ExcalidrawElement>(
const _newElementBase = <T extends ExcalidrawElement>(
type: T["type"],
{
x,
@ -42,44 +42,41 @@ function _newElementBase<T extends ExcalidrawElement>(
angle = 0,
...rest
}: ElementConstructorOpts & Partial<ExcalidrawGenericElement>,
) {
return {
id: rest.id || randomId(),
type,
x,
y,
width,
height,
angle,
strokeColor,
backgroundColor,
fillStyle,
strokeWidth,
strokeStyle,
roughness,
opacity,
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,
versionNonce: rest.versionNonce ?? 0,
isDeleted: false as false,
};
}
) => ({
id: rest.id || randomId(),
type,
x,
y,
width,
height,
angle,
strokeColor,
backgroundColor,
fillStyle,
strokeWidth,
strokeStyle,
roughness,
opacity,
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,
versionNonce: rest.versionNonce ?? 0,
isDeleted: false as false,
});
export function newElement(
export const newElement = (
opts: {
type: ExcalidrawGenericElement["type"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawGenericElement> {
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
}
): NonDeleted<ExcalidrawGenericElement> =>
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
export function newTextElement(
export const newTextElement = (
opts: {
text: string;
font: string;
textAlign: TextAlign;
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> {
): NonDeleted<ExcalidrawTextElement> => {
const metrics = measureText(opts.text, opts.font);
const textElement = newElementWith(
{
@ -98,26 +95,26 @@ export function newTextElement(
);
return textElement;
}
};
export function newLinearElement(
export const newLinearElement = (
opts: {
type: ExcalidrawLinearElement["type"];
lastCommittedPoint?: ExcalidrawLinearElement["lastCommittedPoint"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawLinearElement> {
): NonDeleted<ExcalidrawLinearElement> => {
return {
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
points: [],
lastCommittedPoint: opts.lastCommittedPoint || null,
};
}
};
// Simplified deep clone for the purpose of cloning ExcalidrawElement only
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.)
//
// Adapted from https://github.com/lukeed/klona
function _duplicateElement(val: any, depth: number = 0) {
const _duplicateElement = (val: any, depth: number = 0) => {
if (val == null || typeof val !== "object") {
return val;
}
@ -149,12 +146,12 @@ function _duplicateElement(val: any, depth: number = 0) {
}
return val;
}
};
export function duplicateElement<TElement extends Mutable<ExcalidrawElement>>(
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
element: TElement,
overrides?: Partial<TElement>,
): TElement {
): TElement => {
let copy: TElement = _duplicateElement(element);
copy.id = randomId();
copy.seed = randomInteger();
@ -162,4 +159,4 @@ export function duplicateElement<TElement extends Mutable<ExcalidrawElement>>(
copy = Object.assign(copy, overrides);
}
return copy;
}
};

@ -13,27 +13,24 @@ import { AppState } from "../types";
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
function isInHandlerRect(
const isInHandlerRect = (
handler: [number, number, number, number],
x: number,
y: number,
) {
return (
x >= handler[0] &&
x <= handler[0] + handler[2] &&
y >= handler[1] &&
y <= handler[1] + handler[3]
);
}
) =>
x >= handler[0] &&
x <= handler[0] + handler[2] &&
y >= handler[1] &&
y <= handler[1] + handler[3];
export function resizeTest(
export const resizeTest = (
element: NonDeletedExcalidrawElement,
appState: AppState,
x: number,
y: number,
zoom: number,
pointerType: PointerType,
): HandlerRectanglesRet | false {
): HandlerRectanglesRet | false => {
if (!appState.selectedElementIds[element.id]) {
return false;
}
@ -66,30 +63,29 @@ export function resizeTest(
}
return false;
}
};
export function getElementWithResizeHandler(
export const getElementWithResizeHandler = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
{ x, y }: { x: number; y: number },
zoom: number,
pointerType: PointerType,
) {
return elements.reduce((result, element) => {
) =>
elements.reduce((result, element) => {
if (result) {
return result;
}
const resizeHandle = resizeTest(element, appState, x, y, zoom, pointerType);
return resizeHandle ? { element, resizeHandle } : null;
}, null as { element: NonDeletedExcalidrawElement; resizeHandle: ReturnType<typeof resizeTest> } | null);
}
export function getResizeHandlerFromCoords(
export const getResizeHandlerFromCoords = (
[x1, y1, x2, y2]: readonly [number, number, number, number],
{ x, y }: { x: number; y: number },
zoom: number,
pointerType: PointerType,
) {
) => {
const handlers = handlerRectanglesFromCoords(
[x1, y1, x2, y2],
0,
@ -103,7 +99,7 @@ export function getResizeHandlerFromCoords(
return handler && isInHandlerRect(handler, x, y);
});
return (found || false) as HandlerRectanglesRet;
}
};
const RESIZE_CURSORS = ["ns", "nesw", "ew", "nwse"];
const rotateResizeCursor = (cursor: string, angle: number) => {
@ -118,10 +114,10 @@ const rotateResizeCursor = (cursor: string, angle: number) => {
/*
* Returns bi-directional cursor for the element being resized
*/
export function getCursorForResizingElement(resizingElement: {
export const getCursorForResizingElement = (resizingElement: {
element?: ExcalidrawElement;
resizeHandle: ReturnType<typeof resizeTest>;
}): string {
}): string => {
const { element, resizeHandle } = resizingElement;
const shouldSwapCursors =
element && Math.sign(element.height) * Math.sign(element.width) === -1;
@ -161,12 +157,12 @@ export function getCursorForResizingElement(resizingElement: {
}
return cursor ? `${cursor}-resize` : "";
}
};
export function normalizeResizeHandle(
export const normalizeResizeHandle = (
element: ExcalidrawElement,
resizeHandle: HandlerRectanglesRet,
): HandlerRectanglesRet {
): HandlerRectanglesRet => {
if (element.width >= 0 && element.height >= 0) {
return resizeHandle;
}
@ -215,4 +211,4 @@ export function normalizeResizeHandle(
}
return resizeHandle;
}
};

@ -3,21 +3,23 @@ import { mutateElement } from "./mutateElement";
import { isLinearElement } from "./typeChecks";
import { SHIFT_LOCKING_ANGLE } from "../constants";
export function isInvisiblySmallElement(element: ExcalidrawElement): boolean {
export const isInvisiblySmallElement = (
element: ExcalidrawElement,
): boolean => {
if (isLinearElement(element)) {
return element.points.length < 2;
}
return element.width === 0 && element.height === 0;
}
};
/**
* Makes a perfect shape or diagonal/horizontal/vertical line
*/
export function getPerfectElementSize(
export const getPerfectElementSize = (
elementType: string,
width: number,
height: number,
): { width: number; height: number } {
): { width: number; height: number } => {
const absWidth = Math.abs(width);
const absHeight = Math.abs(height);
@ -42,13 +44,13 @@ export function getPerfectElementSize(
height = absWidth * Math.sign(height);
}
return { width, height };
}
};
export function resizePerfectLineForNWHandler(
export const resizePerfectLineForNWHandler = (
element: ExcalidrawElement,
x: number,
y: number,
) {
) => {
const anchorX = element.x + element.width;
const anchorY = element.y + element.height;
const distanceToAnchorX = x - anchorX;
@ -77,14 +79,14 @@ export function resizePerfectLineForNWHandler(
height: nextHeight,
});
}
}
};
/**
* @returns {boolean} whether element was normalized
*/
export function normalizeDimensions(
export const normalizeDimensions = (
element: ExcalidrawElement | null,
): element is ExcalidrawElement {
): element is ExcalidrawElement => {
if (!element || (element.width >= 0 && element.height >= 0)) {
return false;
}
@ -106,4 +108,4 @@ export function normalizeDimensions(
}
return true;
}
};

@ -4,7 +4,7 @@ import { globalSceneState } from "../scene";
import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants";
function trimText(text: string) {
const trimText = (text: string) => {
// whitespace only → trim all because we'd end up inserting invisible element
if (!text.trim()) {
return "";
@ -13,7 +13,7 @@ function trimText(text: string) {
// box calculation (there's also a bug in FF which inserts trailing newline
// for multiline texts)
return text.replace(/^\n+|\n+$/g, "");
}
};
type TextWysiwygParams = {
id: string;
@ -31,7 +31,7 @@ type TextWysiwygParams = {
onCancel: () => void;
};
export function textWysiwyg({
export const textWysiwyg = ({
id,
initText,
x,
@ -45,7 +45,7 @@ export function textWysiwyg({
textAlign,
onSubmit,
onCancel,
}: TextWysiwygParams) {
}: TextWysiwygParams) => {
const editable = document.createElement("div");
try {
editable.contentEditable = "plaintext-only";
@ -126,20 +126,20 @@ export function textWysiwyg({
}
};
function stopEvent(event: Event) {
const stopEvent = (event: Event) => {
event.stopPropagation();
}
};
function handleSubmit() {
const handleSubmit = () => {
if (editable.innerText) {
onSubmit(trimText(editable.innerText));
} else {
onCancel();
}
cleanup();
}
};
function cleanup() {
const cleanup = () => {
if (isDestroyed) {
return;
}
@ -158,7 +158,7 @@ export function textWysiwyg({
unbindUpdate();
document.body.removeChild(editable);
}
};
const rebindBlur = () => {
window.removeEventListener("pointerup", rebindBlur);
@ -210,4 +210,4 @@ export function textWysiwyg({
document.body.appendChild(editable);
editable.focus();
selectNode(editable);
}
};

@ -4,24 +4,24 @@ import {
ExcalidrawLinearElement,
} from "./types";
export function isTextElement(
export const isTextElement = (
element: ExcalidrawElement | null,
): element is ExcalidrawTextElement {
): element is ExcalidrawTextElement => {
return element != null && element.type === "text";
}
};
export function isLinearElement(
export const isLinearElement = (
element?: ExcalidrawElement | null,
): element is ExcalidrawLinearElement {
): element is ExcalidrawLinearElement => {
return (
element != null &&
(element.type === "arrow" ||
element.type === "line" ||
element.type === "draw")
);
}
};
export function isExcalidrawElement(element: any): boolean {
export const isExcalidrawElement = (element: any): boolean => {
return (
element?.type === "text" ||
element?.type === "diamond" ||
@ -31,4 +31,4 @@ export function isExcalidrawElement(element: any): boolean {
element?.type === "draw" ||
element?.type === "line"
);
}
};

@ -1,18 +1,16 @@
import { PointerCoords } from "./types";
import { normalizeScroll } from "./scene";
export function getCenter(pointers: Map<number, PointerCoords>) {
export const getCenter = (pointers: Map<number, PointerCoords>) => {
const allCoords = Array.from(pointers.values());
return {
x: normalizeScroll(sum(allCoords, (coords) => coords.x) / allCoords.length),
y: normalizeScroll(sum(allCoords, (coords) => coords.y) / allCoords.length),
};
}
};
export function getDistance([a, b]: readonly PointerCoords[]) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
export const getDistance = ([a, b]: readonly PointerCoords[]) =>
Math.hypot(a.x - b.x, a.y - b.y);
function sum<T>(array: readonly T[], mapper: (item: T) => number): number {
return array.reduce((acc, item) => acc + mapper(item), 0);
}
const sum = <T>(array: readonly T[], mapper: (item: T) => number): number =>
array.reduce((acc, item) => acc + mapper(item), 0);

@ -27,11 +27,11 @@ export class SceneHistory {
this.redoStack.length = 0;
}
private generateEntry(
private generateEntry = (
appState: AppState,
elements: readonly ExcalidrawElement[],
) {
return JSON.stringify({
) =>
JSON.stringify({
appState: clearAppStatePropertiesForHistory(appState),
elements: elements.reduce((elements, element) => {
if (
@ -69,7 +69,6 @@ export class SceneHistory {
return elements;
}, [] as Mutable<typeof elements>),
});
}
pushEntry(appState: AppState, elements: readonly ExcalidrawElement[]) {
const newEntry = this.generateEntry(appState, elements);

@ -43,20 +43,18 @@ export const languages = [
let currentLanguage = languages[0];
const fallbackLanguage = languages[0];
export function setLanguage(newLng: string | undefined) {
export const setLanguage = (newLng: string | undefined) => {
currentLanguage =
languages.find((language) => language.lng === newLng) || fallbackLanguage;
document.documentElement.dir = currentLanguage.rtl ? "rtl" : "ltr";
languageDetector.cacheUserLanguage(currentLanguage.lng);
}
};
export function getLanguage() {
return currentLanguage;
}
export const getLanguage = () => currentLanguage;
function findPartsForData(data: any, parts: string[]) {
const findPartsForData = (data: any, parts: string[]) => {
for (var i = 0; i < parts.length; ++i) {
const part = parts[i];
if (data[part] === undefined) {
@ -68,9 +66,9 @@ function findPartsForData(data: any, parts: string[]) {
return undefined;
}
return data;
}
};
export function t(path: string, replacement?: { [key: string]: string }) {
export const t = (path: string, replacement?: { [key: string]: string }) => {
const parts = path.split(".");
let translation =
findPartsForData(currentLanguage.data, parts) ||
@ -85,14 +83,12 @@ export function t(path: string, replacement?: { [key: string]: string }) {
}
}
return translation;
}
};
const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {
formatLanguageCode: function (lng: string) {
return lng;
},
formatLanguageCode: (lng: string) => lng,
isWhitelisted: () => true,
},
checkWhitelist: false,

@ -50,7 +50,7 @@ Sentry.init({
// Block pinch-zooming on iOS outside of the content area
document.addEventListener(
"touchmove",
function (event) {
(event) => {
// @ts-ignore
if (event.scale !== 1) {
event.preventDefault();

@ -2,7 +2,11 @@ import React, { useState, useEffect, useRef, useContext } from "react";
const context = React.createContext(false);
export function IsMobileProvider({ children }: { children: React.ReactNode }) {
export const IsMobileProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const query = useRef<MediaQueryList>();
if (!query.current) {
query.current = window.matchMedia
@ -24,7 +28,7 @@ export function IsMobileProvider({ children }: { children: React.ReactNode }) {
}, []);
return <context.Provider value={isMobile}>{children}</context.Provider>;
}
};
export default function useIsMobile() {
return useContext(context);

@ -20,16 +20,14 @@ export const KEYS = {
export type Key = keyof typeof KEYS;
export function isArrowKey(keyCode: string) {
return (
keyCode === KEYS.ARROW_LEFT ||
keyCode === KEYS.ARROW_RIGHT ||
keyCode === KEYS.ARROW_DOWN ||
keyCode === KEYS.ARROW_UP
);
}
export const isArrowKey = (keyCode: string) =>
keyCode === KEYS.ARROW_LEFT ||
keyCode === KEYS.ARROW_RIGHT ||
keyCode === KEYS.ARROW_DOWN ||
keyCode === KEYS.ARROW_UP;
export const getResizeCenterPointKey = (event: MouseEvent | KeyboardEvent) =>
event.altKey || event.which === KEYS.ALT_KEY_CODE;
export const getResizeWithSidesSameLengthKey = (event: MouseEvent) =>
event.shiftKey;

@ -2,14 +2,14 @@ import { Point } from "./types";
import { LINE_CONFIRM_THRESHOLD } from "./constants";
// https://stackoverflow.com/a/6853926/232122
export function distanceBetweenPointAndSegment(
export const distanceBetweenPointAndSegment = (
x: number,
y: number,
x1: number,
y1: number,
x2: number,
y2: number,
) {
) => {
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
@ -38,23 +38,22 @@ export function distanceBetweenPointAndSegment(
const dx = x - xx;
const dy = y - yy;
return Math.hypot(dx, dy);
}
};
export function rotate(
export const rotate = (
x1: number,
y1: number,
x2: number,
y2: number,
angle: number,
): [number, number] {
): [number, number] =>
// 𝑎𝑥=(𝑎𝑥𝑐𝑥)cos𝜃(𝑎𝑦𝑐𝑦)sin𝜃+𝑐𝑥
// 𝑎𝑦=(𝑎𝑥𝑐𝑥)sin𝜃+(𝑎𝑦𝑐𝑦)cos𝜃+𝑐𝑦.
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
return [
[
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
];
}
export const adjustXYWithRotation = (
side: "n" | "s" | "w" | "e" | "nw" | "ne" | "sw" | "se",
@ -233,15 +232,15 @@ export const getPointOnAPath = (point: Point, path: Point[]) => {
return null;
};
export function distance2d(x1: number, y1: number, x2: number, y2: number) {
export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
const xd = x2 - x1;
const yd = y2 - y1;
return Math.hypot(xd, yd);
}
};
// Checks if the first and last point are close enough
// to be considered a loop
export function isPathALoop(points: Point[]): boolean {
export const isPathALoop = (points: Point[]): boolean => {
if (points.length >= 3) {
const [firstPoint, lastPoint] = [points[0], points[points.length - 1]];
return (
@ -250,16 +249,16 @@ export function isPathALoop(points: Point[]): boolean {
);
}
return false;
}
};
// Draw a line from the point to the right till infiinty
// Check how many lines of the polygon does this infinite line intersects with
// If the number of intersections is odd, point is in the polygon
export function isPointInPolygon(
export const isPointInPolygon = (
points: Point[],
x: number,
y: number,
): boolean {
): boolean => {
const vertices = points.length;
// There must be at least 3 vertices in polygon
@ -281,32 +280,32 @@ export function isPointInPolygon(
}
// true if count is off
return count % 2 === 1;
}
};
// Check if q lies on the line segment pr
function onSegment(p: Point, q: Point, r: Point) {
const onSegment = (p: Point, q: Point, r: Point) => {
return (
q[0] <= Math.max(p[0], r[0]) &&
q[0] >= Math.min(p[0], r[0]) &&
q[1] <= Math.max(p[1], r[1]) &&
q[1] >= Math.min(p[1], r[1])
);
}
};
// For the ordered points p, q, r, return
// 0 if p, q, r are collinear
// 1 if Clockwise
// 2 if counterclickwise
function orientation(p: Point, q: Point, r: Point) {
const orientation = (p: Point, q: Point, r: Point) => {
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
if (val === 0) {
return 0;
}
return val > 0 ? 1 : 2;
}
};
// Check is p1q1 intersects with p2q2
function doIntersect(p1: Point, q1: Point, p2: Point, q2: Point) {
const doIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
const o1 = orientation(p1, q1, p2);
const o2 = orientation(p1, q1, q2);
const o3 = orientation(p2, q2, p1);
@ -337,4 +336,4 @@ function doIntersect(p1: Point, q1: Point, p2: Point, q2: Point) {
}
return false;
}
};

@ -1,18 +1,18 @@
import { Point } from "./types";
export function getSizeFromPoints(points: readonly Point[]) {
export const getSizeFromPoints = (points: readonly Point[]) => {
const xs = points.map((point) => point[0]);
const ys = points.map((point) => point[1]);
return {
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys),
};
}
export function rescalePoints(
};
export const rescalePoints = (
dimension: 0 | 1,
nextDimensionSize: number,
prevPoints: readonly Point[],
): Point[] {
): Point[] => {
const prevDimValues = prevPoints.map((point) => point[dimension]);
const prevMaxDimension = Math.max(...prevDimValues);
const prevMinDimension = Math.min(...prevDimValues);
@ -50,4 +50,4 @@ export function rescalePoints(
);
return nextPoints;
}
};

@ -4,15 +4,12 @@ import nanoid from "nanoid";
let random = new Random(Date.now());
let testIdBase = 0;
export function randomInteger() {
return Math.floor(random.next() * 2 ** 31);
}
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
export function reseed(seed: number) {
export const reseed = (seed: number) => {
random = new Random(seed);
testIdBase = 0;
}
};
export function randomId() {
return process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();
}
export const randomId = () =>
process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();

@ -31,10 +31,10 @@ export interface ExcalidrawElementWithCanvas {
canvasOffsetY: number;
}
function generateElementCanvas(
const generateElementCanvas = (
element: NonDeletedExcalidrawElement,
zoom: number,
): ExcalidrawElementWithCanvas {
): ExcalidrawElementWithCanvas => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
@ -75,13 +75,13 @@ function generateElementCanvas(
1 / (window.devicePixelRatio * zoom),
);
return { element, canvas, canvasZoom: zoom, canvasOffsetX, canvasOffsetY };
}
};
function drawElementOnCanvas(
const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
) {
) => {
context.globalAlpha = element.opacity / 100;
switch (element.type) {
case "rectangle":
@ -132,7 +132,7 @@ function drawElementOnCanvas(
}
}
context.globalAlpha = 1;
}
};
const elementWithCanvasCache = new WeakMap<
ExcalidrawElement,
@ -144,15 +144,13 @@ const shapeCache = new WeakMap<
Drawable | Drawable[] | null
>();
export function getShapeForElement(element: ExcalidrawElement) {
return shapeCache.get(element);
}
export const getShapeForElement = (element: ExcalidrawElement) =>
shapeCache.get(element);
export function invalidateShapeForElement(element: ExcalidrawElement) {
export const invalidateShapeForElement = (element: ExcalidrawElement) =>
shapeCache.delete(element);
}
export function generateRoughOptions(element: ExcalidrawElement): Options {
export const generateRoughOptions = (element: ExcalidrawElement): Options => {
const options: Options = {
seed: element.seed,
strokeLineDash:
@ -214,13 +212,13 @@ export function generateRoughOptions(element: ExcalidrawElement): Options {
throw new Error(`Unimplemented type ${element.type}`);
}
}
}
};
function generateElement(
const generateElement = (
element: NonDeletedExcalidrawElement,
generator: RoughGenerator,
sceneState?: SceneState,
) {
) => {
let shape = shapeCache.get(element) || null;
if (!shape) {
elementWithCanvasCache.delete(element);
@ -319,14 +317,14 @@ function generateElement(
return elementWithCanvas;
}
return prevElementWithCanvas;
}
};
function drawElementFromCanvas(
const drawElementFromCanvas = (
elementWithCanvas: ExcalidrawElementWithCanvas,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
sceneState: SceneState,
) {
) => {
const element = elementWithCanvas.element;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
@ -346,15 +344,15 @@ function drawElementFromCanvas(
context.rotate(-element.angle);
context.translate(-cx, -cy);
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
};
export function renderElement(
export const renderElement = (
element: NonDeletedExcalidrawElement,
rc: RoughCanvas,
context: CanvasRenderingContext2D,
renderOptimizations: boolean,
sceneState: SceneState,
) {
) => {
const generator = rc.generator;
switch (element.type) {
case "selection": {
@ -404,15 +402,15 @@ export function renderElement(
throw new Error(`Unimplemented type ${element.type}`);
}
}
}
};
export function renderElementToSvg(
export const renderElementToSvg = (
element: NonDeletedExcalidrawElement,
rsvg: RoughSVG,
svgRoot: SVGElement,
offsetX?: number,
offsetY?: number,
) {
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x2 - x1) / 2 - (element.x - x1);
const cy = (y2 - y1) / 2 - (element.y - y1);
@ -528,4 +526,4 @@ export function renderElementToSvg(
}
}
}
}
};

@ -30,7 +30,7 @@ import colors from "../colors";
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
function colorsForClientId(clientId: string) {
const colorsForClientId = (clientId: string) => {
// Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
@ -41,9 +41,9 @@ function colorsForClientId(clientId: string) {
background: backgrounds[sum % backgrounds.length],
stroke: strokes[sum % strokes.length],
};
}
};
function strokeRectWithRotation(
const strokeRectWithRotation = (
context: CanvasRenderingContext2D,
x: number,
y: number,
@ -53,7 +53,7 @@ function strokeRectWithRotation(
cy: number,
angle: number,
fill?: boolean,
) {
) => {
context.translate(cx, cy);
context.rotate(angle);
if (fill) {
@ -62,22 +62,22 @@ function strokeRectWithRotation(
context.strokeRect(x - cx, y - cy, width, height);
context.rotate(-angle);
context.translate(-cx, -cy);
}
};
function strokeCircle(
const strokeCircle = (
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
) {
) => {
context.beginPath();
context.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
context.fill();
context.stroke();
}
};
export function renderScene(
export const renderScene = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
selectionElement: NonDeletedExcalidrawElement | null,
@ -98,7 +98,7 @@ export function renderScene(
renderSelection?: boolean;
renderOptimizations?: boolean;
} = {},
) {
) => {
if (!canvas) {
return { atLeastOneVisibleElement: false };
}
@ -461,9 +461,9 @@ export function renderScene(
context.scale(1 / scale, 1 / scale);
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
}
};
function isVisibleElement(
const isVisibleElement = (
element: ExcalidrawElement,
viewportWidth: number,
viewportHeight: number,
@ -476,7 +476,7 @@ function isVisibleElement(
scrollY: FlooredNumber;
zoom: number;
},
) {
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
// Apply zoom
@ -492,10 +492,10 @@ function isVisibleElement(
y2 + scrollY - viewportHeightDiff / 2 >= 0 &&
y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom
);
}
};
// This should be only called for exporting purposes
export function renderSceneToSvg(
export const renderSceneToSvg = (
elements: readonly NonDeletedExcalidrawElement[],
rsvg: RoughSVG,
svgRoot: SVGElement,
@ -506,7 +506,7 @@ export function renderSceneToSvg(
offsetX?: number;
offsetY?: number;
} = {},
) {
) => {
if (!svgRoot) {
return;
}
@ -522,4 +522,4 @@ export function renderSceneToSvg(
);
}
});
}
};

@ -8,14 +8,14 @@
* @param {Number} height The height of the rectangle
* @param {Number} radius The corner radius
*/
export function roundRect(
export const roundRect = (
context: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number,
radius: number,
) {
) => {
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width - radius, y);
@ -34,4 +34,4 @@ export function roundRect(
context.closePath();
context.fill();
context.stroke();
}
};

@ -23,13 +23,13 @@ export const hasStroke = (type: string) =>
export const hasText = (type: string) => type === "text";
export function getElementAtPosition(
export const getElementAtPosition = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
x: number,
y: number,
zoom: number,
) {
) => {
let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array)
for (let i = elements.length - 1; i >= 0; --i) {
@ -43,13 +43,13 @@ export function getElementAtPosition(
}
return hitElement;
}
};
export function getElementContainingPosition(
export const getElementContainingPosition = (
elements: readonly ExcalidrawElement[],
x: number,
y: number,
) {
) => {
let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array)
for (let i = elements.length - 1; i >= 0; --i) {
@ -63,4 +63,4 @@ export function getElementContainingPosition(
}
}
return hitElement;
}
};

@ -11,7 +11,7 @@ import { t } from "../i18n";
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
export function exportToCanvas(
export const exportToCanvas = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
{
@ -27,16 +27,13 @@ export function exportToCanvas(
viewBackgroundColor: string;
shouldAddWatermark: boolean;
},
createCanvas: (width: number, height: number) => any = function (
width,
height,
) {
createCanvas: (width: number, height: number) => any = (width, height) => {
const tempCanvas = document.createElement("canvas");
tempCanvas.width = width * scale;
tempCanvas.height = height * scale;
return tempCanvas;
},
) {
) => {
let sceneElements = elements;
if (shouldAddWatermark) {
const [, , maxX, maxY] = getCommonBounds(elements);
@ -78,9 +75,9 @@ export function exportToCanvas(
);
return tempCanvas;
}
};
export function exportToSvg(
export const exportToSvg = (
elements: readonly NonDeletedExcalidrawElement[],
{
exportBackground,
@ -93,7 +90,7 @@ export function exportToSvg(
viewBackgroundColor: string;
shouldAddWatermark: boolean;
},
): SVGSVGElement {
): SVGSVGElement => {
let sceneElements = elements;
if (shouldAddWatermark) {
const [, , maxX, maxY] = getCommonBounds(elements);
@ -148,9 +145,9 @@ export function exportToSvg(
});
return svgRoot;
}
};
function getWatermarkElement(maxX: number, maxY: number) {
const getWatermarkElement = (maxX: number, maxY: number) => {
const text = t("labels.madeWithExcalidraw");
const font = "16px Virgil";
const { width: textWidth } = measureText(text, font);
@ -169,4 +166,4 @@ function getWatermarkElement(maxX: number, maxY: number) {
roughness: 1,
opacity: 100,
});
}
};

@ -2,13 +2,12 @@ import { FlooredNumber } from "../types";
import { ExcalidrawElement } from "../element/types";
import { getCommonBounds } from "../element";
export function normalizeScroll(pos: number) {
return Math.floor(pos) as FlooredNumber;
}
export const normalizeScroll = (pos: number) =>
Math.floor(pos) as FlooredNumber;
export function calculateScrollCenter(
export const calculateScrollCenter = (
elements: readonly ExcalidrawElement[],
): { scrollX: FlooredNumber; scrollY: FlooredNumber } {
): { scrollX: FlooredNumber; scrollY: FlooredNumber } => {
if (!elements.length) {
return {
scrollX: normalizeScroll(0),
@ -25,4 +24,4 @@ export function calculateScrollCenter(
scrollX: normalizeScroll(window.innerWidth / 2 - centerX),
scrollY: normalizeScroll(window.innerHeight / 2 - centerY),
};
}
};

@ -9,7 +9,7 @@ export const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
export function getScrollBars(
export const getScrollBars = (
elements: readonly ExcalidrawElement[],
viewportWidth: number,
viewportHeight: number,
@ -22,7 +22,7 @@ export function getScrollBars(
scrollY: FlooredNumber;
zoom: number;
},
): ScrollBars {
): ScrollBars => {
// This is the bounding box of all the elements
const [
elementsMinX,
@ -100,9 +100,13 @@ export function getScrollBars(
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
},
};
}
};
export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) {
export const isOverScrollBars = (
scrollBars: ScrollBars,
x: number,
y: number,
) => {
const [isOverHorizontalScrollBar, isOverVerticalScrollBar] = [
scrollBars.horizontal,
scrollBars.vertical,
@ -120,4 +124,4 @@ export function isOverScrollBars(scrollBars: ScrollBars, x: number, y: number) {
isOverHorizontalScrollBar,
isOverVerticalScrollBar,
};
}
};

@ -6,10 +6,10 @@ import { getElementAbsoluteCoords, getElementBounds } from "../element";
import { AppState } from "../types";
import { newElementWith } from "../element/mutateElement";
export function getElementsWithinSelection(
export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement,
) {
) => {
const [
selectionX1,
selectionY1,
@ -29,12 +29,12 @@ export function getElementsWithinSelection(
selectionY2 >= elementY2
);
});
}
};
export function deleteSelectedElements(
export const deleteSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) {
) => {
return {
elements: elements.map((el) => {
if (appState.selectedElementIds[el.id]) {
@ -47,24 +47,24 @@ export function deleteSelectedElements(
selectedElementIds: {},
},
};
}
};
export function isSomeElementSelected(
export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
): boolean {
): boolean => {
return elements.some((element) => appState.selectedElementIds[element.id]);
}
};
/**
* Returns common attribute (picked by `getAttribute` callback) of selected
* elements. If elements don't share the same value, returns `null`.
*/
export function getCommonAttributeOfSelectedElements<T>(
export const getCommonAttributeOfSelectedElements = <T>(
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
getAttribute: (element: ExcalidrawElement) => T,
): T | null {
): T | null => {
const attributes = Array.from(
new Set(
getSelectedElements(elements, appState).map((element) =>
@ -73,20 +73,20 @@ export function getCommonAttributeOfSelectedElements<T>(
),
);
return attributes.length === 1 ? attributes[0] : null;
}
};
export function getSelectedElements(
export const getSelectedElements = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) {
) => {
return elements.filter((element) => appState.selectedElementIds[element.id]);
}
};
export function getTargetElement(
export const getTargetElement = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) {
) => {
return appState.editingElement
? [appState.editingElement]
: getSelectedElements(elements, appState);
}
};

@ -1,4 +1,7 @@
export function getZoomOrigin(canvas: HTMLCanvasElement | null, scale: number) {
export const getZoomOrigin = (
canvas: HTMLCanvasElement | null,
scale: number,
) => {
if (canvas === null) {
return { x: 0, y: 0 };
}
@ -14,10 +17,10 @@ export function getZoomOrigin(canvas: HTMLCanvasElement | null, scale: number) {
x: normalizedCanvasWidth / 2,
y: normalizedCanvasHeight / 2,
};
}
};
export function getNormalizedZoom(zoom: number): number {
export const getNormalizedZoom = (zoom: number): number => {
const normalizedZoom = parseFloat(zoom.toFixed(2));
const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));
return clampedZoom;
}
};

@ -25,7 +25,7 @@ type Config = {
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
export const register = (config?: Config) => {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
@ -57,9 +57,9 @@ export function register(config?: Config) {
}
});
}
}
};
function registerValidSW(swUrl: string, config?: Config) {
const registerValidSW = (swUrl: string, config?: Config) => {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
@ -103,9 +103,9 @@ function registerValidSW(swUrl: string, config?: Config) {
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
};
function checkValidServiceWorker(swUrl: string, config?: Config) {
const checkValidServiceWorker = (swUrl: string, config?: Config) => {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
@ -133,9 +133,9 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
// "No internet connection found. App is running in offline mode.",
// );
});
}
};
export function unregister() {
export const unregister = () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
@ -145,4 +145,4 @@ export function unregister() {
console.error(error.message);
});
}
}
};

@ -100,10 +100,7 @@ export const shapesShortcutKeys = SHAPES.map((shape, index) => [
(index + 1).toString(),
]).flat(1);
export function findShapeByKey(key: string) {
return (
SHAPES.find((shape, index) => {
return shape.key === key.toLowerCase() || key === (index + 1).toString();
})?.value || "selection"
);
}
export const findShapeByKey = (key: string) =>
SHAPES.find((shape, index) => {
return shape.key === key.toLowerCase() || key === (index + 1).toString();
})?.value || "selection";

@ -16,20 +16,20 @@ const renderScene = jest.spyOn(Renderer, "renderScene");
let getByToolName: (name: string) => HTMLElement = null!;
let canvas: HTMLCanvasElement = null!;
function clickTool(toolName: ToolName) {
const clickTool = (toolName: ToolName) => {
fireEvent.click(getByToolName(toolName));
}
};
let lastClientX = 0;
let lastClientY = 0;
let pointerType: "mouse" | "pen" | "touch" = "mouse";
function pointerDown(
const pointerDown = (
clientX: number = lastClientX,
clientY: number = lastClientY,
altKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
lastClientX = clientX;
lastClientY = clientY;
fireEvent.pointerDown(canvas, {
@ -40,41 +40,41 @@ function pointerDown(
pointerId: 1,
pointerType,
});
}
};
function pointer2Down(clientX: number, clientY: number) {
const pointer2Down = (clientX: number, clientY: number) => {
fireEvent.pointerDown(canvas, {
clientX,
clientY,
pointerId: 2,
pointerType,
});
}
};
function pointer2Move(clientX: number, clientY: number) {
const pointer2Move = (clientX: number, clientY: number) => {
fireEvent.pointerMove(canvas, {
clientX,
clientY,
pointerId: 2,
pointerType,
});
}
};
function pointer2Up(clientX: number, clientY: number) {
const pointer2Up = (clientX: number, clientY: number) => {
fireEvent.pointerUp(canvas, {
clientX,
clientY,
pointerId: 2,
pointerType,
});
}
};
function pointerMove(
const pointerMove = (
clientX: number = lastClientX,
clientY: number = lastClientY,
altKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
lastClientX = clientX;
lastClientY = clientY;
fireEvent.pointerMove(canvas, {
@ -85,72 +85,72 @@ function pointerMove(
pointerId: 1,
pointerType,
});
}
};
function pointerUp(
const pointerUp = (
clientX: number = lastClientX,
clientY: number = lastClientY,
altKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
lastClientX = clientX;
lastClientY = clientY;
fireEvent.pointerUp(canvas, { pointerId: 1, pointerType, shiftKey, altKey });
}
};
function hotkeyDown(key: Key) {
const hotkeyDown = (key: Key) => {
fireEvent.keyDown(document, { key: KEYS[key] });
}
};
function hotkeyUp(key: Key) {
const hotkeyUp = (key: Key) => {
fireEvent.keyUp(document, {
key: KEYS[key],
});
}
};
function keyDown(
const keyDown = (
key: string,
ctrlKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
fireEvent.keyDown(document, { key, ctrlKey, shiftKey });
}
};
function keyUp(
const keyUp = (
key: string,
ctrlKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
fireEvent.keyUp(document, {
key,
ctrlKey,
shiftKey,
});
}
};
function hotkeyPress(key: Key) {
const hotkeyPress = (key: Key) => {
hotkeyDown(key);
hotkeyUp(key);
}
};
function keyPress(
const keyPress = (
key: string,
ctrlKey: boolean = false,
shiftKey: boolean = false,
) {
) => {
keyDown(key, ctrlKey, shiftKey);
keyUp(key, ctrlKey, shiftKey);
}
};
function clickLabeledElement(label: string) {
const clickLabeledElement = (label: string) => {
const element = document.querySelector(`[aria-label='${label}']`);
if (!element) {
throw new Error(`No labeled element found: ${label}`);
}
fireEvent.click(element);
}
};
function getSelectedElement(): ExcalidrawElement {
const getSelectedElement = (): ExcalidrawElement => {
const selectedElements = h.elements.filter(
(element) => h.state.selectedElementIds[element.id],
);
@ -160,10 +160,10 @@ function getSelectedElement(): ExcalidrawElement {
);
}
return selectedElements[0];
}
};
type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
function getResizeHandles() {
const getResizeHandles = () => {
const rects = handlerRectangles(
getSelectedElement(),
h.state.zoom,
@ -181,14 +181,14 @@ function getResizeHandles() {
}
return rv;
}
};
/**
* This is always called at the end of your test, so usually you don't need to call it.
* However, if you have a long test, you might want to call it during the test so it's easier
* to debug where a test failure came from.
*/
function checkpoint(name: string) {
const checkpoint = (name: string) => {
expect(renderScene.mock.calls.length).toMatchSnapshot(
`[${name}] number of renders`,
);
@ -198,7 +198,7 @@ function checkpoint(name: string) {
h.elements.forEach((element, i) =>
expect(element).toMatchSnapshot(`[${name}] element ${i}`),
);
}
};
beforeEach(() => {
// Unmount ReactDOM from root

@ -22,9 +22,9 @@ beforeEach(() => {
const { h } = window;
function populateElements(
const populateElements = (
elements: { id: string; isDeleted?: boolean; isSelected?: boolean }[],
) {
) => {
const selectedElementIds: any = {};
h.elements = elements.map(({ id, isDeleted = false, isSelected = false }) => {
@ -54,7 +54,7 @@ function populateElements(
});
return selectedElementIds;
}
};
type Actions =
| typeof actionBringForward
@ -62,20 +62,20 @@ type Actions =
| typeof actionBringToFront
| typeof actionSendToBack;
function assertZindex({
const assertZindex = ({
elements,
operations,
}: {
elements: { id: string; isDeleted?: true; isSelected?: true }[];
operations: [Actions, string[]][];
}) {
}) => {
const selectedElementIds = populateElements(elements);
operations.forEach(([action, expected]) => {
h.app.actionManager.executeAction(action);
expect(h.elements.map((element) => element.id)).toEqual(expected);
expect(h.state.selectedElementIds).toEqual(selectedElementIds);
});
}
};
describe("z-index manipulation", () => {
it("send back", () => {

@ -6,9 +6,9 @@ export const SVG_NS = "http://www.w3.org/2000/svg";
let mockDateTime: string | null = null;
export function setDateTimeForTests(dateTime: string) {
export const setDateTimeForTests = (dateTime: string) => {
mockDateTime = dateTime;
}
};
export const getDateTime = () => {
if (mockDateTime) {
@ -25,51 +25,43 @@ export const getDateTime = () => {
return `${year}-${month}-${day}-${hr}${min}`;
};
export function capitalizeString(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const capitalizeString = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1);
export function isToolIcon(
export const isToolIcon = (
target: Element | EventTarget | null,
): target is HTMLElement {
return target instanceof HTMLElement && target.className.includes("ToolIcon");
}
): target is HTMLElement =>
target instanceof HTMLElement && target.className.includes("ToolIcon");
export function isInputLike(
export const isInputLike = (
target: Element | EventTarget | null,
): target is
| HTMLInputElement
| HTMLTextAreaElement
| HTMLSelectElement
| HTMLBRElement
| HTMLDivElement {
return (
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
target instanceof HTMLBRElement || // newline in wysiwyg
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement
);
}
export function isWritableElement(
| HTMLDivElement =>
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
target instanceof HTMLBRElement || // newline in wysiwyg
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement;
export const isWritableElement = (
target: Element | EventTarget | null,
): target is
| HTMLInputElement
| HTMLTextAreaElement
| HTMLBRElement
| HTMLDivElement {
return (
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
target instanceof HTMLBRElement || // newline in wysiwyg
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLInputElement &&
(target.type === "text" || target.type === "number"))
);
}
| HTMLDivElement =>
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
target instanceof HTMLBRElement || // newline in wysiwyg
target instanceof HTMLTextAreaElement ||
(target instanceof HTMLInputElement &&
(target.type === "text" || target.type === "number"));
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
export function measureText(text: string, font: string) {
export const measureText = (text: string, font: string) => {
const line = document.createElement("div");
const body = document.body;
line.style.position = "absolute";
@ -93,12 +85,12 @@ export function measureText(text: string, font: string) {
document.body.removeChild(line);
return { width, height, baseline };
}
};
export function debounce<T extends any[]>(
export const debounce = <T extends any[]>(
fn: (...args: T) => void,
timeout: number,
) {
) => {
let handle = 0;
let lastArgs: T;
const ret = (...args: T) => {
@ -111,9 +103,9 @@ export function debounce<T extends any[]>(
fn(...lastArgs);
};
return ret;
}
};
export function selectNode(node: Element) {
export const selectNode = (node: Element) => {
const selection = window.getSelection();
if (selection) {
const range = document.createRange();
@ -121,30 +113,28 @@ export function selectNode(node: Element) {
selection.removeAllRanges();
selection.addRange(range);
}
}
};
export function removeSelection() {
export const removeSelection = () => {
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
}
};
export function distance(x: number, y: number) {
return Math.abs(x - y);
}
export const distance = (x: number, y: number) => Math.abs(x - y);
export function resetCursor() {
export const resetCursor = () => {
document.documentElement.style.cursor = "";
}
};
export function setCursorForShape(shape: string) {
export const setCursorForShape = (shape: string) => {
if (shape === "selection") {
resetCursor();
} else {
document.documentElement.style.cursor = CURSOR_TYPE.CROSSHAIR;
}
}
};
export const isFullScreen = () =>
document.fullscreenElement?.nodeName === "HTML";
@ -165,7 +155,7 @@ export const getShortcutKey = (shortcut: string): string => {
}
return `${shortcut.replace(/CtrlOrCmd/i, "Ctrl")}`;
};
export function viewportCoordsToSceneCoords(
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
{
scrollX,
@ -178,7 +168,7 @@ export function viewportCoordsToSceneCoords(
},
canvas: HTMLCanvasElement | null,
scale: number,
) {
) => {
const zoomOrigin = getZoomOrigin(canvas, scale);
const clientXWithZoom = zoomOrigin.x + (clientX - zoomOrigin.x) / zoom;
const clientYWithZoom = zoomOrigin.y + (clientY - zoomOrigin.y) / zoom;
@ -187,9 +177,9 @@ export function viewportCoordsToSceneCoords(
const y = clientYWithZoom - scrollY;
return { x, y };
}
};
export function sceneCoordsToViewportCoords(
export const sceneCoordsToViewportCoords = (
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
{
scrollX,
@ -202,7 +192,7 @@ export function sceneCoordsToViewportCoords(
},
canvas: HTMLCanvasElement | null,
scale: number,
) {
) => {
const zoomOrigin = getZoomOrigin(canvas, scale);
const sceneXWithZoomAndScroll =
zoomOrigin.x - (zoomOrigin.x - sceneX - scrollX) * zoom;
@ -213,10 +203,7 @@ export function sceneCoordsToViewportCoords(
const y = sceneYWithZoomAndScroll;
return { x, y };
}
};
export function getGlobalCSSVariable(name: string) {
return getComputedStyle(document.documentElement).getPropertyValue(
`--${name}`,
);
}
export const getGlobalCSSVariable = (name: string) =>
getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);

@ -1,14 +1,14 @@
import { moveOneLeft, moveOneRight, moveAllLeft, moveAllRight } from "./zindex";
function expectMove<T>(
const expectMove = <T>(
fn: (elements: T[], indicesToMove: number[]) => void,
elems: T[],
indices: number[],
equal: T[],
) {
) => {
fn(elems, indices);
expect(elems).toEqual(equal);
}
};
it("should moveOneLeft", () => {
expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 2], ["b", "c", "a", "d"]);

@ -1,10 +1,10 @@
function swap<T>(elements: T[], indexA: number, indexB: number) {
const swap = <T>(elements: T[], indexA: number, indexB: number) => {
const element = elements[indexA];
elements[indexA] = elements[indexB];
elements[indexB] = element;
}
};
export function moveOneLeft<T>(elements: T[], indicesToMove: number[]) {
export const moveOneLeft = <T>(elements: T[], indicesToMove: number[]) => {
indicesToMove.sort((a: number, b: number) => a - b);
let isSorted = true;
// We go from left to right to avoid overriding the wrong elements
@ -19,9 +19,9 @@ export function moveOneLeft<T>(elements: T[], indicesToMove: number[]) {
});
return elements;
}
};
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
export const moveOneRight = <T>(elements: T[], indicesToMove: number[]) => {
const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a,
);
@ -38,7 +38,7 @@ export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
swap(elements, index + 1, index);
});
return elements;
}
};
// Let's go through an example
// | |
@ -86,7 +86,7 @@ export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
// [c, f, a, b, d, e, g]
//
// And we are done!
export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
export const moveAllLeft = <T>(elements: T[], indicesToMove: number[]) => {
indicesToMove.sort((a: number, b: number) => a - b);
// Copy the elements to move
@ -117,7 +117,7 @@ export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
});
return elements;
}
};
// Let's go through an example
// | |
@ -164,7 +164,7 @@ export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
// [a, b, d, e, g, c, f]
//
// And we are done!
export function moveAllRight<T>(elements: T[], indicesToMove: number[]) {
export const moveAllRight = <T>(elements: T[], indicesToMove: number[]) => {
const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a,
);
@ -199,4 +199,4 @@ export function moveAllRight<T>(elements: T[], indicesToMove: number[]) {
});
return elements;
}
};

Loading…
Cancel
Save