123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- import oc from "open-color";
- import colors from "./colors";
- import {
- CURSOR_TYPE,
- DEFAULT_VERSION,
- EVENT,
- FONT_FAMILY,
- MIME_TYPES,
- THEME,
- WINDOWS_EMOJI_FALLBACK_FONT,
- } from "./constants";
- import { FontFamilyValues, FontString } from "./element/types";
- import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types";
- import { unstable_batchedUpdates } from "react-dom";
- import { isDarwin } from "./keys";
- import { SHAPES } from "./shapes";
- let mockDateTime: string | null = null;
- export const setDateTimeForTests = (dateTime: string) => {
- mockDateTime = dateTime;
- };
- export const getDateTime = () => {
- if (mockDateTime) {
- return mockDateTime;
- }
- const date = new Date();
- const year = date.getFullYear();
- 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");
- return `${year}-${month}-${day}-${hr}${min}`;
- };
- export const capitalizeString = (str: string) =>
- str.charAt(0).toUpperCase() + str.slice(1);
- export const isToolIcon = (
- target: Element | EventTarget | null,
- ): target is HTMLElement =>
- target instanceof HTMLElement && target.className.includes("ToolIcon");
- export const isInputLike = (
- target: Element | EventTarget | null,
- ): target is
- | HTMLInputElement
- | HTMLTextAreaElement
- | HTMLSelectElement
- | HTMLBRElement
- | HTMLDivElement =>
- (target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
- target instanceof HTMLBRElement ||
- target instanceof HTMLInputElement ||
- target instanceof HTMLTextAreaElement ||
- target instanceof HTMLSelectElement;
- export const isWritableElement = (
- target: Element | EventTarget | null,
- ): target is
- | HTMLInputElement
- | HTMLTextAreaElement
- | HTMLBRElement
- | HTMLDivElement =>
- (target instanceof HTMLElement && target.dataset.type === "wysiwyg") ||
- target instanceof HTMLBRElement ||
- target instanceof HTMLTextAreaElement ||
- (target instanceof HTMLInputElement &&
- (target.type === "text" || target.type === "number"));
- export const getFontFamilyString = ({
- fontFamily,
- }: {
- fontFamily: FontFamilyValues;
- }) => {
- for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
- if (id === fontFamily) {
- return `${fontFamilyString}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
- }
- }
- return WINDOWS_EMOJI_FALLBACK_FONT;
- };
- export const getFontString = ({
- fontSize,
- fontFamily,
- }: {
- fontSize: number;
- fontFamily: FontFamilyValues;
- }) => {
- return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
- };
- export const debounce = <T extends any[]>(
- fn: (...args: T) => void,
- timeout: number,
- ) => {
- let handle = 0;
- let lastArgs: T | null = null;
- const ret = (...args: T) => {
- lastArgs = args;
- clearTimeout(handle);
- handle = window.setTimeout(() => {
- lastArgs = null;
- fn(...args);
- }, timeout);
- };
- ret.flush = () => {
- clearTimeout(handle);
- if (lastArgs) {
- const _lastArgs = lastArgs;
- lastArgs = null;
- fn(..._lastArgs);
- }
- };
- ret.cancel = () => {
- lastArgs = null;
- clearTimeout(handle);
- };
- return ret;
- };
- export const throttleRAF = <T extends any[]>(
- fn: (...args: T) => void,
- opts?: { trailing?: boolean },
- ) => {
- let timerId: number | null = null;
- let lastArgs: T | null = null;
- 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);
- }
- });
- };
- const ret = (...args: T) => {
- if (process.env.NODE_ENV === "test") {
- fn(...args);
- return;
- }
- lastArgs = args;
- if (timerId === null) {
- scheduleFunc(lastArgs);
- } else if (opts?.trailing) {
- lastArgsTrailing = args;
- }
- };
- ret.flush = () => {
- if (timerId !== null) {
- cancelAnimationFrame(timerId);
- timerId = null;
- }
- if (lastArgs) {
- fn(...(lastArgsTrailing || lastArgs));
- lastArgs = lastArgsTrailing = null;
- }
- };
- ret.cancel = () => {
- lastArgs = lastArgsTrailing = null;
- if (timerId !== null) {
- cancelAnimationFrame(timerId);
- timerId = null;
- }
- };
- return ret;
- };
- export const chunk = <T extends any>(
- array: readonly T[],
- size: number,
- ): T[][] => {
- 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;
- };
- export const selectNode = (node: Element) => {
- const selection = window.getSelection();
- if (selection) {
- const range = document.createRange();
- range.selectNodeContents(node);
- selection.removeAllRanges();
- selection.addRange(range);
- }
- };
- export const removeSelection = () => {
- const selection = window.getSelection();
- if (selection) {
- selection.removeAllRanges();
- }
- };
- export const distance = (x: number, y: number) => Math.abs(x - y);
- 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,
- };
- };
- 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;
- }
- };
- 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`,
- );
- };
- export const setCursorForShape = (
- canvas: HTMLCanvasElement | null,
- appState: AppState,
- ) => {
- if (!canvas) {
- return;
- }
- if (appState.activeTool.type === "selection") {
- resetCursor(canvas);
- } else if (appState.activeTool.type === "eraser") {
- setEraserCursor(canvas, appState.theme);
-
-
-
- } else if (!["image", "custom"].includes(appState.activeTool.type)) {
- canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
- }
- };
- export const isFullScreen = () =>
- document.fullscreenElement?.nodeName === "HTML";
- export const allowFullScreen = () =>
- document.documentElement.requestFullscreen();
- export const exitFullScreen = () => document.exitFullscreen();
- export const getShortcutKey = (shortcut: string): string => {
- 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
- .replace(/\bCtrlOrCmd\b/i, "Cmd")
- .replace(/\bAlt\b/i, "Option");
- }
- return shortcut.replace(/\bCtrlOrCmd\b/i, "Ctrl");
- };
- export const viewportCoordsToSceneCoords = (
- { clientX, clientY }: { clientX: number; clientY: number },
- {
- zoom,
- offsetLeft,
- offsetTop,
- scrollX,
- scrollY,
- }: {
- zoom: Zoom;
- offsetLeft: number;
- offsetTop: number;
- scrollX: number;
- scrollY: number;
- },
- ) => {
- const invScale = 1 / zoom.value;
- const x = (clientX - offsetLeft) * invScale - scrollX;
- const y = (clientY - offsetTop) * invScale - scrollY;
- return { x, y };
- };
- export const sceneCoordsToViewportCoords = (
- { sceneX, sceneY }: { sceneX: number; sceneY: number },
- {
- zoom,
- offsetLeft,
- offsetTop,
- scrollX,
- scrollY,
- }: {
- zoom: Zoom;
- offsetLeft: number;
- offsetTop: number;
- scrollX: number;
- scrollY: number;
- },
- ) => {
- const x = (sceneX + scrollX) * zoom.value + offsetLeft;
- const y = (sceneY + scrollY) * zoom.value + offsetTop;
- return { x, y };
- };
- export const getGlobalCSSVariable = (name: string) =>
- getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
- 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}]`);
- export const isRTL = (text: string) => RE_RTL_CHECK.test(text);
- export const tupleToCoors = (
- xyTuple: readonly [number, number],
- ): { x: number; y: number } => {
- const [x, y] = xyTuple;
- return { x, y };
- };
- export const muteFSAbortError = (error?: Error) => {
- if (error?.name === "AbortError") {
- console.warn(error);
- return;
- }
- throw error;
- };
- 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));
- let index = fromIndex - 1;
- while (++index < array.length) {
- if (cb(array[index], index, array)) {
- return index;
- }
- }
- 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));
- let index = fromIndex + 1;
- while (--index > -1) {
- if (cb(array[index], index, array)) {
- return index;
- }
- }
- return -1;
- };
- 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]
- );
- };
- 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>;
- };
- export const withBatchedUpdates = <
- TFunction extends ((event: any) => void) | (() => void),
- >(
- func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
- ) =>
- ((event) => {
- unstable_batchedUpdates(func as TFunction, event);
- }) as TFunction;
- export const withBatchedUpdatesThrottled = <
- TFunction extends ((event: any) => void) | (() => void),
- >(
- func: Parameters<TFunction>["length"] extends 0 | 1 ? TFunction : never,
- ) => {
-
- return throttleRAF<Parameters<TFunction>>(((event) => {
- unstable_batchedUpdates(func, event);
- }) as TFunction);
- };
- //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
- );
- };
- export const getVersion = () => {
- return (
- document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content ||
- DEFAULT_VERSION
- );
- };
- 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";
-
-
- ctx.fillText("😀", 0, 0);
- return ctx.getImageData(offset, offset, 1, 1).data[0] !== 0;
- };
- 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 &&
- (overflowY === "auto" ||
- overflowY === "scroll" ||
- overflowY === "overlay")
- ) {
- return parent;
- }
- parent = parent.parentElement;
- }
- return document;
- };
- export const focusNearestParent = (element: HTMLInputElement) => {
- let parent = element.parentElement;
- while (parent) {
- if (parent.tabIndex > -1) {
- parent.focus();
- return;
- }
- parent = parent.parentElement;
- }
- };
- export const preventUnload = (event: BeforeUnloadEvent) => {
- event.preventDefault();
-
- event.returnValue = "";
- };
- export const bytesToHexString = (bytes: Uint8Array) => {
- return Array.from(bytes)
- .map((byte) => `0${byte.toString(16)}`.slice(-2))
- .join("");
- };
- export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
- 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());
- };
- export const isTestEnv = () =>
- typeof process !== "undefined" && process.env?.NODE_ENV === "test";
- export const isProdEnv = () =>
- typeof process !== "undefined" && process.env?.NODE_ENV === "production";
- export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
- return new CustomEvent(name, {
- detail: {
- nativeEvent,
- },
- cancelable: true,
- });
- };
- 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 &&
-
- (typeof value !== "object" || value === null)
- ) {
- continue;
- }
- didChange = true;
- }
- }
- if (!didChange) {
- return obj;
- }
- return {
- ...obj,
- ...updates,
- };
- };
- 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";
- }
- };
- 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
- );
- };
- 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,
- )
- : [];
- };
|