excalidraw/src/utils.ts

251 lines
7.0 KiB
TypeScript
Raw Normal View History

import { FlooredNumber } from "./types";
import { getZoomOrigin } from "./scene";
import { CURSOR_TYPE, FONT_FAMILY } from "./constants";
import { FontFamily, FontString } from "./element/types";
export const SVG_NS = "http://www.w3.org/2000/svg";
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: FontFamily;
}) => {
return FONT_FAMILY[fontFamily];
};
/** 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.js
export 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,
2020-01-24 12:04:54 +02:00
timeout: number,
) => {
let handle = 0;
2020-01-20 18:37:42 +01:00
let lastArgs: T;
const ret = (...args: T) => {
lastArgs = args;
clearTimeout(handle);
handle = window.setTimeout(() => fn(...args), timeout);
};
2020-01-20 18:37:42 +01:00
ret.flush = () => {
clearTimeout(handle);
fn(...lastArgs);
};
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;
}
};
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 => {
const isMac = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
if (isMac) {
return `${shortcut
.replace(/\bCtrlOrCmd\b/i, "Cmd")
.replace(/\bAlt\b/i, "Option")
.replace(/\bDel\b/i, "Delete")
.replace(/\b(Enter|Return)\b/i, "Enter")}`;
}
return `${shortcut.replace(/\bCtrlOrCmd\b/i, "Ctrl")}`;
};
export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number },
{
scrollX,
scrollY,
zoom,
}: {
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
canvas: HTMLCanvasElement | null,
scale: number,
) => {
const zoomOrigin = getZoomOrigin(canvas, scale);
const clientXWithZoom = zoomOrigin.x + (clientX - zoomOrigin.x) / zoom;
const clientYWithZoom = zoomOrigin.y + (clientY - zoomOrigin.y) / zoom;
const x = clientXWithZoom - scrollX;
const y = clientYWithZoom - scrollY;
return { x, y };
};
export const sceneCoordsToViewportCoords = (
{ sceneX, sceneY }: { sceneX: number; sceneY: number },
{
scrollX,
scrollY,
zoom,
}: {
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
canvas: HTMLCanvasElement | null,
scale: number,
) => {
const zoomOrigin = getZoomOrigin(canvas, scale);
const x = zoomOrigin.x - (zoomOrigin.x - sceneX - scrollX) * zoom;
const y = zoomOrigin.y - (zoomOrigin.y - sceneY - scrollY) * zoom;
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) => {
return RE_RTL_CHECK.test(text);
};
export function tupleToCoors(
xyTuple: [number, number],
): { x: number; y: number } {
const [x, y] = xyTuple;
return { x, y };
}