mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-02-18 13:29:36 +01:00
Prefer arrow functions and callbacks (#1210)
This commit is contained in:
parent
33fe223b5d
commit
c427aa3cce
@ -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,65 +83,61 @@ 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}`);
|
const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
|
||||||
const shortcut = `${capitalizeString(key)} ${t("shortcutsDialog.or")} ${
|
index + 1
|
||||||
index + 1
|
}`;
|
||||||
}`;
|
return (
|
||||||
return (
|
<ToolButton
|
||||||
<ToolButton
|
key={value}
|
||||||
key={value}
|
type="radio"
|
||||||
type="radio"
|
icon={icon}
|
||||||
icon={icon}
|
checked={elementType === value}
|
||||||
checked={elementType === value}
|
name="editor-current-shape"
|
||||||
name="editor-current-shape"
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
keyBindingLabel={`${index + 1}`}
|
||||||
keyBindingLabel={`${index + 1}`}
|
aria-label={capitalizeString(label)}
|
||||||
aria-label={capitalizeString(label)}
|
aria-keyshortcuts={`${key} ${index + 1}`}
|
||||||
aria-keyshortcuts={`${key} ${index + 1}`}
|
data-testid={value}
|
||||||
data-testid={value}
|
onChange={() => {
|
||||||
onChange={() => {
|
setAppState({
|
||||||
setAppState({
|
elementType: value,
|
||||||
elementType: value,
|
multiElement: null,
|
||||||
multiElement: null,
|
selectedElementIds: {},
|
||||||
selectedElementIds: {},
|
});
|
||||||
});
|
setCursorForShape(value);
|
||||||
setCursorForShape(value);
|
setAppState({});
|
||||||
setAppState({});
|
}}
|
||||||
}}
|
></ToolButton>
|
||||||
></ToolButton>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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")}
|
{renderAction("zoomOut")}
|
||||||
{renderAction("zoomOut")}
|
{renderAction("resetZoom")}
|
||||||
{renderAction("resetZoom")}
|
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
|
||||||
<div style={{ marginInlineStart: 4 }}>{(zoom * 100).toFixed(0)}%</div>
|
</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,23 +10,21 @@ 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
|
key={option.text}
|
||||||
key={option.text}
|
className={value === option.value ? "active" : ""}
|
||||||
className={value === option.value ? "active" : ""}
|
>
|
||||||
>
|
<input
|
||||||
<input
|
type="radio"
|
||||||
type="radio"
|
name={group}
|
||||||
name={group}
|
onChange={() => onChange(option.value)}
|
||||||
onChange={() => onChange(option.value)}
|
checked={value === option.value ? true : false}
|
||||||
checked={value === option.value ? true : false}
|
/>
|
||||||
/>
|
{option.text}
|
||||||
{option.text}
|
</label>
|
||||||
</label>
|
))}
|
||||||
))}
|
</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,45 +16,41 @@ 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}
|
left={left}
|
||||||
left={left}
|
fitInViewport={true}
|
||||||
fitInViewport={true}
|
>
|
||||||
|
<ul
|
||||||
|
className="context-menu"
|
||||||
|
onContextMenu={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<ul
|
{options.map((option, idx) => (
|
||||||
className="context-menu"
|
<li key={idx} onClick={onCloseRequest}>
|
||||||
onContextMenu={(event) => event.preventDefault()}
|
<ContextMenuOption {...option} />
|
||||||
>
|
</li>
|
||||||
{options.map((option, idx) => (
|
))}
|
||||||
<li key={idx} onClick={onCloseRequest}>
|
</ul>
|
||||||
<ContextMenuOption {...option} />
|
</Popover>
|
||||||
</li>
|
);
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</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,23 +11,21 @@ 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${
|
floating ? " dropdown-select--floating" : ""
|
||||||
floating ? " dropdown-select--floating" : ""
|
}`}
|
||||||
}`}
|
onChange={({ target }) => onChange(target.value)}
|
||||||
onChange={({ target }) => onChange(target.value)}
|
value={currentLanguage}
|
||||||
value={currentLanguage}
|
aria-label={i18n.t("buttons.selectLanguage")}
|
||||||
aria-label={i18n.t("buttons.selectLanguage")}
|
>
|
||||||
>
|
{languages.map((language) => (
|
||||||
{languages.map((language) => (
|
<option key={language.lng} value={language.lng}>
|
||||||
<option key={language.lng} value={language.lng}>
|
{language.label}
|
||||||
{language.label}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</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,108 +39,106 @@ export function MobileMenu({
|
|||||||
onUsernameChange,
|
onUsernameChange,
|
||||||
onRoomDestroy,
|
onRoomDestroy,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
}: MobileMenuProps) {
|
}: MobileMenuProps) => (
|
||||||
return (
|
<>
|
||||||
<>
|
{appState.isLoading && <LoadingMessage />}
|
||||||
{appState.isLoading && <LoadingMessage />}
|
<FixedSideContainer side="top">
|
||||||
<FixedSideContainer side="top">
|
<Section heading="shapes">
|
||||||
<Section heading="shapes">
|
{(heading) => (
|
||||||
{(heading) => (
|
<Stack.Col gap={4} align="center">
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Row gap={1}>
|
||||||
<Stack.Row gap={1}>
|
<Island padding={1}>
|
||||||
<Island padding={1}>
|
{heading}
|
||||||
{heading}
|
<Stack.Row gap={1}>
|
||||||
<Stack.Row gap={1}>
|
<ShapesSwitcher
|
||||||
<ShapesSwitcher
|
elementType={appState.elementType}
|
||||||
elementType={appState.elementType}
|
setAppState={setAppState}
|
||||||
setAppState={setAppState}
|
|
||||||
/>
|
|
||||||
</Stack.Row>
|
|
||||||
</Island>
|
|
||||||
<LockIcon
|
|
||||||
checked={appState.elementLocked}
|
|
||||||
onChange={onLockToggle}
|
|
||||||
title={t("toolBar.lock")}
|
|
||||||
/>
|
|
||||||
</Stack.Row>
|
|
||||||
</Stack.Col>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
<HintViewer appState={appState} elements={elements} />
|
|
||||||
</FixedSideContainer>
|
|
||||||
<div
|
|
||||||
className="App-bottom-bar"
|
|
||||||
style={{
|
|
||||||
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Island padding={3}>
|
|
||||||
{appState.openMenu === "canvas" ? (
|
|
||||||
<Section className="App-mobile-menu" heading="canvasActions">
|
|
||||||
<div className="panelColumn">
|
|
||||||
<Stack.Col gap={4}>
|
|
||||||
{actionManager.renderAction("loadScene")}
|
|
||||||
{actionManager.renderAction("saveScene")}
|
|
||||||
{exportButton}
|
|
||||||
{actionManager.renderAction("clearCanvas")}
|
|
||||||
<RoomDialog
|
|
||||||
isCollaborating={appState.isCollaborating}
|
|
||||||
collaboratorCount={appState.collaborators.size}
|
|
||||||
username={appState.username}
|
|
||||||
onUsernameChange={onUsernameChange}
|
|
||||||
onRoomCreate={onRoomCreate}
|
|
||||||
onRoomDestroy={onRoomDestroy}
|
|
||||||
/>
|
/>
|
||||||
{actionManager.renderAction("changeViewBackgroundColor")}
|
</Stack.Row>
|
||||||
<fieldset>
|
</Island>
|
||||||
<legend>{t("labels.language")}</legend>
|
<LockIcon
|
||||||
<LanguageList
|
checked={appState.elementLocked}
|
||||||
onChange={(lng) => {
|
onChange={onLockToggle}
|
||||||
setLanguage(lng);
|
title={t("toolBar.lock")}
|
||||||
setAppState({});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</Stack.Col>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
) : appState.openMenu === "shape" &&
|
|
||||||
showSelectedShapeActions(appState, elements) ? (
|
|
||||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
|
||||||
<SelectedShapeActions
|
|
||||||
appState={appState}
|
|
||||||
elements={elements}
|
|
||||||
renderAction={actionManager.renderAction}
|
|
||||||
elementType={appState.elementType}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Stack.Row>
|
||||||
) : null}
|
</Stack.Col>
|
||||||
<footer className="App-toolbar">
|
)}
|
||||||
<div className="App-toolbar-content">
|
</Section>
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
<HintViewer appState={appState} elements={elements} />
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
</FixedSideContainer>
|
||||||
{actionManager.renderAction("undo")}
|
<div
|
||||||
{actionManager.renderAction("redo")}
|
className="App-bottom-bar"
|
||||||
{actionManager.renderAction(
|
style={{
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
)}
|
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Island padding={3}>
|
||||||
|
{appState.openMenu === "canvas" ? (
|
||||||
|
<Section className="App-mobile-menu" heading="canvasActions">
|
||||||
|
<div className="panelColumn">
|
||||||
|
<Stack.Col gap={4}>
|
||||||
|
{actionManager.renderAction("loadScene")}
|
||||||
|
{actionManager.renderAction("saveScene")}
|
||||||
|
{exportButton}
|
||||||
|
{actionManager.renderAction("clearCanvas")}
|
||||||
|
<RoomDialog
|
||||||
|
isCollaborating={appState.isCollaborating}
|
||||||
|
collaboratorCount={appState.collaborators.size}
|
||||||
|
username={appState.username}
|
||||||
|
onUsernameChange={onUsernameChange}
|
||||||
|
onRoomCreate={onRoomCreate}
|
||||||
|
onRoomDestroy={onRoomDestroy}
|
||||||
|
/>
|
||||||
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.language")}</legend>
|
||||||
|
<LanguageList
|
||||||
|
onChange={(lng) => {
|
||||||
|
setLanguage(lng);
|
||||||
|
setAppState({});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
{appState.scrolledOutside && (
|
</Section>
|
||||||
<button
|
) : appState.openMenu === "shape" &&
|
||||||
className="scroll-back-to-content"
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
onClick={() => {
|
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||||
setAppState({ ...calculateScrollCenter(elements) });
|
<SelectedShapeActions
|
||||||
}}
|
appState={appState}
|
||||||
>
|
elements={elements}
|
||||||
{t("buttons.scrollBackToContent")}
|
renderAction={actionManager.renderAction}
|
||||||
</button>
|
elementType={appState.elementType}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
) : null}
|
||||||
|
<footer className="App-toolbar">
|
||||||
|
<div className="App-toolbar-content">
|
||||||
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
|
{actionManager.renderAction("undo")}
|
||||||
|
{actionManager.renderAction("redo")}
|
||||||
|
{actionManager.renderAction(
|
||||||
|
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||||
)}
|
)}
|
||||||
</footer>
|
{actionManager.renderAction("deleteSelectedElements")}
|
||||||
</Island>
|
</div>
|
||||||
</div>
|
{appState.scrolledOutside && (
|
||||||
</>
|
<button
|
||||||
);
|
className="scroll-back-to-content"
|
||||||
}
|
onClick={() => {
|
||||||
|
setAppState({ ...calculateScrollCenter(elements) });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("buttons.scrollBackToContent")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</Island>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
@ -4,13 +4,13 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { 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,44 +42,41 @@ 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,
|
y,
|
||||||
y,
|
width,
|
||||||
width,
|
height,
|
||||||
height,
|
angle,
|
||||||
angle,
|
strokeColor,
|
||||||
strokeColor,
|
backgroundColor,
|
||||||
backgroundColor,
|
fillStyle,
|
||||||
fillStyle,
|
strokeWidth,
|
||||||
strokeWidth,
|
strokeStyle,
|
||||||
strokeStyle,
|
roughness,
|
||||||
roughness,
|
opacity,
|
||||||
opacity,
|
seed: rest.seed ?? randomInteger(),
|
||||||
seed: rest.seed ?? randomInteger(),
|
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);
|
||||||
|
20
src/i18n.ts
20
src/i18n.ts
@ -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);
|
||||||
|
14
src/keys.ts
14
src/keys.ts
@ -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;
|
||||||
|
39
src/math.ts
39
src/math.ts
@ -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", () => {
|
||||||
|
99
src/utils.ts
99
src/utils.ts
@ -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…
Reference in New Issue
Block a user