excalidraw/src/utils.ts

671 lines
17 KiB
TypeScript
Raw Normal View History

import oc from "open-color";
import colors from "./colors";
import {
CURSOR_TYPE,
2021-01-10 20:48:12 +02:00
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";
2020-03-23 16:38:41 -07:00
let mockDateTime: string | null = null;
export const setDateTimeForTests = (dateTime: string) => {
2020-03-23 16:38:41 -07:00
mockDateTime = dateTime;
};
2020-03-23 16:38:41 -07:00
export const getDateTime = () => {
2020-03-23 16:38:41 -07:00
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 = (
2020-01-24 12:04:54 +02:00
target: Element | EventTarget | null,
): target is
| HTMLInputElement
| HTMLTextAreaElement
| HTMLSelectElement
2020-02-04 11:50:18 +01:00
| HTMLBRElement
| 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
| 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
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;
};
/** returns fontSize+fontFamily string for assignment to DOM elements */
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,
2020-01-24 12:04:54 +02:00
timeout: number,
) => {
let handle = 0;
let lastArgs: T | null = null;
2020-01-20 18:37:42 +01:00
const ret = (...args: T) => {
lastArgs = args;
clearTimeout(handle);
handle = window.setTimeout(() => {
lastArgs = null;
fn(...args);
}, timeout);
};
2020-01-20 18:37:42 +01:00
ret.flush = () => {
clearTimeout(handle);
if (lastArgs) {
const _lastArgs = lastArgs;
lastArgs = null;
fn(..._lastArgs);
}
2020-01-20 18:37:42 +01:00
};
ret.cancel = () => {
lastArgs = null;
clearTimeout(handle);
};
2020-01-20 18:37:42 +01:00
return ret;
};
// throttle callback to execute once per animation frame
export const throttleRAF = <T extends any[]>(fn: (...args: T) => void) => {
let handle: number | null = null;
let lastArgs: T | null = null;
let callback: ((...args: T) => void) | null = null;
const ret = (...args: T) => {
if (process.env.NODE_ENV === "test") {
fn(...args);
return;
}
lastArgs = args;
callback = fn;
if (handle === null) {
handle = window.requestAnimationFrame(() => {
handle = null;
lastArgs = null;
callback = null;
fn(...args);
});
}
};
ret.flush = () => {
if (handle !== null) {
cancelAnimationFrame(handle);
handle = null;
}
if (lastArgs) {
const _lastArgs = lastArgs;
const _callback = callback;
lastArgs = null;
callback = null;
if (_callback !== null) {
_callback(..._lastArgs);
}
}
};
ret.cancel = () => {
lastArgs = null;
callback = null;
if (handle !== null) {
cancelAnimationFrame(handle);
handle = null;
}
};
return ret;
};
feat: Allow publishing libraries from UI (#4115) * feat: Allow publishing libraries from UI * Add status for each library item and show publish only for unpublished libs * Add publish library dialog * Pass the data to publish the library * pass lib blob * Handle old and new libraries when importing * Better error handling * Show publish success when library submitted for review * don't close library when publish success dialog open * Support multiple libs deletion and publish * Set status to published once library submitted for review * Save to LS after library published * unique key for publish and delete * fix layout shift when hover and also highlight selected library items * design improvements * migrate old library to the new one * fix * fix tests * use i18n * Support submit type in toolbutton * Use html5 form validation, add asteriks for required fields, add twitter handle, mark github handle optional * Add twitter handle in form state * revert html5 validation as fetch is giving some issues :/ * clarify types around LibraryItems * Add website optional field * event.preventDefault to make htm5 form validationw work * improve png generation by drawing a bounding box rect and aligining pngs to support multiple libs png * remove ts-ignore * add placeholders for fields * decrease clickable area for checkbox by 0.5em * add checkbox background color * rename `items` to `elements` * improve checkbox hit area * show selected library items in publish dialog * decrease dimensions by 3px to improve jerky experience when opening/closing library menu * Don't close publish dialog when clicked outside * Show selected library actions only when any library item selected and use icons instead of button * rename library to libraryItems in excalidrawLib and added migration * change icon and swap bg/color * use blue brand color for hover/selected states * prompt for confirmation when deleting library items * separate unpublished items from published * factor `LibraryMenu` into own file * i18n and minor fixes for unpublished items * fix not rendering empty cells when library empty * don't render published section if empty and unpublished is not * Add edit name functionality for library items * fix * edit lib name with onchange/blur * bump library version * prefer response error message * add library urls to ENV vars * mark lib item name as required * Use input only for lib item name * better error validation for lib items * fix label styling for lib items * design and i18n fixes * Save publish dialog data to local storage and clear once published * Add a note about MIT License * Add note for guidelines * Add tooltip for publish button * Show spinner in submit button when submission is in progress * assign id for older lib items when installed and set status as published for all lib when installed * update export icon and support export library for selected items * move LibraryMenuItems into its own component as its best to keep one comp per file * fix spec * Refactoring the library actions for reusablility * show only load when items not present * close on click outside in publish dialog * ad dialog description and tweak copy * vertically center input labels * align input styles * move author name input to other usernames * rename param * inline to simplify * fix to not inline `undefined` class names * fix version & include only latest lib schema in library export type * await response callback * refactor types * refactor * i18n * align casing & tweaks * move ls logic to publishLibrary * support removal of item inside publish dialog * fix labels for trash icon when items selected * replace window.confirm for removal libs with confirm dialog * fix input/textarea styling * move library item menu scss to its own file * use blue for load and cyan for publish * reduce margin for submit and make submit => Submit * Make library items header sticky * move publish icon to left so there is no jerkiness when unpublish items selected * update url * fix grid gap between lib items * Mark older items imported from initial data as unpublished * add text to publish button on non-mobile * add items counter * fix test * show personal and excal libs sections and personal goes first * show toast on adding to library via contextMenu * Animate plus icon and not the pending item * fix snap * use i18n when no item in publish dialog * tweak style of new lib item * show empty cells for both sections and set status as published for installed libs * fix * push selected item first in unpublished section * set status as published for imported from webiste but unpublished for json * Add items to the begining of library * add `created` library item attr * fix test * use `defaultValue` instead of `value` * fix dark theme styles * fix toggle button not closing library * close library menu on Escape * tweak publish dialog item remove style * fix remove icon in publish dialog Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-17 23:53:43 +05:30
// https://github.com/lodash/lodash/blob/es/chunk.js
export const chunk = <T extends any>(
array: readonly T[],
size: number,
): T[][] => {
feat: Allow publishing libraries from UI (#4115) * feat: Allow publishing libraries from UI * Add status for each library item and show publish only for unpublished libs * Add publish library dialog * Pass the data to publish the library * pass lib blob * Handle old and new libraries when importing * Better error handling * Show publish success when library submitted for review * don't close library when publish success dialog open * Support multiple libs deletion and publish * Set status to published once library submitted for review * Save to LS after library published * unique key for publish and delete * fix layout shift when hover and also highlight selected library items * design improvements * migrate old library to the new one * fix * fix tests * use i18n * Support submit type in toolbutton * Use html5 form validation, add asteriks for required fields, add twitter handle, mark github handle optional * Add twitter handle in form state * revert html5 validation as fetch is giving some issues :/ * clarify types around LibraryItems * Add website optional field * event.preventDefault to make htm5 form validationw work * improve png generation by drawing a bounding box rect and aligining pngs to support multiple libs png * remove ts-ignore * add placeholders for fields * decrease clickable area for checkbox by 0.5em * add checkbox background color * rename `items` to `elements` * improve checkbox hit area * show selected library items in publish dialog * decrease dimensions by 3px to improve jerky experience when opening/closing library menu * Don't close publish dialog when clicked outside * Show selected library actions only when any library item selected and use icons instead of button * rename library to libraryItems in excalidrawLib and added migration * change icon and swap bg/color * use blue brand color for hover/selected states * prompt for confirmation when deleting library items * separate unpublished items from published * factor `LibraryMenu` into own file * i18n and minor fixes for unpublished items * fix not rendering empty cells when library empty * don't render published section if empty and unpublished is not * Add edit name functionality for library items * fix * edit lib name with onchange/blur * bump library version * prefer response error message * add library urls to ENV vars * mark lib item name as required * Use input only for lib item name * better error validation for lib items * fix label styling for lib items * design and i18n fixes * Save publish dialog data to local storage and clear once published * Add a note about MIT License * Add note for guidelines * Add tooltip for publish button * Show spinner in submit button when submission is in progress * assign id for older lib items when installed and set status as published for all lib when installed * update export icon and support export library for selected items * move LibraryMenuItems into its own component as its best to keep one comp per file * fix spec * Refactoring the library actions for reusablility * show only load when items not present * close on click outside in publish dialog * ad dialog description and tweak copy * vertically center input labels * align input styles * move author name input to other usernames * rename param * inline to simplify * fix to not inline `undefined` class names * fix version & include only latest lib schema in library export type * await response callback * refactor types * refactor * i18n * align casing & tweaks * move ls logic to publishLibrary * support removal of item inside publish dialog * fix labels for trash icon when items selected * replace window.confirm for removal libs with confirm dialog * fix input/textarea styling * move library item menu scss to its own file * use blue for load and cyan for publish * reduce margin for submit and make submit => Submit * Make library items header sticky * move publish icon to left so there is no jerkiness when unpublish items selected * update url * fix grid gap between lib items * Mark older items imported from initial data as unpublished * add text to publish button on non-mobile * add items counter * fix test * show personal and excal libs sections and personal goes first * show toast on adding to library via contextMenu * Animate plus icon and not the pending item * fix snap * use i18n when no item in publish dialog * tweak style of new lib item * show empty cells for both sections and set status as published for installed libs * fix * push selected item first in unpublished section * set status as published for imported from webiste but unpublished for json * Add items to the begining of library * add `created` library item attr * fix test * use `defaultValue` instead of `value` * fix dark theme styles * fix toggle button not closing library * close library menu on Escape * tweak publish dialog item remove style * fix remove icon in publish dialog Co-authored-by: dwelle <luzar.david@gmail.com>
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;
};
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);
// do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor
// Ignore custom type as well and let host decide
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
}
};
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();
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");
};
2020-11-04 17:49:15 +00:00
export const viewportCoordsToSceneCoords = (
{ 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-11-04 17:49:15 +00:00
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 },
2020-11-04 17:49:15 +00:00
{
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}]`);
/**
* 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-11-06 22:06:39 +02:00
export const tupleToCoors = (
Allow binding linear elements to other elements (#1899) * Refactor: simplify linear element type * Refactor: dedupe scrollbar handling * First step towards binding - establish relationship and basic test for dragged lines * Refactor: use zoom from appstate * Refactor: generalize getElementAtPosition * Only consider bindable elements in hit test * Refactor: pull out pieces of hit test for reuse later * Refactor: pull out diamond from hit test for reuse later * Refactor: pull out text from hit test for reuse later * Suggest binding when hovering * Give shapes in regression test real size * Give shapes in undo/redo test real size * Keep bound element highlighted * Show binding suggestion for multi-point elements * Move binding to its on module with functions so that I can use it from actions, add support for binding end of multi-point elements * Use Id instead of ID * Improve boundary offset for non-squarish elements * Fix localStorage for binding on linear elements * Simplify dragging code and fix elements bound twice to the same shape * Fix binding for rectangles * Bind both ends at the end of the linear element creation, needed for focus points * wip * Refactor: Renames and reshapes for next commit * Calculate and store focus points and gaps, but dont use them yet * Focus points for rectangles * Dont blow up when canceling linear element * Stop suggesting binding when a non-compatible tool is selected * Clean up collision code * Using Geometric Algebra for hit tests * Correct binding for all shapes * Constant gap around polygon corners * Fix rotation handling * Generalize update and fix hit test for rotated elements * Handle rotation realtime * Handle scaling * Remove vibration when moving bound and binding element together * Handle simultenous scaling * Allow binding and unbinding when editing linear elements * Dont delete binding when the end point wasnt touched * Bind on enter/escape when editing * Support multiple suggested bindable elements in preparation for supporting linear elements dragging * Update binding when moving linear elements * Update binding when resizing linear elements * Dont re-render UI on binding hints * Update both ends when one is moved * Use distance instead of focus point for binding * Complicated approach for posterity, ignore this commit * Revert the complicated approach * Better focus point strategy, working for all shapes * Update snapshots * Dont break binding gap when mirroring shape * Dont break binding gap when grid mode pushes it inside * Dont bind draw elements * Support alt duplication * Fix alt duplication to * Support cmd+D duplication * All copy mechanisms are supported * Allow binding shapes to arrows, having arrows created first * Prevent arrows from disappearing for ellipses * Better binding suggestion highlight for shapes * Dont suggest second binding for simple elements when editing or moving them * Dont steal already bound linear elements when moving shapes * Fix highlighting diamonds and more precisely highlight other shapes * Highlight linear element edges for binding * Highlight text binding too * Handle deletion * Dont suggest second binding for simple linear elements when creating them * Dont highlight bound element during creation * Fix binding for rotated linear elements * Fix collision check for ellipses * Dont show suggested bindings for selected pairs * Bind multi-point linear elements when the tool is switched - important for mobile * Handle unbinding one of two bound edges correctly * Rename boundElement in state to startBoundElement * Dont double account for zoom when rendering binding highlight * Fix rendering of edited linear element point handles * Suggest binding when adding new point to a linear element * Bind when adding a new point to a linear element and dont unbind when moving middle elements * Handle deleting points * Add cmd modifier key to disable binding * Use state for enabling binding, fix not binding for linear elements during creation * Drop support for binding lines, only arrows are bindable * Reset binding mode on blur * Fix not binding lines
2020-08-08 21:04:15 -07:00
xyTuple: readonly [number, number],
2020-11-06 22:06:39 +02:00
): { x: number; y: number } => {
const [x, y] = xyTuple;
return { x, y };
2020-11-06 22:06:39 +02:00
};
/** use as a rejectionHandler to mute filesystem Abort errors */
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));
2020-11-06 22:06:30 +02:00
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));
2020-11-06 22:06:30 +02:00
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>;
};
/**
* @param func handler taking at most single parameter (event).
*/
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;
/**
* 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);
};
//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 = () => {
return (
document.querySelector<HTMLMetaElement>('meta[name="version"]')?.content ||
DEFAULT_VERSION
);
2021-01-10 20:48:12 +02: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;
};
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();
// NOTE: modern browsers no longer allow showing a custom message here
event.returnValue = "";
};
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
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
/**
* 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());
};
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 &&
// 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";
}
};
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
);
};