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

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

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

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

@ -11,7 +11,7 @@ import Stack from "./Stack";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
export function SelectedShapeActions({ export const SelectedShapeActions = ({
appState, appState,
elements, elements,
renderAction, renderAction,
@ -21,7 +21,7 @@ export function SelectedShapeActions({
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
elementType: ExcalidrawElement["type"]; elementType: ExcalidrawElement["type"];
}) { }) => {
const targetElements = getTargetElement( const targetElements = getTargetElement(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
@ -83,16 +83,15 @@ export function SelectedShapeActions({
)} )}
</div> </div>
); );
} };
export function ShapesSwitcher({ export const ShapesSwitcher = ({
elementType, elementType,
setAppState, setAppState,
}: { }: {
elementType: ExcalidrawElement["type"]; elementType: ExcalidrawElement["type"];
setAppState: any; setAppState: any;
}) { }) => (
return (
<> <>
{SHAPES.map(({ value, icon, key }, index) => { {SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`); const label = t(`toolBar.${value}`);
@ -125,16 +124,14 @@ export function ShapesSwitcher({
})} })}
</> </>
); );
}
export function ZoomActions({ export const ZoomActions = ({
renderAction, renderAction,
zoom, zoom,
}: { }: {
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
zoom: number; zoom: number;
}) { }) => (
return (
<Stack.Col gap={1}> <Stack.Col gap={1}>
<Stack.Row gap={1} align="center"> <Stack.Row gap={1} align="center">
{renderAction("zoomIn")} {renderAction("zoomIn")}
@ -144,4 +141,3 @@ export function ZoomActions({
</Stack.Row> </Stack.Row>
</Stack.Col> </Stack.Col>
); );
}

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

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
export function ButtonSelect<T>({ export const ButtonSelect = <T extends Object>({
options, options,
value, value,
onChange, onChange,
@ -10,8 +10,7 @@ export function ButtonSelect<T>({
value: T | null; value: T | null;
onChange: (value: T) => void; onChange: (value: T) => void;
group: string; group: string;
}) { }) => (
return (
<div className="buttonList"> <div className="buttonList">
{options.map((option) => ( {options.map((option) => (
<label <label
@ -29,4 +28,3 @@ export function ButtonSelect<T>({
))} ))}
</div> </div>
); );
}

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

@ -16,8 +16,7 @@ type Props = {
left: number; left: number;
}; };
function ContextMenu({ options, onCloseRequest, top, left }: Props) { const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => (
return (
<Popover <Popover
onCloseRequest={onCloseRequest} onCloseRequest={onCloseRequest}
top={top} top={top}
@ -36,25 +35,22 @@ function ContextMenu({ options, onCloseRequest, top, left }: Props) {
</ul> </ul>
</Popover> </Popover>
); );
}
function ContextMenuOption({ label, action }: ContextMenuOption) { const ContextMenuOption = ({ label, action }: ContextMenuOption) => (
return (
<button className="context-menu-option" onClick={action}> <button className="context-menu-option" onClick={action}>
{label} {label}
</button> </button>
); );
}
let contextMenuNode: HTMLDivElement; let contextMenuNode: HTMLDivElement;
function getContextMenuNode(): HTMLDivElement { const getContextMenuNode = (): HTMLDivElement => {
if (contextMenuNode) { if (contextMenuNode) {
return contextMenuNode; return contextMenuNode;
} }
const div = document.createElement("div"); const div = document.createElement("div");
document.body.appendChild(div); document.body.appendChild(div);
return (contextMenuNode = div); return (contextMenuNode = div);
} };
type ContextMenuParams = { type ContextMenuParams = {
options: (ContextMenuOption | false | null | undefined)[]; options: (ContextMenuOption | false | null | undefined)[];
@ -62,9 +58,9 @@ type ContextMenuParams = {
left: number; left: number;
}; };
function handleClose() { const handleClose = () => {
unmountComponentAtNode(getContextMenuNode()); unmountComponentAtNode(getContextMenuNode());
} };
export default { export default {
push(params: ContextMenuParams) { push(params: ContextMenuParams) {

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

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

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

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

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

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import * as i18n from "../i18n"; import * as i18n from "../i18n";
export function LanguageList({ export const LanguageList = ({
onChange, onChange,
languages = i18n.languages, languages = i18n.languages,
currentLanguage = i18n.getLanguage().lng, currentLanguage = i18n.getLanguage().lng,
@ -11,8 +11,7 @@ export function LanguageList({
onChange: (value: string) => void; onChange: (value: string) => void;
currentLanguage?: string; currentLanguage?: string;
floating?: boolean; floating?: boolean;
}) { }) => (
return (
<React.Fragment> <React.Fragment>
<select <select
className={`dropdown-select dropdown-select__language${ className={`dropdown-select dropdown-select__language${
@ -30,4 +29,3 @@ export function LanguageList({
</select> </select>
</React.Fragment> </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}`; const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
return ( return (
@ -64,4 +64,4 @@ export function LockIcon(props: LockIconProps) {
</div> </div>
</label> </label>
); );
} };

@ -29,7 +29,7 @@ type MobileMenuProps = {
onLockToggle: () => void; onLockToggle: () => void;
}; };
export function MobileMenu({ export const MobileMenu = ({
appState, appState,
elements, elements,
actionManager, actionManager,
@ -39,8 +39,7 @@ export function MobileMenu({
onUsernameChange, onUsernameChange,
onRoomDestroy, onRoomDestroy,
onLockToggle, onLockToggle,
}: MobileMenuProps) { }: MobileMenuProps) => (
return (
<> <>
{appState.isLoading && <LoadingMessage />} {appState.isLoading && <LoadingMessage />}
<FixedSideContainer side="top"> <FixedSideContainer side="top">
@ -143,4 +142,3 @@ export function MobileMenu({
</div> </div>
</> </>
); );
}

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

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

@ -9,7 +9,7 @@ import { copyTextToSystemClipboard } from "../clipboard";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { AppState } from "../types"; import { AppState } from "../types";
function RoomModal({ const RoomModal = ({
activeRoomLink, activeRoomLink,
username, username,
onUsernameChange, onUsernameChange,
@ -23,21 +23,21 @@ function RoomModal({
onRoomCreate: () => void; onRoomCreate: () => void;
onRoomDestroy: () => void; onRoomDestroy: () => void;
onPressingEnter: () => void; onPressingEnter: () => void;
}) { }) => {
const roomLinkInput = useRef<HTMLInputElement>(null); const roomLinkInput = useRef<HTMLInputElement>(null);
function copyRoomLink() { const copyRoomLink = () => {
copyTextToSystemClipboard(activeRoomLink); copyTextToSystemClipboard(activeRoomLink);
if (roomLinkInput.current) { if (roomLinkInput.current) {
roomLinkInput.current.select(); roomLinkInput.current.select();
} }
} };
function selectInput(event: React.MouseEvent<HTMLInputElement>) { const selectInput = (event: React.MouseEvent<HTMLInputElement>) => {
if (event.target !== document.activeElement) { if (event.target !== document.activeElement) {
event.preventDefault(); event.preventDefault();
(event.target as HTMLInputElement).select(); (event.target as HTMLInputElement).select();
} }
} };
return ( return (
<div className="RoomDialog-modal"> <div className="RoomDialog-modal">
@ -113,9 +113,9 @@ function RoomModal({
)} )}
</div> </div>
); );
} };
export function RoomDialog({ export const RoomDialog = ({
isCollaborating, isCollaborating,
collaboratorCount, collaboratorCount,
username, username,
@ -129,7 +129,7 @@ export function RoomDialog({
onUsernameChange: (username: string) => void; onUsernameChange: (username: string) => void;
onRoomCreate: () => void; onRoomCreate: () => void;
onRoomDestroy: () => void; onRoomDestroy: () => void;
}) { }) => {
const [modalIsShown, setModalIsShown] = useState(false); const [modalIsShown, setModalIsShown] = useState(false);
const [activeRoomLink, setActiveRoomLink] = useState(""); 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); children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
} }
export function Section({ heading, children, ...props }: SectionProps) { export const Section = ({ heading, children, ...props }: SectionProps) => {
const header = ( const header = (
<h2 className="visually-hidden" id={`${heading}-title`}> <h2 className="visually-hidden" id={`${heading}-title`}>
{t(`headings.${heading}`)} {t(`headings.${heading}`)}
@ -24,4 +24,4 @@ export function Section({ heading, children, ...props }: SectionProps) {
)} )}
</section> </section>
); );
} };

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

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

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

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

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

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

@ -12,13 +12,13 @@ import { calculateScrollCenter } from "../scene";
import { randomId } from "../random"; import { randomId } from "../random";
import { DEFAULT_TEXT_ALIGN } from "../appState"; 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 // we're making the elements mutable for this API because we want to
// efficiently remove/tweak properties on them (to migrate old scenes) // efficiently remove/tweak properties on them (to migrate old scenes)
savedElements: readonly Mutable<ExcalidrawElement>[], savedElements: readonly Mutable<ExcalidrawElement>[],
savedState: AppState | null, savedState: AppState | null,
opts?: { scrollToContent: boolean }, opts?: { scrollToContent: boolean },
): DataState { ): DataState => {
const elements = savedElements const elements = savedElements
.filter((el) => { .filter((el) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
@ -94,4 +94,4 @@ export function restore(
elements: elements, elements: elements,
appState: savedState, 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 // 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. // This set of functions retrieves the absolute position of the 4 points.
export function getElementAbsoluteCoords( export const getElementAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
): [number, number, number, number] { ): [number, number, number, number] => {
if (isLinearElement(element)) { if (isLinearElement(element)) {
return getLinearElementAbsoluteCoords(element); return getLinearElementAbsoluteCoords(element);
} }
@ -24,9 +24,9 @@ export function getElementAbsoluteCoords(
element.x + element.width, element.x + element.width,
element.y + element.height, 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 // Here we add +1 to avoid these numbers to be 0
// otherwise rough.js will throw an error complaining about it // otherwise rough.js will throw an error complaining about it
const topX = Math.floor(element.width / 2) + 1; const topX = Math.floor(element.width / 2) + 1;
@ -39,16 +39,16 @@ export function getDiamondPoints(element: ExcalidrawElement) {
const leftY = rightY; const leftY = rightY;
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY]; 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) { for (const set of shape.sets) {
if (set.type === "path") { if (set.type === "path") {
return set.ops; return set.ops;
} }
} }
return shape.sets[0].ops; return shape.sets[0].ops;
} };
const getMinMaxXYFromCurvePathOps = ( const getMinMaxXYFromCurvePathOps = (
ops: Op[], ops: Op[],
@ -150,10 +150,10 @@ const getLinearElementAbsoluteCoords = (
]; ];
}; };
export function getArrowPoints( export const getArrowPoints = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
shape: Drawable[], shape: Drawable[],
) { ) => {
const ops = getCurvePathOps(shape[0]); const ops = getCurvePathOps(shape[0]);
const data = ops[ops.length - 1].data; 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); const [x4, y4] = rotate(xs, ys, x2, y2, (angle * Math.PI) / 180);
return [x2, y2, x3, y3, x4, y4]; return [x2, y2, x3, y3, x4, y4];
} };
const getLinearElementRotatedBounds = ( const getLinearElementRotatedBounds = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,

@ -19,10 +19,10 @@ import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
import { isLinearElement } from "./typeChecks"; import { isLinearElement } from "./typeChecks";
function isElementDraggableFromInside( const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
): boolean { ): boolean => {
const dragFromInside = const dragFromInside =
element.backgroundColor !== "transparent" || element.backgroundColor !== "transparent" ||
appState.selectedElementIds[element.id]; appState.selectedElementIds[element.id];
@ -30,15 +30,15 @@ function isElementDraggableFromInside(
return dragFromInside && isPathALoop(element.points); return dragFromInside && isPathALoop(element.points);
} }
return dragFromInside; return dragFromInside;
} };
export function hitTest( export const hitTest = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
x: number, x: number,
y: number, y: number,
zoom: number, zoom: number,
): boolean { ): boolean => {
// For shapes that are composed of lines, we only enable point-selection when the distance // For shapes that are composed of lines, we only enable point-selection when the distance
// of the click is less than x pixels of any of the lines that the shape is composed of // of the click is less than x pixels of any of the lines that the shape is composed of
const lineThreshold = 10 / zoom; const lineThreshold = 10 / zoom;
@ -210,7 +210,7 @@ export function hitTest(
return false; return false;
} }
throw new Error(`Unimplemented type ${element.type}`); throw new Error(`Unimplemented type ${element.type}`);
} };
const pointInBezierEquation = ( const pointInBezierEquation = (
p0: Point, p0: Point,

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

@ -49,35 +49,30 @@ export {
} from "./sizeHelpers"; } from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions"; export { showSelectedShapeActions } from "./showSelectedShapeActions";
export function getSyncableElements(elements: readonly ExcalidrawElement[]) { export const getSyncableElements = (
// There are places in Excalidraw where synthetic invisibly small elements are added and removed. 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 // 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 :) // 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[]) { export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
return elements.reduce( elements.reduce(
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => { (acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
acc[element.id] = element; acc[element.id] = element;
return acc; return acc;
}, },
{}, {},
); );
}
export function getDrawingVersion(elements: readonly ExcalidrawElement[]) { export const getDrawingVersion = (elements: readonly ExcalidrawElement[]) =>
return elements.reduce((acc, el) => acc + el.version, 0); elements.reduce((acc, el) => acc + el.version, 0);
}
export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) { export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
return elements.filter( elements.filter(
(element) => !element.isDeleted, (element) => !element.isDeleted,
) as readonly NonDeletedExcalidrawElement[]; ) as readonly NonDeletedExcalidrawElement[];
}
export function isNonDeletedElement<T extends ExcalidrawElement>( export const isNonDeletedElement = <T extends ExcalidrawElement>(
element: T, element: T,
): element is NonDeleted<T> { ): element is NonDeleted<T> => !element.isDeleted;
return !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 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 // 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(). // 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, element: TElement,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
) { ) => {
// casting to any because can't use `in` operator // casting to any because can't use `in` operator
// (see https://github.com/microsoft/TypeScript/issues/21732) // (see https://github.com/microsoft/TypeScript/issues/21732)
const { points } = updates as any; const { points } = updates as any;
@ -45,16 +45,14 @@ export function mutateElement<TElement extends Mutable<ExcalidrawElement>>(
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
globalSceneState.informMutation(); globalSceneState.informMutation();
} };
export function newElementWith<TElement extends ExcalidrawElement>( export const newElementWith = <TElement extends ExcalidrawElement>(
element: TElement, element: TElement,
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
): TElement { ): TElement => ({
return {
...element, ...element,
version: element.version + 1, version: element.version + 1,
versionNonce: randomInteger(), versionNonce: randomInteger(),
...updates, ...updates,
}; });
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -31,10 +31,10 @@ export interface ExcalidrawElementWithCanvas {
canvasOffsetY: number; canvasOffsetY: number;
} }
function generateElementCanvas( const generateElementCanvas = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
zoom: number, zoom: number,
): ExcalidrawElementWithCanvas { ): ExcalidrawElementWithCanvas => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
@ -75,13 +75,13 @@ function generateElementCanvas(
1 / (window.devicePixelRatio * zoom), 1 / (window.devicePixelRatio * zoom),
); );
return { element, canvas, canvasZoom: zoom, canvasOffsetX, canvasOffsetY }; return { element, canvas, canvasZoom: zoom, canvasOffsetX, canvasOffsetY };
} };
function drawElementOnCanvas( const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
) { ) => {
context.globalAlpha = element.opacity / 100; context.globalAlpha = element.opacity / 100;
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
@ -132,7 +132,7 @@ function drawElementOnCanvas(
} }
} }
context.globalAlpha = 1; context.globalAlpha = 1;
} };
const elementWithCanvasCache = new WeakMap< const elementWithCanvasCache = new WeakMap<
ExcalidrawElement, ExcalidrawElement,
@ -144,15 +144,13 @@ const shapeCache = new WeakMap<
Drawable | Drawable[] | null Drawable | Drawable[] | null
>(); >();
export function getShapeForElement(element: ExcalidrawElement) { export const getShapeForElement = (element: ExcalidrawElement) =>
return shapeCache.get(element); shapeCache.get(element);
}
export function invalidateShapeForElement(element: ExcalidrawElement) { export const invalidateShapeForElement = (element: ExcalidrawElement) =>
shapeCache.delete(element); shapeCache.delete(element);
}
export function generateRoughOptions(element: ExcalidrawElement): Options { export const generateRoughOptions = (element: ExcalidrawElement): Options => {
const options: Options = { const options: Options = {
seed: element.seed, seed: element.seed,
strokeLineDash: strokeLineDash:
@ -214,13 +212,13 @@ export function generateRoughOptions(element: ExcalidrawElement): Options {
throw new Error(`Unimplemented type ${element.type}`); throw new Error(`Unimplemented type ${element.type}`);
} }
} }
} };
function generateElement( const generateElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
generator: RoughGenerator, generator: RoughGenerator,
sceneState?: SceneState, sceneState?: SceneState,
) { ) => {
let shape = shapeCache.get(element) || null; let shape = shapeCache.get(element) || null;
if (!shape) { if (!shape) {
elementWithCanvasCache.delete(element); elementWithCanvasCache.delete(element);
@ -319,14 +317,14 @@ function generateElement(
return elementWithCanvas; return elementWithCanvas;
} }
return prevElementWithCanvas; return prevElementWithCanvas;
} };
function drawElementFromCanvas( const drawElementFromCanvas = (
elementWithCanvas: ExcalidrawElementWithCanvas, elementWithCanvas: ExcalidrawElementWithCanvas,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
sceneState: SceneState, sceneState: SceneState,
) { ) => {
const element = elementWithCanvas.element; const element = elementWithCanvas.element;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio; const cx = ((x1 + x2) / 2 + sceneState.scrollX) * window.devicePixelRatio;
@ -346,15 +344,15 @@ function drawElementFromCanvas(
context.rotate(-element.angle); context.rotate(-element.angle);
context.translate(-cx, -cy); context.translate(-cx, -cy);
context.scale(window.devicePixelRatio, window.devicePixelRatio); context.scale(window.devicePixelRatio, window.devicePixelRatio);
} };
export function renderElement( export const renderElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
rc: RoughCanvas, rc: RoughCanvas,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
renderOptimizations: boolean, renderOptimizations: boolean,
sceneState: SceneState, sceneState: SceneState,
) { ) => {
const generator = rc.generator; const generator = rc.generator;
switch (element.type) { switch (element.type) {
case "selection": { case "selection": {
@ -404,15 +402,15 @@ export function renderElement(
throw new Error(`Unimplemented type ${element.type}`); throw new Error(`Unimplemented type ${element.type}`);
} }
} }
} };
export function renderElementToSvg( export const renderElementToSvg = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
rsvg: RoughSVG, rsvg: RoughSVG,
svgRoot: SVGElement, svgRoot: SVGElement,
offsetX?: number, offsetX?: number,
offsetY?: number, offsetY?: number,
) { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x2 - x1) / 2 - (element.x - x1); const cx = (x2 - x1) / 2 - (element.x - x1);
const cy = (y2 - y1) / 2 - (element.y - y1); 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>; type HandlerRectanglesRet = keyof ReturnType<typeof handlerRectangles>;
function colorsForClientId(clientId: string) { const colorsForClientId = (clientId: string) => {
// Naive way of getting an integer out of the clientId // Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0); 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], background: backgrounds[sum % backgrounds.length],
stroke: strokes[sum % strokes.length], stroke: strokes[sum % strokes.length],
}; };
} };
function strokeRectWithRotation( const strokeRectWithRotation = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
x: number, x: number,
y: number, y: number,
@ -53,7 +53,7 @@ function strokeRectWithRotation(
cy: number, cy: number,
angle: number, angle: number,
fill?: boolean, fill?: boolean,
) { ) => {
context.translate(cx, cy); context.translate(cx, cy);
context.rotate(angle); context.rotate(angle);
if (fill) { if (fill) {
@ -62,22 +62,22 @@ function strokeRectWithRotation(
context.strokeRect(x - cx, y - cy, width, height); context.strokeRect(x - cx, y - cy, width, height);
context.rotate(-angle); context.rotate(-angle);
context.translate(-cx, -cy); context.translate(-cx, -cy);
} };
function strokeCircle( const strokeCircle = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number, height: number,
) { ) => {
context.beginPath(); context.beginPath();
context.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2); context.arc(x + width / 2, y + height / 2, width / 2, 0, Math.PI * 2);
context.fill(); context.fill();
context.stroke(); context.stroke();
} };
export function renderScene( export const renderScene = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
selectionElement: NonDeletedExcalidrawElement | null, selectionElement: NonDeletedExcalidrawElement | null,
@ -98,7 +98,7 @@ export function renderScene(
renderSelection?: boolean; renderSelection?: boolean;
renderOptimizations?: boolean; renderOptimizations?: boolean;
} = {}, } = {},
) { ) => {
if (!canvas) { if (!canvas) {
return { atLeastOneVisibleElement: false }; return { atLeastOneVisibleElement: false };
} }
@ -461,9 +461,9 @@ export function renderScene(
context.scale(1 / scale, 1 / scale); context.scale(1 / scale, 1 / scale);
return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars };
} };
function isVisibleElement( const isVisibleElement = (
element: ExcalidrawElement, element: ExcalidrawElement,
viewportWidth: number, viewportWidth: number,
viewportHeight: number, viewportHeight: number,
@ -476,7 +476,7 @@ function isVisibleElement(
scrollY: FlooredNumber; scrollY: FlooredNumber;
zoom: number; zoom: number;
}, },
) { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
// Apply zoom // Apply zoom
@ -492,10 +492,10 @@ function isVisibleElement(
y2 + scrollY - viewportHeightDiff / 2 >= 0 && y2 + scrollY - viewportHeightDiff / 2 >= 0 &&
y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom y1 + scrollY - viewportHeightDiff / 2 <= viewportHeightWithZoom
); );
} };
// This should be only called for exporting purposes // This should be only called for exporting purposes
export function renderSceneToSvg( export const renderSceneToSvg = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
rsvg: RoughSVG, rsvg: RoughSVG,
svgRoot: SVGElement, svgRoot: SVGElement,
@ -506,7 +506,7 @@ export function renderSceneToSvg(
offsetX?: number; offsetX?: number;
offsetY?: number; offsetY?: number;
} = {}, } = {},
) { ) => {
if (!svgRoot) { if (!svgRoot) {
return; return;
} }
@ -522,4 +522,4 @@ export function renderSceneToSvg(
); );
} }
}); });
} };

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

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

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

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

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

