2022-03-15 20:56:39 +05:30
|
|
|
import oc from "open-color";
|
2023-05-18 16:06:27 +02:00
|
|
|
import { COLOR_PALETTE } from "./colors";
|
2020-07-19 21:14:45 +02:00
|
|
|
import {
|
|
|
|
CURSOR_TYPE,
|
2021-01-10 20:48:12 +02:00
|
|
|
DEFAULT_VERSION,
|
2022-02-08 11:25:35 +01:00
|
|
|
EVENT,
|
2020-07-19 21:14:45 +02:00
|
|
|
FONT_FAMILY,
|
2023-01-22 12:33:15 +01:00
|
|
|
isDarwin,
|
2022-03-15 20:56:39 +05:30
|
|
|
MIME_TYPES,
|
|
|
|
THEME,
|
2020-07-19 21:14:45 +02:00
|
|
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
|
|
|
} from "./constants";
|
2023-06-15 00:42:01 +08:00
|
|
|
import {
|
|
|
|
FontFamilyValues,
|
|
|
|
FontString,
|
|
|
|
NonDeletedExcalidrawElement,
|
|
|
|
} from "./element/types";
|
2023-01-23 16:12:28 +01:00
|
|
|
import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
|
2020-12-05 20:00:53 +05:30
|
|
|
import { unstable_batchedUpdates } from "react-dom";
|
2022-05-06 18:21:22 +05:30
|
|
|
import { SHAPES } from "./shapes";
|
2023-01-23 16:12:28 +01:00
|
|
|
import { isEraserActive, isHandToolActive } from "./appState";
|
2023-03-04 19:21:57 +01:00
|
|
|
import { ResolutionType } from "./utility-types";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-03-23 16:38:41 -07:00
|
|
|
let mockDateTime: string | null = null;
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const setDateTimeForTests = (dateTime: string) => {
|
2020-03-23 16:38:41 -07:00
|
|
|
mockDateTime = dateTime;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-23 16:38:41 -07:00
|
|
|
|
2020-04-08 01:31:28 +03:00
|
|
|
export const getDateTime = () => {
|
2020-03-23 16:38:41 -07:00
|
|
|
if (mockDateTime) {
|
|
|
|
return mockDateTime;
|
|
|
|
}
|
|
|
|
|
2020-01-06 20:24:54 +04:00
|
|
|
const date = new Date();
|
|
|
|
const year = date.getFullYear();
|
2020-04-08 01:31:28 +03:00
|
|
|
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
|
|
const day = `${date.getDate()}`.padStart(2, "0");
|
|
|
|
const hr = `${date.getHours()}`.padStart(2, "0");
|
|
|
|
const min = `${date.getMinutes()}`.padStart(2, "0");
|
2020-01-06 20:24:54 +04:00
|
|
|
|
2020-04-08 01:31:28 +03:00
|
|
|
return `${year}-${month}-${day}-${hr}${min}`;
|
|
|
|
};
|
2020-01-06 20:24:54 +04:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const capitalizeString = (str: string) =>
|
|
|
|
str.charAt(0).toUpperCase() + str.slice(1);
|
2020-01-06 20:24:54 +04:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const isToolIcon = (
|
2020-01-27 22:14:35 +01:00
|
|
|
target: Element | EventTarget | null,
|
2020-05-20 16:21:37 +03:00
|
|
|
): target is HTMLElement =>
|
|
|
|
target instanceof HTMLElement && target.className.includes("ToolIcon");
|
2020-01-27 22:14:35 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const isInputLike = (
|
2020-01-24 12:04:54 +02:00
|
|
|
target: Element | EventTarget | null,
|
2020-01-27 22:14:35 +01:00
|
|
|
): target is
|
|
|
|
| HTMLInputElement
|
|
|
|
| HTMLTextAreaElement
|
|
|
|
| HTMLSelectElement
|
2020-02-04 11:50:18 +01:00
|
|
|
| HTMLBRElement
|
2020-05-20 16:21:37 +03:00
|
|
|
| HTMLDivElement =>
|
|
|
|
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
|
|
|
|
target instanceof HTMLBRElement || // newline in wysiwyg
|
|
|
|
target instanceof HTMLInputElement ||
|
|
|
|
target instanceof HTMLTextAreaElement ||
|
|
|
|
target instanceof HTMLSelectElement;
|
|
|
|
|
2023-06-06 22:04:06 +02:00
|
|
|
export const isInteractive = (target: Element | EventTarget | null) => {
|
|
|
|
return (
|
|
|
|
isInputLike(target) ||
|
|
|
|
(target instanceof Element && !!target.closest("label, button"))
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const isWritableElement = (
|
2020-02-04 11:50:18 +01:00
|
|
|
target: Element | EventTarget | null,
|
|
|
|
): target is
|
|
|
|
| HTMLInputElement
|
|
|
|
| HTMLTextAreaElement
|
|
|
|
| HTMLBRElement
|
2020-05-20 16:21:37 +03:00
|
|
|
| HTMLDivElement =>
|
|
|
|
(target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
|
|
|
|
target instanceof HTMLBRElement || // newline in wysiwyg
|
|
|
|
target instanceof HTMLTextAreaElement ||
|
|
|
|
(target instanceof HTMLInputElement &&
|
|
|
|
(target.type === "text" || target.type === "number"));
|
2020-02-04 11:50:18 +01:00
|
|
|
|
2020-05-27 15:14:50 +02:00
|
|
|
export const getFontFamilyString = ({
|
|
|
|
fontFamily,
|
|
|
|
}: {
|
2021-06-13 21:26:55 +05:30
|
|
|
fontFamily: FontFamilyValues;
|
2020-05-27 15:14:50 +02:00
|
|
|
}) => {
|
2021-06-13 21:26:55 +05:30
|
|
|
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
|
|
|
|
if (id === fontFamily) {
|
|
|
|
return `${fontFamilyString}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return WINDOWS_EMOJI_FALLBACK_FONT;
|
2020-05-27 15:14:50 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/** returns fontSize+fontFamily string for assignment to DOM elements */
|
|
|
|
export const getFontString = ({
|
|
|
|
fontSize,
|
|
|
|
fontFamily,
|
|
|
|
}: {
|
|
|
|
fontSize: number;
|
2021-06-13 21:26:55 +05:30
|
|
|
fontFamily: FontFamilyValues;
|
2020-05-27 15:14:50 +02:00
|
|
|
}) => {
|
|
|
|
return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
|
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const debounce = <T extends any[]>(
|
2020-01-11 20:15:41 -08:00
|
|
|
fn: (...args: T) => void,
|
2020-01-24 12:04:54 +02:00
|
|
|
timeout: number,
|
2020-05-20 16:21:37 +03:00
|
|
|
) => {
|
2020-01-11 20:15:41 -08:00
|
|
|
let handle = 0;
|
2021-03-26 17:12:32 +01:00
|
|
|
let lastArgs: T | null = null;
|
2020-01-20 18:37:42 +01:00
|
|
|
const ret = (...args: T) => {
|
|
|
|
lastArgs = args;
|
2020-01-11 20:15:41 -08:00
|
|
|
clearTimeout(handle);
|
2021-03-26 17:12:32 +01:00
|
|
|
handle = window.setTimeout(() => {
|
|
|
|
lastArgs = null;
|
|
|
|
fn(...args);
|
|
|
|
}, timeout);
|
2020-01-11 20:15:41 -08:00
|
|
|
};
|
2020-01-20 18:37:42 +01:00
|
|
|
ret.flush = () => {
|
|
|
|
clearTimeout(handle);
|
2021-03-26 17:12:32 +01:00
|
|
|
if (lastArgs) {
|
|
|
|
const _lastArgs = lastArgs;
|
|
|
|
lastArgs = null;
|
|
|
|
fn(..._lastArgs);
|
|
|
|
}
|
2020-01-20 18:37:42 +01:00
|
|
|
};
|
2020-12-07 18:35:16 +02:00
|
|
|
ret.cancel = () => {
|
2021-03-26 17:12:32 +01:00
|
|
|
lastArgs = null;
|
2020-12-07 18:35:16 +02:00
|
|
|
clearTimeout(handle);
|
|
|
|
};
|
2020-01-20 18:37:42 +01:00
|
|
|
return ret;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-15 20:42:02 +05:00
|
|
|
|
2022-02-06 17:45:37 +01:00
|
|
|
// throttle callback to execute once per animation frame
|
2022-07-07 11:47:37 +02:00
|
|
|
export const throttleRAF = <T extends any[]>(
|
|
|
|
fn: (...args: T) => void,
|
|
|
|
opts?: { trailing?: boolean },
|
|
|
|
) => {
|
|
|
|
let timerId: number | null = null;
|
2022-02-06 17:45:37 +01:00
|
|
|
let lastArgs: T | null = null;
|
2022-07-07 11:47:37 +02:00
|
|
|
let lastArgsTrailing: T | null = null;
|
|
|
|
|
|
|
|
const scheduleFunc = (args: T) => {
|
|
|
|
timerId = window.requestAnimationFrame(() => {
|
|
|
|
timerId = null;
|
|
|
|
fn(...args);
|
|
|
|
lastArgs = null;
|
|
|
|
if (lastArgsTrailing) {
|
|
|
|
lastArgs = lastArgsTrailing;
|
|
|
|
lastArgsTrailing = null;
|
|
|
|
scheduleFunc(lastArgs);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-02-06 17:45:37 +01:00
|
|
|
const ret = (...args: T) => {
|
2023-07-26 15:55:20 +05:30
|
|
|
if (import.meta.env.MODE === "test") {
|
2022-02-06 17:45:37 +01:00
|
|
|
fn(...args);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
lastArgs = args;
|
2022-07-07 11:47:37 +02:00
|
|
|
if (timerId === null) {
|
|
|
|
scheduleFunc(lastArgs);
|
|
|
|
} else if (opts?.trailing) {
|
|
|
|
lastArgsTrailing = args;
|
2022-02-06 17:45:37 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
ret.flush = () => {
|
2022-07-07 11:47:37 +02:00
|
|
|
if (timerId !== null) {
|
|
|
|
cancelAnimationFrame(timerId);
|
|
|
|
timerId = null;
|
2022-02-06 17:45:37 +01:00
|
|
|
}
|
|
|
|
if (lastArgs) {
|
2022-07-07 11:47:37 +02:00
|
|
|
fn(...(lastArgsTrailing || lastArgs));
|
|
|
|
lastArgs = lastArgsTrailing = null;
|
2022-02-06 17:45:37 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
ret.cancel = () => {
|
2022-07-07 11:47:37 +02:00
|
|
|
lastArgs = lastArgsTrailing = null;
|
|
|
|
if (timerId !== null) {
|
|
|
|
cancelAnimationFrame(timerId);
|
|
|
|
timerId = null;
|
2022-02-06 17:45:37 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
2023-03-25 15:26:58 -07:00
|
|
|
/**
|
|
|
|
* Exponential ease-out method
|
|
|
|
*
|
|
|
|
* @param {number} k - The value to be tweened.
|
|
|
|
* @returns {number} The tweened value.
|
|
|
|
*/
|
2023-06-29 12:36:38 +02:00
|
|
|
export const easeOut = (k: number) => {
|
2023-03-25 15:26:58 -07:00
|
|
|
return 1 - Math.pow(1 - k, 4);
|
2023-06-29 12:36:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const easeOutInterpolate = (from: number, to: number, progress: number) => {
|
|
|
|
return (to - from) * easeOut(progress) + from;
|
|
|
|
};
|
2023-03-25 15:26:58 -07:00
|
|
|
|
|
|
|
/**
|
2023-06-29 12:36:38 +02:00
|
|
|
* Animates values from `fromValues` to `toValues` using the requestAnimationFrame API.
|
|
|
|
* Executes the `onStep` callback on each step with the interpolated values.
|
|
|
|
* Returns a function that can be called to cancel the animation.
|
2023-03-25 15:26:58 -07:00
|
|
|
*
|
2023-06-29 12:36:38 +02:00
|
|
|
* @example
|
|
|
|
* // Example usage:
|
|
|
|
* const fromValues = { x: 0, y: 0 };
|
|
|
|
* const toValues = { x: 100, y: 200 };
|
|
|
|
* const onStep = ({x, y}) => {
|
|
|
|
* setState(x, y)
|
|
|
|
* };
|
|
|
|
* const onCancel = () => {
|
|
|
|
* console.log("Animation canceled");
|
|
|
|
* };
|
2023-03-25 15:26:58 -07:00
|
|
|
*
|
2023-06-29 12:36:38 +02:00
|
|
|
* const cancelAnimation = easeToValuesRAF({
|
|
|
|
* fromValues,
|
|
|
|
* toValues,
|
|
|
|
* onStep,
|
|
|
|
* onCancel,
|
|
|
|
* });
|
2023-03-25 15:26:58 -07:00
|
|
|
*
|
2023-06-29 12:36:38 +02:00
|
|
|
* // To cancel the animation:
|
|
|
|
* cancelAnimation();
|
2023-03-25 15:26:58 -07:00
|
|
|
*/
|
2023-06-29 12:36:38 +02:00
|
|
|
export const easeToValuesRAF = <
|
|
|
|
T extends Record<keyof T, number>,
|
|
|
|
K extends keyof T,
|
|
|
|
>({
|
|
|
|
fromValues,
|
|
|
|
toValues,
|
|
|
|
onStep,
|
|
|
|
duration = 250,
|
|
|
|
interpolateValue,
|
|
|
|
onStart,
|
|
|
|
onEnd,
|
|
|
|
onCancel,
|
|
|
|
}: {
|
|
|
|
fromValues: T;
|
|
|
|
toValues: T;
|
|
|
|
/**
|
|
|
|
* Interpolate a single value.
|
|
|
|
* Return undefined to be handled by the default interpolator.
|
|
|
|
*/
|
|
|
|
interpolateValue?: (
|
|
|
|
fromValue: number,
|
|
|
|
toValue: number,
|
|
|
|
/** no easing applied */
|
|
|
|
progress: number,
|
|
|
|
key: K,
|
|
|
|
) => number | undefined;
|
|
|
|
onStep: (values: T) => void;
|
|
|
|
duration?: number;
|
|
|
|
onStart?: () => void;
|
|
|
|
onEnd?: () => void;
|
|
|
|
onCancel?: () => void;
|
|
|
|
}) => {
|
2023-03-25 15:26:58 -07:00
|
|
|
let canceled = false;
|
|
|
|
let frameId = 0;
|
|
|
|
let startTime: number;
|
|
|
|
|
|
|
|
function step(timestamp: number) {
|
|
|
|
if (canceled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (startTime === undefined) {
|
|
|
|
startTime = timestamp;
|
2023-06-29 12:36:38 +02:00
|
|
|
onStart?.();
|
2023-03-25 15:26:58 -07:00
|
|
|
}
|
|
|
|
|
2023-06-29 12:36:38 +02:00
|
|
|
const elapsed = Math.min(timestamp - startTime, duration);
|
|
|
|
const factor = easeOut(elapsed / duration);
|
|
|
|
|
|
|
|
const newValues = {} as T;
|
|
|
|
|
|
|
|
Object.keys(fromValues).forEach((key) => {
|
|
|
|
const _key = key as keyof T;
|
|
|
|
const result = ((toValues[_key] - fromValues[_key]) * factor +
|
|
|
|
fromValues[_key]) as T[keyof T];
|
|
|
|
newValues[_key] = result;
|
|
|
|
});
|
|
|
|
|
|
|
|
onStep(newValues);
|
2023-03-25 15:26:58 -07:00
|
|
|
|
|
|
|
if (elapsed < duration) {
|
2023-06-29 12:36:38 +02:00
|
|
|
const progress = elapsed / duration;
|
|
|
|
|
|
|
|
const newValues = {} as T;
|
|
|
|
|
|
|
|
Object.keys(fromValues).forEach((key) => {
|
|
|
|
const _key = key as K;
|
|
|
|
const startValue = fromValues[_key];
|
|
|
|
const endValue = toValues[_key];
|
|
|
|
|
|
|
|
let result;
|
|
|
|
|
|
|
|
result = interpolateValue
|
|
|
|
? interpolateValue(startValue, endValue, progress, _key)
|
|
|
|
: easeOutInterpolate(startValue, endValue, progress);
|
|
|
|
|
|
|
|
if (result == null) {
|
|
|
|
result = easeOutInterpolate(startValue, endValue, progress);
|
|
|
|
}
|
|
|
|
|
|
|
|
newValues[_key] = result as T[K];
|
|
|
|
});
|
|
|
|
onStep(newValues);
|
|
|
|
|
2023-03-25 15:26:58 -07:00
|
|
|
frameId = window.requestAnimationFrame(step);
|
|
|
|
} else {
|
2023-06-29 12:36:38 +02:00
|
|
|
onStep(toValues);
|
|
|
|
onEnd?.();
|
2023-03-25 15:26:58 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
frameId = window.requestAnimationFrame(step);
|
|
|
|
|
|
|
|
return () => {
|
2023-06-29 12:36:38 +02:00
|
|
|
onCancel?.();
|
2023-03-25 15:26:58 -07:00
|
|
|
canceled = true;
|
|
|
|
window.cancelAnimationFrame(frameId);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2021-11-17 23:53:43 +05:30
|
|
|
// https://github.com/lodash/lodash/blob/es/chunk.js
|
2021-11-26 11:46:13 +01:00
|
|
|
export const chunk = <T extends any>(
|
|
|
|
array: readonly T[],
|
|
|
|
size: number,
|
|
|
|
): T[][] => {
|
2021-11-17 23:53:43 +05:30
|
|
|
if (!array.length || size < 1) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
let index = 0;
|
|
|
|
let resIndex = 0;
|
|
|
|
const result = Array(Math.ceil(array.length / size));
|
|
|
|
while (index < array.length) {
|
|
|
|
result[resIndex++] = array.slice(index, (index += size));
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const selectNode = (node: Element) => {
|
2020-01-15 20:42:02 +05:00
|
|
|
const selection = window.getSelection();
|
|
|
|
if (selection) {
|
|
|
|
const range = document.createRange();
|
|
|
|
range.selectNodeContents(node);
|
|
|
|
selection.removeAllRanges();
|
|
|
|
selection.addRange(range);
|
|
|
|
}
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-15 20:42:02 +05:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const removeSelection = () => {
|
2020-01-15 20:42:02 +05:00
|
|
|
const selection = window.getSelection();
|
|
|
|
if (selection) {
|
|
|
|
selection.removeAllRanges();
|
|
|
|
}
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-24 20:45:52 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const distance = (x: number, y: number) => Math.abs(x - y);
|
2020-02-01 15:49:18 +04:00
|
|
|
|
2022-05-06 18:21:22 +05:30
|
|
|
export const updateActiveTool = (
|
|
|
|
appState: Pick<AppState, "activeTool">,
|
|
|
|
data: (
|
2023-07-24 16:51:53 +02:00
|
|
|
| {
|
|
|
|
type:
|
|
|
|
| typeof SHAPES[number]["value"]
|
|
|
|
| "eraser"
|
|
|
|
| "hand"
|
|
|
|
| "frame"
|
|
|
|
| "embeddable";
|
|
|
|
}
|
2022-05-06 18:21:22 +05:30
|
|
|
| { type: "custom"; customType: string }
|
2023-01-23 16:12:28 +01:00
|
|
|
) & { lastActiveToolBeforeEraser?: LastActiveTool },
|
2022-05-06 18:21:22 +05:30
|
|
|
): AppState["activeTool"] => {
|
|
|
|
if (data.type === "custom") {
|
|
|
|
return {
|
|
|
|
...appState.activeTool,
|
|
|
|
type: "custom",
|
|
|
|
customType: data.customType,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...appState.activeTool,
|
2023-01-23 16:12:28 +01:00
|
|
|
lastActiveTool:
|
2022-05-06 18:21:22 +05:30
|
|
|
data.lastActiveToolBeforeEraser === undefined
|
2023-01-23 16:12:28 +01:00
|
|
|
? appState.activeTool.lastActiveTool
|
2022-05-06 18:21:22 +05:30
|
|
|
: data.lastActiveToolBeforeEraser,
|
|
|
|
type: data.type,
|
|
|
|
customType: null,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
export const resetCursor = (canvas: HTMLCanvasElement | null) => {
|
|
|
|
if (canvas) {
|
|
|
|
canvas.style.cursor = "";
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
|
|
|
|
if (canvas) {
|
|
|
|
canvas.style.cursor = cursor;
|
|
|
|
}
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2022-03-15 20:56:39 +05:30
|
|
|
let eraserCanvasCache: any;
|
|
|
|
let previewDataURL: string;
|
|
|
|
export const setEraserCursor = (
|
|
|
|
canvas: HTMLCanvasElement | null,
|
|
|
|
theme: AppState["theme"],
|
|
|
|
) => {
|
|
|
|
const cursorImageSizePx = 20;
|
|
|
|
|
|
|
|
const drawCanvas = () => {
|
|
|
|
const isDarkTheme = theme === THEME.DARK;
|
|
|
|
eraserCanvasCache = document.createElement("canvas");
|
|
|
|
eraserCanvasCache.theme = theme;
|
|
|
|
eraserCanvasCache.height = cursorImageSizePx;
|
|
|
|
eraserCanvasCache.width = cursorImageSizePx;
|
|
|
|
const context = eraserCanvasCache.getContext("2d")!;
|
|
|
|
context.lineWidth = 1;
|
|
|
|
context.beginPath();
|
|
|
|
context.arc(
|
|
|
|
eraserCanvasCache.width / 2,
|
|
|
|
eraserCanvasCache.height / 2,
|
|
|
|
5,
|
|
|
|
0,
|
|
|
|
2 * Math.PI,
|
|
|
|
);
|
|
|
|
context.fillStyle = isDarkTheme ? oc.black : oc.white;
|
|
|
|
context.fill();
|
|
|
|
context.strokeStyle = isDarkTheme ? oc.white : oc.black;
|
|
|
|
context.stroke();
|
|
|
|
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
|
|
|
|
};
|
|
|
|
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
|
|
|
|
drawCanvas();
|
|
|
|
}
|
|
|
|
|
|
|
|
setCursor(
|
|
|
|
canvas,
|
|
|
|
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
|
|
|
cursorImageSizePx / 2
|
|
|
|
}, auto`,
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2021-03-03 14:04:02 +01:00
|
|
|
export const setCursorForShape = (
|
|
|
|
canvas: HTMLCanvasElement | null,
|
2023-05-08 10:14:02 +02:00
|
|
|
appState: Pick<AppState, "activeTool" | "theme">,
|
2021-03-03 14:04:02 +01:00
|
|
|
) => {
|
|
|
|
if (!canvas) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-25 20:46:01 +05:30
|
|
|
if (appState.activeTool.type === "selection") {
|
2021-03-03 14:04:02 +01:00
|
|
|
resetCursor(canvas);
|
2023-01-23 16:12:28 +01:00
|
|
|
} else if (isHandToolActive(appState)) {
|
|
|
|
canvas.style.cursor = CURSOR_TYPE.GRAB;
|
|
|
|
} else if (isEraserActive(appState)) {
|
2022-03-15 20:56:39 +05:30
|
|
|
setEraserCursor(canvas, appState.theme);
|
2021-10-21 22:05:48 +02:00
|
|
|
// do nothing if image tool is selected which suggests there's
|
|
|
|
// a image-preview set as the cursor
|
2022-05-20 18:43:38 +05:30
|
|
|
// Ignore custom type as well and let host decide
|
|
|
|
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
2021-03-03 14:04:02 +01:00
|
|
|
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
2020-04-06 22:26:54 +02:00
|
|
|
}
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-04-06 22:26:54 +02:00
|
|
|
|
2020-04-06 03:17:13 +05:30
|
|
|
export const isFullScreen = () =>
|
|
|
|
document.fullscreenElement?.nodeName === "HTML";
|
|
|
|
|
|
|
|
export const allowFullScreen = () =>
|
|
|
|
document.documentElement.requestFullscreen();
|
|
|
|
|
|
|
|
export const exitFullScreen = () => document.exitFullscreen();
|
|
|
|
|
2020-04-07 14:39:06 +03:00
|
|
|
export const getShortcutKey = (shortcut: string): string => {
|
2020-12-22 11:00:51 +01:00
|
|
|
shortcut = shortcut
|
|
|
|
.replace(/\bAlt\b/i, "Alt")
|
|
|
|
.replace(/\bShift\b/i, "Shift")
|
2022-11-19 18:28:08 +01:00
|
|
|
.replace(/\b(Enter|Return)\b/i, "Enter");
|
2020-12-22 11:00:51 +01:00
|
|
|
if (isDarwin) {
|
|
|
|
return shortcut
|
2022-12-05 21:03:13 +05:30
|
|
|
.replace(/\bCtrlOrCmd\b/gi, "Cmd")
|
2020-12-22 11:00:51 +01:00
|
|
|
.replace(/\bAlt\b/i, "Option");
|
2020-03-09 15:06:35 +02:00
|
|
|
}
|
2022-12-05 21:03:13 +05:30
|
|
|
return shortcut.replace(/\bCtrlOrCmd\b/gi, "Ctrl");
|
2020-03-09 15:06:35 +02:00
|
|
|
};
|
2020-11-04 17:49:15 +00:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const viewportCoordsToSceneCoords = (
|
2020-03-07 10:20:38 -05:00
|
|
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
2020-11-04 17:49:15 +00:00
|
|
|
{
|
|
|
|
zoom,
|
|
|
|
offsetLeft,
|
|
|
|
offsetTop,
|
|
|
|
scrollX,
|
|
|
|
scrollY,
|
|
|
|
}: {
|
|
|
|
zoom: Zoom;
|
|
|
|
offsetLeft: number;
|
|
|
|
offsetTop: number;
|
|
|
|
scrollX: number;
|
|
|
|
scrollY: number;
|
|
|
|
},
|
2020-05-20 16:21:37 +03:00
|
|
|
) => {
|
2023-01-08 16:22:04 +01:00
|
|
|
const x = (clientX - offsetLeft) / zoom.value - scrollX;
|
|
|
|
const y = (clientY - offsetTop) / zoom.value - scrollY;
|
2022-01-29 21:12:44 +01:00
|
|
|
|
2020-03-07 10:20:38 -05:00
|
|
|
return { x, y };
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const sceneCoordsToViewportCoords = (
|
2020-03-07 10:20:38 -05:00
|
|
|
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
|
2020-11-04 17:49:15 +00:00
|
|
|
{
|
|
|
|
zoom,
|
|
|
|
offsetLeft,
|
|
|
|
offsetTop,
|
|
|
|
scrollX,
|
|
|
|
scrollY,
|
|
|
|
}: {
|
|
|
|
zoom: Zoom;
|
|
|
|
offsetLeft: number;
|
|
|
|
offsetTop: number;
|
|
|
|
scrollX: number;
|
|
|
|
scrollY: number;
|
|
|
|
},
|
2020-05-20 16:21:37 +03:00
|
|
|
) => {
|
2022-01-29 21:12:44 +01:00
|
|
|
const x = (sceneX + scrollX) * zoom.value + offsetLeft;
|
|
|
|
const y = (sceneY + scrollY) * zoom.value + offsetTop;
|
2020-03-07 10:20:38 -05:00
|
|
|
return { x, y };
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-18 11:31:40 -04:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const getGlobalCSSVariable = (name: string) =>
|
|
|
|
getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
|
2020-06-07 10:55:08 +01:00
|
|
|
|
|
|
|
const RS_LTR_CHARS =
|
|
|
|
"A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF" +
|
|
|
|
"\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF";
|
|
|
|
const RS_RTL_CHARS = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
|
|
|
|
const RE_RTL_CHECK = new RegExp(`^[^${RS_LTR_CHARS}]*[${RS_RTL_CHARS}]`);
|
|
|
|
/**
|
|
|
|
* Checks whether first directional character is RTL. Meaning whether it starts
|
|
|
|
* with RTL characters, or indeterminate (numbers etc.) characters followed by
|
|
|
|
* RTL.
|
|
|
|
* See https://github.com/excalidraw/excalidraw/pull/1722#discussion_r436340171
|
|
|
|
*/
|
2020-11-06 22:06:30 +02:00
|
|
|
export const isRTL = (text: string) => RE_RTL_CHECK.test(text);
|
2020-07-09 09:30:38 -07:00
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
export const tupleToCoors = (
|
2020-08-08 21:04:15 -07:00
|
|
|
xyTuple: readonly [number, number],
|
2020-11-06 22:06:39 +02:00
|
|
|
): { x: number; y: number } => {
|
2020-07-09 09:30:38 -07:00
|
|
|
const [x, y] = xyTuple;
|
|
|
|
return { x, y };
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-07-27 15:29:19 +03:00
|
|
|
|
|
|
|
/** use as a rejectionHandler to mute filesystem Abort errors */
|
|
|
|
export const muteFSAbortError = (error?: Error) => {
|
|
|
|
if (error?.name === "AbortError") {
|
2021-11-19 10:54:23 +01:00
|
|
|
console.warn(error);
|
2020-07-27 15:29:19 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw error;
|
|
|
|
};
|
2020-09-11 17:06:07 +02:00
|
|
|
|
|
|
|
export const findIndex = <T>(
|
|
|
|
array: readonly T[],
|
|
|
|
cb: (element: T, index: number, array: readonly T[]) => boolean,
|
|
|
|
fromIndex: number = 0,
|
|
|
|
) => {
|
|
|
|
if (fromIndex < 0) {
|
|
|
|
fromIndex = array.length + fromIndex;
|
|
|
|
}
|
|
|
|
fromIndex = Math.min(array.length, Math.max(fromIndex, 0));
|
2020-11-06 22:06:30 +02:00
|
|
|
let index = fromIndex - 1;
|
|
|
|
while (++index < array.length) {
|
|
|
|
if (cb(array[index], index, array)) {
|
|
|
|
return index;
|
2020-09-11 17:06:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const findLastIndex = <T>(
|
|
|
|
array: readonly T[],
|
|
|
|
cb: (element: T, index: number, array: readonly T[]) => boolean,
|
|
|
|
fromIndex: number = array.length - 1,
|
|
|
|
) => {
|
|
|
|
if (fromIndex < 0) {
|
|
|
|
fromIndex = array.length + fromIndex;
|
|
|
|
}
|
|
|
|
fromIndex = Math.min(array.length - 1, Math.max(fromIndex, 0));
|
2020-11-06 22:06:30 +02:00
|
|
|
let index = fromIndex + 1;
|
|
|
|
while (--index > -1) {
|
|
|
|
if (cb(array[index], index, array)) {
|
|
|
|
return index;
|
2020-09-11 17:06:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
};
|
2020-11-27 02:13:38 +05:30
|
|
|
|
|
|
|
export const isTransparent = (color: string) => {
|
|
|
|
const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
|
|
|
|
const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
|
|
|
|
return (
|
|
|
|
isRGBTransparent ||
|
|
|
|
isRRGGBBTransparent ||
|
2023-05-18 16:06:27 +02:00
|
|
|
color === COLOR_PALETTE.transparent
|
2020-11-27 02:13:38 +05:30
|
|
|
);
|
|
|
|
};
|
2020-12-05 20:00:53 +05:30
|
|
|
|
|
|
|
export type ResolvablePromise<T> = Promise<T> & {
|
|
|
|
resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void;
|
|
|
|
reject: (error: Error) => void;
|
|
|
|
};
|
|
|
|
export const resolvablePromise = <T>() => {
|
|
|
|
let resolve!: any;
|
|
|
|
let reject!: any;
|
|
|
|
const promise = new Promise((_resolve, _reject) => {
|
|
|
|
resolve = _resolve;
|
|
|
|
reject = _reject;
|
|
|
|
});
|
|
|
|
(promise as any).resolve = resolve;
|
|
|
|
(promise as any).reject = reject;
|
|
|
|
return promise as ResolvablePromise<T>;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param func handler taking at most single parameter (event).
|
|
|
|
*/
|
|
|
|
export const withBatchedUpdates = <
|
2021-11-01 15:24:05 +02:00
|
|
|
TFunction extends ((event: any) => void) | (() => void),
|
2020-12-05 20:00:53 +05:30
|
|
|
>(
|
|
|
|
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
|
|
|
) =>
|
|
|
|
((event) => {
|
|
|
|
unstable_batchedUpdates(func as TFunction, event);
|
|
|
|
}) as TFunction;
|
2020-12-07 18:35:16 +02:00
|
|
|
|
2022-02-06 17:45:37 +01:00
|
|
|
/**
|
|
|
|
* barches React state updates and throttles the calls to a single call per
|
|
|
|
* animation frame
|
|
|
|
*/
|
|
|
|
export const withBatchedUpdatesThrottled = <
|
|
|
|
TFunction extends ((event: any) => void) | (() => void),
|
|
|
|
>(
|
|
|
|
func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
|
|
|
|
) => {
|
|
|
|
// @ts-ignore
|
|
|
|
return throttleRAF<Parameters<TFunction>>(((event) => {
|
|
|
|
unstable_batchedUpdates(func, event);
|
|
|
|
}) as TFunction);
|
|
|
|
};
|
|
|
|
|
2020-12-07 18:35:16 +02:00
|
|
|
//https://stackoverflow.com/a/9462382/8418
|
|
|
|
export const nFormatter = (num: number, digits: number): string => {
|
|
|
|
const si = [
|
|
|
|
{ value: 1, symbol: "b" },
|
|
|
|
{ value: 1e3, symbol: "k" },
|
|
|
|
{ value: 1e6, symbol: "M" },
|
|
|
|
{ value: 1e9, symbol: "G" },
|
|
|
|
];
|
|
|
|
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
|
|
|
|
let index;
|
|
|
|
for (index = si.length - 1; index > 0; index--) {
|
|
|
|
if (num >= si[index].value) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
(num / si[index].value).toFixed(digits).replace(rx, "$1") + si[index].symbol
|
|
|
|
);
|
|
|
|
};
|
2021-01-10 20:48:12 +02:00
|
|
|
|
|
|
|
export const getVersion = () => {
|
2021-01-12 17:47:31 +01:00
|
|
|
return (
|
|
|
|
document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content ||
|
|
|
|
DEFAULT_VERSION
|
|
|
|
);
|
2021-01-10 20:48:12 +02:00
|
|
|
};
|
2021-02-05 18:34:35 +01:00
|
|
|
|
|
|
|
// Adapted from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/emoji.js
|
|
|
|
export const supportsEmoji = () => {
|
|
|
|
const canvas = document.createElement("canvas");
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
if (!ctx) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const offset = 12;
|
|
|
|
ctx.fillStyle = "#f00";
|
|
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.font = "32px Arial";
|
|
|
|
// Modernizr used 🐨, but it is sort of supported on Windows 7.
|
|
|
|
// Luckily 😀 isn't supported.
|
|
|
|
ctx.fillText("😀", 0, 0);
|
|
|
|
return ctx.getImageData(offset, offset, 1, 1).data[0] !== 0;
|
|
|
|
};
|
2021-04-09 20:44:54 +05:30
|
|
|
|
|
|
|
export const getNearestScrollableContainer = (
|
|
|
|
element: HTMLElement,
|
|
|
|
): HTMLElement | Document => {
|
|
|
|
let parent = element.parentElement;
|
|
|
|
while (parent) {
|
|
|
|
if (parent === document.body) {
|
|
|
|
return document;
|
|
|
|
}
|
|
|
|
const { overflowY } = window.getComputedStyle(parent);
|
|
|
|
const hasScrollableContent = parent.scrollHeight > parent.clientHeight;
|
|
|
|
if (
|
|
|
|
hasScrollableContent &&
|
2022-02-19 02:17:43 +08:00
|
|
|
(overflowY === "auto" ||
|
|
|
|
overflowY === "scroll" ||
|
|
|
|
overflowY === "overlay")
|
2021-04-09 20:44:54 +05:30
|
|
|
) {
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
parent = parent.parentElement;
|
|
|
|
}
|
|
|
|
return document;
|
|
|
|
};
|
2021-04-13 01:29:25 +05:30
|
|
|
|
|
|
|
export const focusNearestParent = (element: HTMLInputElement) => {
|
|
|
|
let parent = element.parentElement;
|
|
|
|
while (parent) {
|
|
|
|
if (parent.tabIndex > -1) {
|
|
|
|
parent.focus();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
parent = parent.parentElement;
|
|
|
|
}
|
|
|
|
};
|
2021-10-21 22:05:48 +02:00
|
|
|
|
|
|
|
export const preventUnload = (event: BeforeUnloadEvent) => {
|
|
|
|
event.preventDefault();
|
|
|
|
// NOTE: modern browsers no longer allow showing a custom message here
|
|
|
|
event.returnValue = "";
|
|
|
|
};
|
2021-11-07 14:33:21 +01:00
|
|
|
|
|
|
|
export const bytesToHexString = (bytes: Uint8Array) => {
|
|
|
|
return Array.from(bytes)
|
|
|
|
.map((byte) => `0${byte.toString(16)}`.slice(-2))
|
|
|
|
.join("");
|
|
|
|
};
|
2021-11-24 18:38:33 +01:00
|
|
|
|
2021-12-28 17:17:41 +05:30
|
|
|
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
|
2021-11-26 12:46:23 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Transforms array of objects containing `id` attribute,
|
|
|
|
* or array of ids (strings), into a Map, keyd by `id`.
|
|
|
|
*/
|
|
|
|
export const arrayToMap = <T extends { id: string } | string>(
|
|
|
|
items: readonly T[],
|
|
|
|
) => {
|
|
|
|
return items.reduce((acc: Map<string, T>, element) => {
|
|
|
|
acc.set(typeof element === "string" ? element : element.id, element);
|
|
|
|
return acc;
|
|
|
|
}, new Map());
|
|
|
|
};
|
2021-12-28 16:52:57 +05:30
|
|
|
|
2023-02-02 16:23:39 +08:00
|
|
|
export const arrayToMapWithIndex = <T extends { id: string }>(
|
|
|
|
elements: readonly T[],
|
|
|
|
) =>
|
|
|
|
elements.reduce((acc, element: T, idx) => {
|
|
|
|
acc.set(element.id, [element, idx]);
|
|
|
|
return acc;
|
|
|
|
}, new Map<string, [element: T, index: number]>());
|
|
|
|
|
2023-07-26 15:55:20 +05:30
|
|
|
export const isTestEnv = () => import.meta.env.MODE === "test";
|
2022-04-28 20:19:41 +05:30
|
|
|
|
2022-02-08 11:25:35 +01:00
|
|
|
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
|
|
|
|
return new CustomEvent(name, {
|
|
|
|
detail: {
|
|
|
|
nativeEvent,
|
|
|
|
},
|
|
|
|
cancelable: true,
|
|
|
|
});
|
|
|
|
};
|
2022-03-16 15:59:30 +01:00
|
|
|
|
|
|
|
export const updateObject = <T extends Record<string, any>>(
|
|
|
|
obj: T,
|
|
|
|
updates: Partial<T>,
|
|
|
|
): T => {
|
|
|
|
let didChange = false;
|
|
|
|
for (const key in updates) {
|
|
|
|
const value = (updates as any)[key];
|
|
|
|
if (typeof value !== "undefined") {
|
|
|
|
if (
|
|
|
|
(obj as any)[key] === value &&
|
|
|
|
// if object, always update because its attrs could have changed
|
|
|
|
(typeof value !== "object" || value === null)
|
|
|
|
) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
didChange = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!didChange) {
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...obj,
|
|
|
|
...updates,
|
|
|
|
};
|
|
|
|
};
|
2022-03-28 14:46:40 +02:00
|
|
|
|
|
|
|
export const isPrimitive = (val: any) => {
|
|
|
|
const type = typeof val;
|
|
|
|
return val == null || (type !== "object" && type !== "function");
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getFrame = () => {
|
|
|
|
try {
|
|
|
|
return window.self === window.top ? "top" : "iframe";
|
|
|
|
} catch (error) {
|
|
|
|
return "iframe";
|
|
|
|
}
|
|
|
|
};
|
2022-04-06 14:05:09 +02:00
|
|
|
|
2023-06-12 17:44:31 +02:00
|
|
|
export const isRunningInIframe = () => getFrame() === "iframe";
|
|
|
|
|
2022-04-06 14:05:09 +02:00
|
|
|
export const isPromiseLike = (
|
|
|
|
value: any,
|
|
|
|
): value is Promise<ResolutionType<typeof value>> => {
|
|
|
|
return (
|
|
|
|
!!value &&
|
|
|
|
typeof value === "object" &&
|
|
|
|
"then" in value &&
|
|
|
|
"catch" in value &&
|
|
|
|
"finally" in value
|
|
|
|
);
|
|
|
|
};
|
2022-06-23 17:33:50 +02:00
|
|
|
|
|
|
|
export const queryFocusableElements = (container: HTMLElement | null) => {
|
|
|
|
const focusableElements = container?.querySelectorAll<HTMLElement>(
|
|
|
|
"button, a, input, select, textarea, div[tabindex], label[tabindex]",
|
|
|
|
);
|
|
|
|
|
|
|
|
return focusableElements
|
|
|
|
? Array.from(focusableElements).filter(
|
|
|
|
(element) =>
|
|
|
|
element.tabIndex > -1 && !(element as HTMLInputElement).disabled,
|
|
|
|
)
|
|
|
|
: [];
|
|
|
|
};
|
2022-12-21 14:29:06 +05:30
|
|
|
|
2023-05-04 19:33:31 +02:00
|
|
|
export const isShallowEqual = <
|
|
|
|
T extends Record<string, any>,
|
|
|
|
I extends keyof T,
|
|
|
|
>(
|
2023-01-09 10:24:17 +01:00
|
|
|
objA: T,
|
|
|
|
objB: T,
|
2023-05-04 19:33:31 +02:00
|
|
|
comparators?: Record<I, (a: T[I], b: T[I]) => boolean>,
|
|
|
|
debug = false,
|
2023-01-09 10:24:17 +01:00
|
|
|
) => {
|
|
|
|
const aKeys = Object.keys(objA);
|
2023-05-04 19:33:31 +02:00
|
|
|
const bKeys = Object.keys(objB);
|
2023-01-09 10:24:17 +01:00
|
|
|
if (aKeys.length !== bKeys.length) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-05-04 19:33:31 +02:00
|
|
|
return aKeys.every((key) => {
|
|
|
|
const comparator = comparators?.[key as I];
|
|
|
|
const ret = comparator
|
|
|
|
? comparator(objA[key], objB[key])
|
|
|
|
: objA[key] === objB[key];
|
|
|
|
if (!ret && debug) {
|
2023-05-08 10:14:02 +02:00
|
|
|
console.info(
|
|
|
|
`%cisShallowEqual: ${key} not equal ->`,
|
|
|
|
"color: #8B4000",
|
|
|
|
objA[key],
|
|
|
|
objB[key],
|
|
|
|
);
|
2023-05-04 19:33:31 +02:00
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
});
|
2023-01-09 10:24:17 +01:00
|
|
|
};
|
2023-01-23 16:54:35 +01:00
|
|
|
|
|
|
|
// taken from Radix UI
|
|
|
|
// https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
|
|
|
|
export const composeEventHandlers = <E>(
|
|
|
|
originalEventHandler?: (event: E) => void,
|
|
|
|
ourEventHandler?: (event: E) => void,
|
|
|
|
{ checkForDefaultPrevented = true } = {},
|
|
|
|
) => {
|
|
|
|
return function handleEvent(event: E) {
|
|
|
|
originalEventHandler?.(event);
|
|
|
|
|
|
|
|
if (
|
|
|
|
!checkForDefaultPrevented ||
|
|
|
|
!(event as unknown as Event).defaultPrevented
|
|
|
|
) {
|
|
|
|
return ourEventHandler?.(event);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
2023-06-15 00:42:01 +08:00
|
|
|
|
|
|
|
export const isOnlyExportingSingleFrame = (
|
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
) => {
|
|
|
|
const frames = elements.filter((element) => element.type === "frame");
|
|
|
|
|
|
|
|
return (
|
|
|
|
frames.length === 1 &&
|
|
|
|
elements.every(
|
|
|
|
(element) => element.type === "frame" || element.frameId === frames[0].id,
|
|
|
|
)
|
|
|
|
);
|
|
|
|
};
|