| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 | import colors from "./colors";import {  CURSOR_TYPE,  DEFAULT_VERSION,  FONT_FAMILY,  WINDOWS_EMOJI_FALLBACK_FONT,} from "./constants";import { FontFamily, FontString } from "./element/types";import { Zoom } from "./types";import { unstable_batchedUpdates } from "react-dom";import { isDarwin } from "./keys";export const SVG_NS = "http://www.w3.org/2000/svg";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 || // newline in wysiwyg  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 || // newline in wysiwyg  target instanceof HTMLTextAreaElement ||  (target instanceof HTMLInputElement &&    (target.type === "text" || target.type === "number"));export const getFontFamilyString = ({  fontFamily,}: {  fontFamily: FontFamily;}) => {  return `${FONT_FAMILY[fontFamily]}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;};/** returns fontSize+fontFamily string for assignment to DOM elements */export const getFontString = ({  fontSize,  fontFamily,}: {  fontSize: number;  fontFamily: FontFamily;}) => {  return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;};// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.jsexport const measureText = (text: string, font: FontString) => {  const line = document.createElement("div");  const body = document.body;  line.style.position = "absolute";  line.style.whiteSpace = "pre";  line.style.font = font;  body.appendChild(line);  line.innerText = text    .split("\n")    // replace empty lines with single space because leading/trailing empty    // lines would be stripped from computation    .map((x) => x || " ")    .join("\n");  const width = line.offsetWidth;  const height = line.offsetHeight;  // Now creating 1px sized item that will be aligned to baseline  // to calculate baseline shift  const span = document.createElement("span");  span.style.display = "inline-block";  span.style.overflow = "hidden";  span.style.width = "1px";  span.style.height = "1px";  line.appendChild(span);  // Baseline is important for positioning text on canvas  const baseline = span.offsetTop + span.offsetHeight;  document.body.removeChild(line);  return { width, height, baseline };};export const debounce = <T extends any[]>(  fn: (...args: T) => void,  timeout: number,) => {  let handle = 0;  let lastArgs: T;  const ret = (...args: T) => {    lastArgs = args;    clearTimeout(handle);    handle = window.setTimeout(() => fn(...args), timeout);  };  ret.flush = () => {    clearTimeout(handle);    if (lastArgs) {      fn(...lastArgs);    }  };  ret.cancel = () => {    clearTimeout(handle);  };  return ret;};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 resetCursor = () => {  document.documentElement.style.cursor = "";};export const setCursorForShape = (shape: string) => {  if (shape === "selection") {    resetCursor();  } else {    document.documentElement.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 - zoom.translation.x - offsetLeft) * invScale - scrollX;  const y = (clientY - zoom.translation.y - 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 + offsetLeft) * zoom.value + zoom.translation.x;  const y = (sceneY + scrollY + offsetTop) * zoom.value + zoom.translation.y;  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 */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 };};/** use as a rejectionHandler to mute filesystem Abort errors */export const muteFSAbortError = (error?: Error) => {  if (error?.name === "AbortError") {    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>;};/** * @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;//https://stackoverflow.com/a/9462382/8418export 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  );};
 |