@ -6,10 +6,10 @@ import { getElementAbsoluteCoords, getElementBounds } from "../element";
import { AppState } from "../types"; import { AppState } from "../types";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
export function getElementsWithinSelection( export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement, selection: NonDeletedExcalidrawElement,
) { ) => {
const [ const [
selectionX1, selectionX1,
selectionY1, selectionY1,
@ -29,12 +29,12 @@ export function getElementsWithinSelection(
selectionY2 >= elementY2 selectionY2 >= elementY2
); );
}); });
} };
export function deleteSelectedElements( export const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) { ) => {
return { return {
elements: elements.map((el) => { elements: elements.map((el) => {
if (appState.selectedElementIds[el.id]) { if (appState.selectedElementIds[el.id]) {
@ -47,24 +47,24 @@ export function deleteSelectedElements(
selectedElementIds: {}, selectedElementIds: {},
}, },
}; };
} };
export function isSomeElementSelected( export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
): boolean { ): boolean => {
return elements.some((element) => appState.selectedElementIds[element.id]); return elements.some((element) => appState.selectedElementIds[element.id]);
} };
/** /**
* Returns common attribute (picked by `getAttribute` callback) of selected * Returns common attribute (picked by `getAttribute` callback) of selected
* elements. If elements don't share the same value, returns `null`. * elements. If elements don't share the same value, returns `null`.
*/ */
export function getCommonAttributeOfSelectedElements<T>( export const getCommonAttributeOfSelectedElements = <T>(
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
getAttribute: (element: ExcalidrawElement) => T, getAttribute: (element: ExcalidrawElement) => T,
): T | null { ): T | null => {
const attributes = Array.from( const attributes = Array.from(
new Set( new Set(
getSelectedElements(elements, appState).map((element) => getSelectedElements(elements, appState).map((element) =>
@ -73,20 +73,20 @@ export function getCommonAttributeOfSelectedElements<T>(
), ),
); );
return attributes.length === 1 ? attributes[0] : null; return attributes.length === 1 ? attributes[0] : null;
} };
export function getSelectedElements( export const getSelectedElements = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
) { ) => {
return elements.filter((element) => appState.selectedElementIds[element.id]); return elements.filter((element) => appState.selectedElementIds[element.id]);
} };
export function getTargetElement( export const getTargetElement = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
) { ) => {
return appState.editingElement return appState.editingElement
? [appState.editingElement] ? [appState.editingElement]
: getSelectedElements(elements, appState); : 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) { if (canvas === null) {
return { x: 0, y: 0 }; return { x: 0, y: 0 };
} }
@ -14,10 +17,10 @@ export function getZoomOrigin(canvas: HTMLCanvasElement | null, scale: number) {
x: normalizedCanvasWidth / 2, x: normalizedCanvasWidth / 2,
y: normalizedCanvasHeight / 2, y: normalizedCanvasHeight / 2,
}; };
} };
export function getNormalizedZoom(zoom: number): number { export const getNormalizedZoom = (zoom: number): number => {
const normalizedZoom = parseFloat(zoom.toFixed(2)); const normalizedZoom = parseFloat(zoom.toFixed(2));
const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2)); const clampedZoom = Math.max(0.1, Math.min(normalizedZoom, 2));
return clampedZoom; return clampedZoom;
} };

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

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

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

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

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

