2022-03-15 20:56:39 +05:30
|
|
|
import oc from "open-color";
|
|
|
|
|
2020-11-27 02:13:38 +05:30
|
|
|
import colors 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,
|
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";
|
2021-06-13 21:26:55 +05:30
|
|
|
import { FontFamilyValues, FontString } from "./element/types";
|
2022-05-06 18:21:22 +05:30
|
|
|
import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types";
|
2020-12-05 20:00:53 +05:30
|
|
|
import { unstable_batchedUpdates } from "react-dom";
|
2020-12-22 11:00:51 +01:00
|
|
|
import { isDarwin } from "./keys";
|
2022-05-06 18:21:22 +05:30
|
|
|
import { SHAPES } from "./shapes";
|
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;
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
if (process.env.NODE_ENV === "test") {
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
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: (
|
|
|
|
| { type: typeof SHAPES[number]["value"] | "eraser" }
|
|
|
|
| { type: "custom"; customType: string }
|
|
|
|
) & { lastActiveToolBeforeEraser?: LastActiveToolBeforeEraser },
|
|
|
|
): AppState["activeTool"] => {
|
|
|
|
if (data.type === "custom") {
|
|
|
|
return {
|
|
|
|
...appState.activeTool,
|
|
|
|
type: "custom",
|
|
|
|
customType: data.customType,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...appState.activeTool,
|
|
|
|
lastActiveToolBeforeEraser:
|
|
|
|
data.lastActiveToolBeforeEraser === undefined
|
|
|
|
? appState.activeTool.lastActiveToolBeforeEraser
|
|
|
|
: 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,
|
2022-03-15 20:56:39 +05:30
|
|
|
appState: AppState,
|
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);
|
2022-03-25 20:46:01 +05:30
|
|
|
} else if (appState.activeTool.type === "eraser") {
|
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")
|
|
|
|
.replace(/\b(Enter|Return)\b/i, "Enter")
|
|
|
|
.replace(/\bDel\b/i, "Delete");
|
|
|
|
|
|
|
|
if (isDarwin) {
|
|
|
|
return shortcut
|
2020-06-01 11:35:44 +02:00
|
|
|
.replace(/\bCtrlOrCmd\b/i, "Cmd")
|
2020-12-22 11:00:51 +01:00
|
|
|
.replace(/\bAlt\b/i, "Option");
|
2020-03-09 15:06:35 +02:00
|
|
|
}
|
2020-12-22 11:00:51 +01:00
|
|
|
return shortcut.replace(/\bCtrlOrCmd\b/i, "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
|
|
|
) => {
|
2020-11-04 17:49:15 +00:00
|
|
|
const invScale = 1 / zoom.value;
|
2022-01-29 21:12:44 +01:00
|
|
|
const x = (clientX - offsetLeft) * invScale - scrollX;
|
|
|
|
const y = (clientY - offsetTop) * invScale - scrollY;
|
|
|
|
|
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 ||
|
|
|
|
color === colors.elementBackground[0]
|
|
|
|
);
|
|
|
|
};
|
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
|
|
|
|
2021-12-28 17:17:41 +05:30
|
|
|
export const isTestEnv = () =>
|
|
|
|
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
|
2022-02-08 11:25:35 +01:00
|
|
|
|
2022-04-28 20:19:41 +05:30
|
|
|
export const isProdEnv = () =>
|
|
|
|
typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
|
|
|
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
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
: [];
|
|
|
|
};
|