Introducing independent change detection for appState and elements Generalizing object change, cleanup, refactoring, comments, solving typing issues Shaping increment, change, delta hierarchy Structural clone of elements Introducing store and incremental API Disabling buttons for canvas actions, smaller store and changes improvements Update history entry based on latest changes, iterate through the stack for visible changes to limit empty commands Solving concurrency issues, solving (partly) linear element issues, introducing commitToStore breaking change Fixing existing tests, updating snapshots Trying to be smarter on the appstate change detection Extending collab test, refactoring action / updateScene params, bugfixes Resetting snapshots Resetting snapshots UI / API tests for history - WIP Changing actions related to the observed appstate to at least update the store snapshot - WIP Adding skipping of snapshot update flag for most no-breaking changes compatible solution Ignoring uncomitted elements from local async actions, updating store directly in updateScene Bound element issues - WIP
375 lines
9.9 KiB
TypeScript
375 lines
9.9 KiB
TypeScript
import cssVariables from "./css/variables.module.scss";
|
|
import { AppProps } from "./types";
|
|
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
|
import { COLOR_PALETTE } from "./colors";
|
|
|
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
export const isWindows = /^Win/.test(navigator.platform);
|
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
|
export const isFirefox =
|
|
"netscape" in window &&
|
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
|
navigator.userAgent.indexOf("Gecko") > 1;
|
|
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
|
export const isSafari =
|
|
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
|
// keeping function so it can be mocked in test
|
|
export const isBrave = () =>
|
|
(navigator as any).brave?.isBrave?.name === "isBrave";
|
|
|
|
export const APP_NAME = "Excalidraw";
|
|
|
|
export const DRAGGING_THRESHOLD = 10; // px
|
|
export const LINE_CONFIRM_THRESHOLD = 8; // px
|
|
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
|
|
export const ELEMENT_TRANSLATE_AMOUNT = 1;
|
|
export const TEXT_TO_CENTER_SNAP_THRESHOLD = 30;
|
|
export const SHIFT_LOCKING_ANGLE = Math.PI / 12;
|
|
export const CURSOR_TYPE = {
|
|
TEXT: "text",
|
|
CROSSHAIR: "crosshair",
|
|
GRABBING: "grabbing",
|
|
GRAB: "grab",
|
|
POINTER: "pointer",
|
|
MOVE: "move",
|
|
AUTO: "",
|
|
};
|
|
export const POINTER_BUTTON = {
|
|
MAIN: 0,
|
|
WHEEL: 1,
|
|
SECONDARY: 2,
|
|
TOUCH: -1,
|
|
} as const;
|
|
|
|
export const POINTER_EVENTS = {
|
|
enabled: "all",
|
|
disabled: "none",
|
|
// asserted as any so it can be freely assigned to React Element
|
|
// "pointerEnvets" CSS prop
|
|
inheritFromUI: "var(--ui-pointerEvents)" as any,
|
|
} as const;
|
|
|
|
export enum EVENT {
|
|
COPY = "copy",
|
|
PASTE = "paste",
|
|
CUT = "cut",
|
|
KEYDOWN = "keydown",
|
|
KEYUP = "keyup",
|
|
MOUSE_MOVE = "mousemove",
|
|
RESIZE = "resize",
|
|
UNLOAD = "unload",
|
|
FOCUS = "focus",
|
|
BLUR = "blur",
|
|
DRAG_OVER = "dragover",
|
|
DROP = "drop",
|
|
GESTURE_END = "gestureend",
|
|
BEFORE_UNLOAD = "beforeunload",
|
|
GESTURE_START = "gesturestart",
|
|
GESTURE_CHANGE = "gesturechange",
|
|
POINTER_MOVE = "pointermove",
|
|
POINTER_DOWN = "pointerdown",
|
|
POINTER_UP = "pointerup",
|
|
STATE_CHANGE = "statechange",
|
|
WHEEL = "wheel",
|
|
TOUCH_START = "touchstart",
|
|
TOUCH_END = "touchend",
|
|
HASHCHANGE = "hashchange",
|
|
VISIBILITY_CHANGE = "visibilitychange",
|
|
SCROLL = "scroll",
|
|
// custom events
|
|
EXCALIDRAW_LINK = "excalidraw-link",
|
|
MENU_ITEM_SELECT = "menu.itemSelect",
|
|
MESSAGE = "message",
|
|
FULLSCREENCHANGE = "fullscreenchange",
|
|
}
|
|
|
|
export const YOUTUBE_STATES = {
|
|
UNSTARTED: -1,
|
|
ENDED: 0,
|
|
PLAYING: 1,
|
|
PAUSED: 2,
|
|
BUFFERING: 3,
|
|
CUED: 5,
|
|
} as const;
|
|
|
|
export const ENV = {
|
|
TEST: "test",
|
|
DEVELOPMENT: "development",
|
|
};
|
|
|
|
export const CLASSES = {
|
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
|
};
|
|
|
|
// 1-based in case we ever do `if(element.fontFamily)`
|
|
export const FONT_FAMILY = {
|
|
Virgil: 1,
|
|
Helvetica: 2,
|
|
Cascadia: 3,
|
|
Assistant: 4,
|
|
};
|
|
|
|
export const THEME = {
|
|
LIGHT: "light",
|
|
DARK: "dark",
|
|
} as const;
|
|
|
|
export const FRAME_STYLE = {
|
|
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
|
strokeWidth: 2 as ExcalidrawElement["strokeWidth"],
|
|
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
|
|
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
|
|
roughness: 0 as ExcalidrawElement["roughness"],
|
|
roundness: null as ExcalidrawElement["roundness"],
|
|
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
|
|
radius: 8,
|
|
nameOffsetY: 3,
|
|
nameColorLightTheme: "#999999",
|
|
nameColorDarkTheme: "#7a7a7a",
|
|
nameFontSize: 14,
|
|
nameLineHeight: 1.25,
|
|
};
|
|
|
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
|
|
|
export const MIN_FONT_SIZE = 1;
|
|
export const DEFAULT_FONT_SIZE = 20;
|
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
|
export const DEFAULT_TEXT_ALIGN = "left";
|
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
|
export const DEFAULT_VERSION = "{version}";
|
|
|
|
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
|
|
|
export const GRID_SIZE = 20; // TODO make it configurable?
|
|
|
|
export const IMAGE_MIME_TYPES = {
|
|
svg: "image/svg+xml",
|
|
png: "image/png",
|
|
jpg: "image/jpeg",
|
|
gif: "image/gif",
|
|
webp: "image/webp",
|
|
bmp: "image/bmp",
|
|
ico: "image/x-icon",
|
|
avif: "image/avif",
|
|
jfif: "image/jfif",
|
|
} as const;
|
|
|
|
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
|
|
|
export const MIME_TYPES = {
|
|
json: "application/json",
|
|
// excalidraw data
|
|
excalidraw: "application/vnd.excalidraw+json",
|
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
|
// image-encoded excalidraw data
|
|
"excalidraw.svg": "image/svg+xml",
|
|
"excalidraw.png": "image/png",
|
|
// binary
|
|
binary: "application/octet-stream",
|
|
// image
|
|
...IMAGE_MIME_TYPES,
|
|
} as const;
|
|
|
|
export const EXPORT_IMAGE_TYPES = {
|
|
png: "png",
|
|
svg: "svg",
|
|
clipboard: "clipboard",
|
|
} as const;
|
|
|
|
export const EXPORT_DATA_TYPES = {
|
|
excalidraw: "excalidraw",
|
|
excalidrawClipboard: "excalidraw/clipboard",
|
|
excalidrawLibrary: "excalidrawlib",
|
|
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
|
} as const;
|
|
|
|
export const EXPORT_SOURCE =
|
|
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
|
|
|
// time in milliseconds
|
|
export const IMAGE_RENDER_TIMEOUT = 500;
|
|
export const TAP_TWICE_TIMEOUT = 300;
|
|
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
|
export const TITLE_TIMEOUT = 10000;
|
|
export const VERSION_TIMEOUT = 30000;
|
|
export const SCROLL_TIMEOUT = 100;
|
|
export const ZOOM_STEP = 0.1;
|
|
export const MIN_ZOOM = 0.1;
|
|
export const MAX_ZOOM = 30.0;
|
|
export const HYPERLINK_TOOLTIP_DELAY = 300;
|
|
|
|
// Report a user inactive after IDLE_THRESHOLD milliseconds
|
|
export const IDLE_THRESHOLD = 60_000;
|
|
// Report a user active each ACTIVE_THRESHOLD milliseconds
|
|
export const ACTIVE_THRESHOLD = 3_000;
|
|
|
|
export const THEME_FILTER = cssVariables.themeFilter;
|
|
|
|
export const URL_QUERY_KEYS = {
|
|
addLibrary: "addLibrary",
|
|
} as const;
|
|
|
|
export const URL_HASH_KEYS = {
|
|
addLibrary: "addLibrary",
|
|
} as const;
|
|
|
|
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|
canvasActions: {
|
|
changeViewBackgroundColor: true,
|
|
clearCanvas: true,
|
|
export: { saveFileToDisk: true },
|
|
loadScene: true,
|
|
saveToActiveFile: true,
|
|
toggleTheme: null,
|
|
saveAsImage: true,
|
|
},
|
|
tools: {
|
|
image: true,
|
|
},
|
|
};
|
|
|
|
// breakpoints
|
|
// -----------------------------------------------------------------------------
|
|
// md screen
|
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
|
// sidebar
|
|
export const MQ_RIGHT_SIDEBAR_MIN_WIDTH = 1229;
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const LIBRARY_SIDEBAR_WIDTH = parseInt(cssVariables.rightSidebarWidth);
|
|
|
|
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
|
|
|
|
export const EXPORT_SCALES = [1, 2, 3];
|
|
export const DEFAULT_EXPORT_PADDING = 10; // px
|
|
|
|
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
|
|
|
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
|
|
|
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
export const ENCRYPTION_KEY_BITS = 128;
|
|
|
|
export const VERSIONS = {
|
|
excalidraw: 2,
|
|
excalidrawLibrary: 2,
|
|
} as const;
|
|
|
|
export const BOUND_TEXT_PADDING = 5;
|
|
export const ARROW_LABEL_WIDTH_FRACTION = 0.7;
|
|
export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11;
|
|
|
|
export const VERTICAL_ALIGN = {
|
|
TOP: "top",
|
|
MIDDLE: "middle",
|
|
BOTTOM: "bottom",
|
|
};
|
|
|
|
export const TEXT_ALIGN = {
|
|
LEFT: "left",
|
|
CENTER: "center",
|
|
RIGHT: "right",
|
|
};
|
|
|
|
export const ELEMENT_READY_TO_ERASE_OPACITY = 20;
|
|
|
|
// Radius represented as 25% of element's largest side (width/height).
|
|
// Used for LEGACY and PROPORTIONAL_RADIUS algorithms, or when the element is
|
|
// below the cutoff size.
|
|
export const DEFAULT_PROPORTIONAL_RADIUS = 0.25;
|
|
// Fixed radius for the ADAPTIVE_RADIUS algorithm. In pixels.
|
|
export const DEFAULT_ADAPTIVE_RADIUS = 32;
|
|
// roundness type (algorithm)
|
|
export const ROUNDNESS = {
|
|
// Used for legacy rounding (rectangles), which currently works the same
|
|
// as PROPORTIONAL_RADIUS, but we need to differentiate for UI purposes and
|
|
// forwards-compat.
|
|
LEGACY: 1,
|
|
|
|
// Used for linear elements & diamonds
|
|
PROPORTIONAL_RADIUS: 2,
|
|
|
|
// Current default algorithm for rectangles, using fixed pixel radius.
|
|
// It's working similarly to a regular border-radius, but attemps to make
|
|
// radius visually similar across differnt element sizes, especially
|
|
// very large and very small elements.
|
|
//
|
|
// NOTE right now we don't allow configuration and use a constant radius
|
|
// (see DEFAULT_ADAPTIVE_RADIUS constant)
|
|
ADAPTIVE_RADIUS: 3,
|
|
} as const;
|
|
|
|
export const ROUGHNESS = {
|
|
architect: 0,
|
|
artist: 1,
|
|
cartoonist: 2,
|
|
} as const;
|
|
|
|
export const STROKE_WIDTH = {
|
|
thin: 1,
|
|
bold: 2,
|
|
extraBold: 4,
|
|
} as const;
|
|
|
|
export const DEFAULT_ELEMENT_PROPS: {
|
|
strokeColor: ExcalidrawElement["strokeColor"];
|
|
backgroundColor: ExcalidrawElement["backgroundColor"];
|
|
fillStyle: ExcalidrawElement["fillStyle"];
|
|
strokeWidth: ExcalidrawElement["strokeWidth"];
|
|
strokeStyle: ExcalidrawElement["strokeStyle"];
|
|
roughness: ExcalidrawElement["roughness"];
|
|
opacity: ExcalidrawElement["opacity"];
|
|
locked: ExcalidrawElement["locked"];
|
|
} = {
|
|
strokeColor: COLOR_PALETTE.black,
|
|
backgroundColor: COLOR_PALETTE.transparent,
|
|
fillStyle: "solid",
|
|
strokeWidth: 2,
|
|
strokeStyle: "solid",
|
|
roughness: ROUGHNESS.artist,
|
|
opacity: 100,
|
|
locked: false,
|
|
};
|
|
|
|
export const LIBRARY_SIDEBAR_TAB = "library";
|
|
|
|
export const DEFAULT_SIDEBAR = {
|
|
name: "default",
|
|
defaultTab: LIBRARY_SIDEBAR_TAB,
|
|
} as const;
|
|
|
|
export const LIBRARY_DISABLED_TYPES = new Set([
|
|
"iframe",
|
|
"embeddable",
|
|
"image",
|
|
] as const);
|
|
|
|
// use these constants to easily identify reference sites
|
|
export const TOOL_TYPE = {
|
|
selection: "selection",
|
|
rectangle: "rectangle",
|
|
diamond: "diamond",
|
|
ellipse: "ellipse",
|
|
arrow: "arrow",
|
|
line: "line",
|
|
freedraw: "freedraw",
|
|
text: "text",
|
|
image: "image",
|
|
eraser: "eraser",
|
|
hand: "hand",
|
|
frame: "frame",
|
|
magicframe: "magicframe",
|
|
embeddable: "embeddable",
|
|
laser: "laser",
|
|
} as const;
|
|
|
|
export const EDITOR_LS_KEYS = {
|
|
OAI_API_KEY: "excalidraw-oai-api-key",
|
|
// legacy naming (non)scheme
|
|
PUBLISH_LIBRARY: "publish-library-data",
|
|
} as const;
|