@ -1,14 +1,14 @@
import { moveOneLeft, moveOneRight, moveAllLeft, moveAllRight } from "./zindex"; import { moveOneLeft, moveOneRight, moveAllLeft, moveAllRight } from "./zindex";
function expectMove<T>( const expectMove = <T>(
fn: (elements: T[], indicesToMove: number[]) => void, fn: (elements: T[], indicesToMove: number[]) => void,
elems: T[], elems: T[],
indices: number[], indices: number[],
equal: T[], equal: T[],
) { ) => {
fn(elems, indices); fn(elems, indices);
expect(elems).toEqual(equal); expect(elems).toEqual(equal);
} };
it("should moveOneLeft", () => { it("should moveOneLeft", () => {
expectMove(moveOneLeft, ["a", "b", "c", "d"], [1, 2], ["b", "c", "a", "d"]); 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]; const element = elements[indexA];
elements[indexA] = elements[indexB]; elements[indexA] = elements[indexB];
elements[indexB] = element; 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); indicesToMove.sort((a: number, b: number) => a - b);
let isSorted = true; let isSorted = true;
// We go from left to right to avoid overriding the wrong elements // 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; return elements;
} };
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) { export const moveOneRight = <T>(elements: T[], indicesToMove: number[]) => {
const reversedIndicesToMove = indicesToMove.sort( const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a, (a: number, b: number) => b - a,
); );
@ -38,7 +38,7 @@ export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
swap(elements, index + 1, index); swap(elements, index + 1, index);
}); });
return elements; return elements;
} };
// Let's go through an example // 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] // [c, f, a, b, d, e, g]
// //
// And we are done! // 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); indicesToMove.sort((a: number, b: number) => a - b);
// Copy the elements to move // Copy the elements to move
@ -117,7 +117,7 @@ export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
}); });
return elements; return elements;
} };
// Let's go through an example // 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] // [a, b, d, e, g, c, f]
// //
// And we are done! // And we are done!
export function moveAllRight<T>(elements: T[], indicesToMove: number[]) { export const moveAllRight = <T>(elements: T[], indicesToMove: number[]) => {
const reversedIndicesToMove = indicesToMove.sort( const reversedIndicesToMove = indicesToMove.sort(
(a: number, b: number) => b - a, (a: number, b: number) => b - a,
); );
@ -199,4 +199,4 @@ export function moveAllRight<T>(elements: T[], indicesToMove: number[]) {
}); });
return elements; return elements;
} };

Loading…
Cancel
Save