@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
1,281 lines • 277 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
var __typeError = (msg) => {
throw TypeError(msg);
};
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
var __runInitializers = (array, flags, self, value) => {
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
return value;
};
var __decorateElement = (array, flags, name, decorators, target, extra) => {
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
return __privateGet(this, extra);
}, set [name](x) {
return __privateSet(this, extra, x);
} }, name));
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
if (k) {
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
}
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
else if (typeof it !== "object" || it === null) __typeError("Object expected");
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
}
return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
};
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
var __setMetaKeyTimeout_dec, __setCtrlKeyTimeout_dec, __setAltKeyTimeout_dec, __setShiftKeyTimeout_dec, _getIsReadonly_dec, _getIsFocused_dec, _getSharedOpacity_dec, _getSharedStyles_dec, __getSelectionSharedStyles_dec, __getBindingsIndexCache_dec, _getCurrentPageRenderingShapesSorted_dec, _getCurrentPageShapesSorted_dec, _getCurrentPageShapes_dec, _getCurrentPageBounds_dec, _getCulledShapes_dec, _getNotVisibleShapes_dec, __getShapeMaskedPageBoundsCache_dec, __getShapeMaskCache_dec, __getShapeClipPathCache_dec, __getShapePageBoundsCache_dec, __getShapePageTransformCache_dec, __getShapeHandlesCache_dec, __getAllAssetsQuery_dec, _getCurrentPageShapeIdsSorted_dec, _getCurrentPageId_dec, _getPages_dec, __getAllPagesQuery_dec, _getRenderingShapes_dec, _getCollaboratorsOnCurrentPage_dec, _getCollaborators_dec, __getCollaboratorsQuery_dec, _getViewportPageBounds_dec, _getViewportScreenCenter_dec, _getViewportScreenBounds_dec, _getZoomLevel_dec, _getCameraForFollowing_dec, _getViewportPageBoundsForFollowing_dec, _getCamera_dec, __unsafe_getCameraId_dec, _getErasingShapes_dec, _getErasingShapeIds_dec, _getHintingShape_dec, _getHintingShapeIds_dec, _getHoveredShape_dec, _getHoveredShapeId_dec, _getRichTextEditor_dec, _getEditingShape_dec, _getEditingShapeId_dec, _getFocusedGroup_dec, _getFocusedGroupId_dec, _getSelectionRotatedScreenBounds_dec, _getSelectionRotatedPageBounds_dec, _getSelectionRotation_dec, _getSelectionPageBounds_dec, _getOnlySelectedShape_dec, _getOnlySelectedShapeId_dec, _getCurrentPageShapesInReadingOrder_dec, _getSelectedShapes_dec, _getSelectedShapeIds_dec, __getCurrentPageStateId_dec, _getCurrentPageState_dec, __getPageStatesQuery_dec, _getPageStates_dec, _getIsMenuOpen_dec, _getOpenMenus_dec, _getInstanceState_dec, _getDocumentSettings_dec, _getCurrentToolId_dec, _getCurrentTool_dec, _getPath_dec, _getCanRedo_dec, _getCanUndo_dec, _getIsShapeHiddenCache_dec, _a, _init;
import {
EMPTY_ARRAY,
atom,
computed,
react,
transact,
unsafe__withoutCapture
} from "@tldraw/state";
import {
reverseRecordsDiff
} from "@tldraw/store";
import {
CameraRecordType,
InstancePageStateRecordType,
PageRecordType,
TLDOCUMENT_ID,
TLINSTANCE_ID,
TLPOINTER_ID,
createBindingId,
createShapeId,
getShapePropKeysByStyle,
isPageId,
isShapeId
} from "@tldraw/tlschema";
import {
FileHelpers,
PerformanceTracker,
Result,
annotateError,
assert,
assertExists,
bind,
compact,
debounce,
dedupe,
exhaustiveSwitchError,
fetch,
getIndexAbove,
getIndexBetween,
getIndices,
getIndicesAbove,
getIndicesBetween,
getOwnProperty,
hasOwnProperty,
last,
lerp,
maxBy,
minBy,
sortById,
sortByIndex,
structuredClone,
uniqueId
} from "@tldraw/utils";
import EventEmitter from "eventemitter3";
import {
getSnapshot,
loadSnapshot
} from "../config/TLEditorSnapshot.mjs";
import { createTLUser } from "../config/createTLUser.mjs";
import { checkBindings } from "../config/defaultBindings.mjs";
import { checkShapesAndAddCore } from "../config/defaultShapes.mjs";
import {
DEFAULT_ANIMATION_OPTIONS,
DEFAULT_CAMERA_OPTIONS,
INTERNAL_POINTER_IDS,
LEFT_MOUSE_BUTTON,
MIDDLE_MOUSE_BUTTON,
RIGHT_MOUSE_BUTTON,
STYLUS_ERASER_BUTTON,
ZOOM_TO_FIT_PADDING
} from "../constants.mjs";
import { exportToSvg } from "../exports/exportToSvg.mjs";
import { getSvgAsImage } from "../exports/getSvgAsImage.mjs";
import { tlenv } from "../globals/environment.mjs";
import { tlmenus } from "../globals/menus.mjs";
import { tltime } from "../globals/time.mjs";
import { defaultTldrawOptions } from "../options.mjs";
import { Box } from "../primitives/Box.mjs";
import { Mat } from "../primitives/Mat.mjs";
import { Vec } from "../primitives/Vec.mjs";
import { EASINGS } from "../primitives/easings.mjs";
import { Group2d } from "../primitives/geometry/Group2d.mjs";
import { intersectPolygonPolygon } from "../primitives/intersect.mjs";
import { PI, approximately, areAnglesCompatible, clamp, pointInPolygon } from "../primitives/utils.mjs";
import { SharedStyleMap } from "../utils/SharedStylesMap.mjs";
import { areShapesContentEqual } from "../utils/areShapesContentEqual.mjs";
import { dataUrlToFile } from "../utils/assets.mjs";
import { debugFlags } from "../utils/debug-flags.mjs";
import {
createDeepLinkString,
parseDeepLinkString
} from "../utils/deepLinks.mjs";
import { getIncrementedName } from "../utils/getIncrementedName.mjs";
import { isAccelKey } from "../utils/keyboard.mjs";
import { getReorderingShapesChanges } from "../utils/reorderShapes.mjs";
import { applyRotationToSnapshotShapes, getRotationSnapshot } from "../utils/rotation.mjs";
import { bindingsIndex } from "./derivations/bindingsIndex.mjs";
import { notVisibleShapes } from "./derivations/notVisibleShapes.mjs";
import { parentsToChildren } from "./derivations/parentsToChildren.mjs";
import { deriveShapeIdsInCurrentPage } from "./derivations/shapeIdsInCurrentPage.mjs";
import { ClickManager } from "./managers/ClickManager/ClickManager.mjs";
import { EdgeScrollManager } from "./managers/EdgeScrollManager/EdgeScrollManager.mjs";
import { FocusManager } from "./managers/FocusManager/FocusManager.mjs";
import { FontManager } from "./managers/FontManager/FontManager.mjs";
import { HistoryManager } from "./managers/HistoryManager/HistoryManager.mjs";
import { ScribbleManager } from "./managers/ScribbleManager/ScribbleManager.mjs";
import { SnapManager } from "./managers/SnapManager/SnapManager.mjs";
import { TextManager } from "./managers/TextManager/TextManager.mjs";
import { TickManager } from "./managers/TickManager/TickManager.mjs";
import { UserPreferencesManager } from "./managers/UserPreferencesManager/UserPreferencesManager.mjs";
import { RootState } from "./tools/RootState.mjs";
class Editor extends (_a = EventEmitter, _getIsShapeHiddenCache_dec = [computed], _getCanUndo_dec = [computed], _getCanRedo_dec = [computed], _getPath_dec = [computed], _getCurrentTool_dec = [computed], _getCurrentToolId_dec = [computed], _getDocumentSettings_dec = [computed], _getInstanceState_dec = [computed], _getOpenMenus_dec = [computed], _getIsMenuOpen_dec = [computed], _getPageStates_dec = [computed], __getPageStatesQuery_dec = [computed], _getCurrentPageState_dec = [computed], __getCurrentPageStateId_dec = [computed], _getSelectedShapeIds_dec = [computed], _getSelectedShapes_dec = [computed], _getCurrentPageShapesInReadingOrder_dec = [computed], _getOnlySelectedShapeId_dec = [computed], _getOnlySelectedShape_dec = [computed], _getSelectionPageBounds_dec = [computed], _getSelectionRotation_dec = [computed], _getSelectionRotatedPageBounds_dec = [computed], _getSelectionRotatedScreenBounds_dec = [computed], _getFocusedGroupId_dec = [computed], _getFocusedGroup_dec = [computed], _getEditingShapeId_dec = [computed], _getEditingShape_dec = [computed], _getRichTextEditor_dec = [computed], _getHoveredShapeId_dec = [computed], _getHoveredShape_dec = [computed], _getHintingShapeIds_dec = [computed], _getHintingShape_dec = [computed], _getErasingShapeIds_dec = [computed], _getErasingShapes_dec = [computed], __unsafe_getCameraId_dec = [computed], _getCamera_dec = [computed], _getViewportPageBoundsForFollowing_dec = [computed], _getCameraForFollowing_dec = [computed], _getZoomLevel_dec = [computed], _getViewportScreenBounds_dec = [computed], _getViewportScreenCenter_dec = [computed], _getViewportPageBounds_dec = [computed], __getCollaboratorsQuery_dec = [computed], _getCollaborators_dec = [computed], _getCollaboratorsOnCurrentPage_dec = [computed], _getRenderingShapes_dec = [computed], __getAllPagesQuery_dec = [computed], _getPages_dec = [computed], _getCurrentPageId_dec = [computed], _getCurrentPageShapeIdsSorted_dec = [computed], __getAllAssetsQuery_dec = [computed], __getShapeHandlesCache_dec = [computed], __getShapePageTransformCache_dec = [computed], __getShapePageBoundsCache_dec = [computed], __getShapeClipPathCache_dec = [computed], __getShapeMaskCache_dec = [computed], __getShapeMaskedPageBoundsCache_dec = [computed], _getNotVisibleShapes_dec = [computed], _getCulledShapes_dec = [computed], _getCurrentPageBounds_dec = [computed], _getCurrentPageShapes_dec = [computed], _getCurrentPageShapesSorted_dec = [computed], _getCurrentPageRenderingShapesSorted_dec = [computed], __getBindingsIndexCache_dec = [computed], __getSelectionSharedStyles_dec = [computed], _getSharedStyles_dec = [computed({ isEqual: (a, b) => a.equals(b) })], _getSharedOpacity_dec = [computed], _getIsFocused_dec = [computed], _getIsReadonly_dec = [computed], __setShiftKeyTimeout_dec = [bind], __setAltKeyTimeout_dec = [bind], __setCtrlKeyTimeout_dec = [bind], __setMetaKeyTimeout_dec = [bind], _a) {
constructor({
store,
user,
shapeUtils,
bindingUtils,
tools,
getContainer,
cameraOptions,
textOptions,
initialState,
autoFocus,
inferDarkMode,
options,
// eslint-disable-next-line @typescript-eslint/no-deprecated
isShapeHidden,
getShapeVisibility,
fontAssetUrls
}) {
super();
__runInitializers(_init, 5, this);
__publicField(this, "id", uniqueId());
__publicField(this, "_getShapeVisibility");
__publicField(this, "options");
__publicField(this, "contextId", uniqueId());
/**
* The editor's store
*
* @public
*/
__publicField(this, "store");
/**
* The root state of the statechart.
*
* @public
*/
__publicField(this, "root");
/**
* A set of functions to call when the app is disposed.
*
* @public
*/
__publicField(this, "disposables", /* @__PURE__ */ new Set());
/**
* Whether the editor is disposed.
*
* @public
*/
__publicField(this, "isDisposed", false);
/** @internal */
__publicField(this, "_tickManager");
/**
* A manager for the app's snapping feature.
*
* @public
*/
__publicField(this, "snaps");
/**
* A manager for the any asynchronous events and making sure they're
* cleaned up upon disposal.
*
* @public
*/
__publicField(this, "timers", tltime.forContext(this.contextId));
/**
* A manager for the user and their preferences.
*
* @public
*/
__publicField(this, "user");
/**
* A helper for measuring text.
*
* @public
*/
__publicField(this, "textMeasure");
/**
* A utility for managing the set of fonts that should be rendered in the document.
*
* @public
*/
__publicField(this, "fonts");
/**
* A manager for the editor's environment.
*
* @deprecated This is deprecated and will be removed in a future version. Use the `tlenv` global export instead.
* @public
*/
__publicField(this, "environment", tlenv);
/**
* A manager for the editor's scribbles.
*
* @public
*/
__publicField(this, "scribbles");
/**
* A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details.
*
* @public
*/
__publicField(this, "sideEffects");
/**
* A manager for moving the camera when the mouse is at the edge of the screen.
*
* @public
*/
__publicField(this, "edgeScrollManager");
/**
* A manager for ensuring correct focus. See FocusManager for details.
*
* @internal
*/
__publicField(this, "focusManager");
/**
* The current HTML element containing the editor.
*
* @example
* ```ts
* const container = editor.getContainer()
* ```
*
* @public
*/
__publicField(this, "getContainer");
/* ------------------- Shape Utils ------------------ */
/**
* A map of shape utility classes (TLShapeUtils) by shape type.
*
* @public
*/
__publicField(this, "shapeUtils");
__publicField(this, "styleProps");
/* ------------------- Binding Utils ------------------ */
/**
* A map of shape utility classes (TLShapeUtils) by shape type.
*
* @public
*/
__publicField(this, "bindingUtils");
/* --------------------- History -------------------- */
/**
* A manager for the app's history.
*
* @readonly
*/
__publicField(this, "history");
__publicField(this, "_shouldIgnoreShapeLock", false);
/** @internal */
__publicField(this, "_crashingError", null);
/** @internal */
__publicField(this, "_isChangingStyleTimeout", -1);
// Menus
__publicField(this, "menus", tlmenus.forContext(this.contextId));
// Rich text editor
__publicField(this, "_currentRichTextEditor", atom("rich text editor", null));
__publicField(this, "_textOptions");
__publicField(this, "_cameraOptions", atom("camera options", DEFAULT_CAMERA_OPTIONS));
/** @internal */
__publicField(this, "_viewportAnimation", null);
// Viewport
/** @internal */
__publicField(this, "_willSetInitialBounds", true);
// Following
// When we are 'locked on' to a user, our camera is derived from their camera.
__publicField(this, "_isLockedOnFollowingUser", atom("isLockedOnFollowingUser", false));
// Camera state
// Camera state does two things: first, it allows us to subscribe to whether
// the camera is moving or not; and second, it allows us to update the rendering
// shapes on the canvas. Changing the rendering shapes may cause shapes to
// unmount / remount in the DOM, which is expensive; and computing visibility is
// also expensive in large projects. For this reason, we use a second bounding
// box just for rendering, and we only update after the camera stops moving.
__publicField(this, "_cameraState", atom("camera state", "idle"));
__publicField(this, "_cameraStateTimeoutRemaining", 0);
/* @internal */
__publicField(this, "_currentPageShapeIds");
/* --------------------- Shapes --------------------- */
__publicField(this, "_shapeGeometryCaches", {});
__publicField(this, "_notVisibleShapes", notVisibleShapes(this));
// Parents and children
/**
* A cache of parents to children.
*
* @internal
*/
__publicField(this, "_parentIdsToChildIds");
__publicField(this, "animatingShapes", /* @__PURE__ */ new Map());
/* --------------------- Content -------------------- */
/** @internal */
__publicField(this, "externalAssetContentHandlers", {
file: null,
url: null
});
/** @internal */
__publicField(this, "temporaryAssetPreview", /* @__PURE__ */ new Map());
/** @internal */
__publicField(this, "externalContentHandlers", {
text: null,
files: null,
"file-replace": null,
embed: null,
"svg-text": null,
url: null,
tldraw: null,
excalidraw: null
});
/* --------------------- Events --------------------- */
/**
* The app's current input state.
*
* @public
*/
__publicField(this, "inputs", {
/** The most recent pointer down's position in the current page space. */
originPagePoint: new Vec(),
/** The most recent pointer down's position in screen space. */
originScreenPoint: new Vec(),
/** The previous pointer position in the current page space. */
previousPagePoint: new Vec(),
/** The previous pointer position in screen space. */
previousScreenPoint: new Vec(),
/** The most recent pointer position in the current page space. */
currentPagePoint: new Vec(),
/** The most recent pointer position in screen space. */
currentScreenPoint: new Vec(),
/** A set containing the currently pressed keys. */
keys: /* @__PURE__ */ new Set(),
/** A set containing the currently pressed buttons. */
buttons: /* @__PURE__ */ new Set(),
/** Whether the input is from a pe. */
isPen: false,
/** Whether the shift key is currently pressed. */
shiftKey: false,
/** Whether the meta key is currently pressed. */
metaKey: false,
/** Whether the control or command key is currently pressed. */
ctrlKey: false,
/** Whether the alt or option key is currently pressed. */
altKey: false,
/** Whether the user is dragging. */
isDragging: false,
/** Whether the user is pointing. */
isPointing: false,
/** Whether the user is pinching. */
isPinching: false,
/** Whether the user is editing. */
isEditing: false,
/** Whether the user is panning. */
isPanning: false,
/** Whether the user is spacebar panning. */
isSpacebarPanning: false,
/** Velocity of mouse pointer, in pixels per millisecond */
pointerVelocity: new Vec()
});
/**
* A manager for recording multiple click events.
*
* @internal
*/
__publicField(this, "_clickManager", new ClickManager(this));
/**
* The previous cursor. Used for restoring the cursor after pan events.
*
* @internal
*/
__publicField(this, "_prevCursor", "default");
/** @internal */
__publicField(this, "_shiftKeyTimeout", -1);
/** @internal */
__publicField(this, "_altKeyTimeout", -1);
/** @internal */
__publicField(this, "_ctrlKeyTimeout", -1);
/** @internal */
__publicField(this, "_metaKeyTimeout", -1);
/** @internal */
__publicField(this, "_restoreToolId", "select");
/** @internal */
__publicField(this, "_pinchStart", 1);
/** @internal */
__publicField(this, "_didPinch", false);
/** @internal */
__publicField(this, "_selectedShapeIdsAtPointerDown", []);
/** @internal */
__publicField(this, "_longPressTimeout", -1);
/** @internal */
__publicField(this, "capturedPointerId", null);
/** @internal */
__publicField(this, "performanceTracker");
/** @internal */
__publicField(this, "performanceTrackerTimeout", -1);
__publicField(this, "_pendingEventsForNextTick", []);
assert(
!(isShapeHidden && getShapeVisibility),
"Cannot use both isShapeHidden and getShapeVisibility"
);
this._getShapeVisibility = isShapeHidden ? (
// eslint-disable-next-line @typescript-eslint/no-deprecated
((shape, editor) => isShapeHidden(shape, editor) ? "hidden" : "inherit")
) : getShapeVisibility;
this.options = { ...defaultTldrawOptions, ...options };
this.store = store;
this.history = new HistoryManager({
store,
annotateError: (error) => {
this.annotateError(error, { origin: "history.batch", willCrashApp: true });
this.crash(error);
}
});
this.snaps = new SnapManager(this);
this.disposables.add(this.timers.dispose);
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions });
this._textOptions = atom("text options", textOptions ?? null);
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false);
this.disposables.add(() => this.user.dispose());
this.getContainer = getContainer;
this.textMeasure = new TextManager(this);
this.disposables.add(() => this.textMeasure.dispose());
this.fonts = new FontManager(this, fontAssetUrls);
this._tickManager = new TickManager(this);
class NewRoot extends RootState {
static initial = initialState ?? "";
}
this.root = new NewRoot(this);
this.root.children = {};
const allShapeUtils = checkShapesAndAddCore(shapeUtils);
const _shapeUtils = {};
const _styleProps = {};
const allStylesById = /* @__PURE__ */ new Map();
for (const Util of allShapeUtils) {
const util = new Util(this);
_shapeUtils[Util.type] = util;
const propKeysByStyle = getShapePropKeysByStyle(Util.props ?? {});
_styleProps[Util.type] = propKeysByStyle;
for (const style of propKeysByStyle.keys()) {
if (!allStylesById.has(style.id)) {
allStylesById.set(style.id, style);
} else if (allStylesById.get(style.id) !== style) {
throw Error(
`Multiple style props with id "${style.id}" in use. Style prop IDs must be unique.`
);
}
}
}
this.shapeUtils = _shapeUtils;
this.styleProps = _styleProps;
const allBindingUtils = checkBindings(bindingUtils);
const _bindingUtils = {};
for (const Util of allBindingUtils) {
const util = new Util(this);
_bindingUtils[Util.type] = util;
}
this.bindingUtils = _bindingUtils;
for (const Tool of [...tools]) {
if (hasOwnProperty(this.root.children, Tool.id)) {
throw Error(`Can't override tool with id "${Tool.id}"`);
}
this.root.children[Tool.id] = new Tool(this, this.root);
}
this.scribbles = new ScribbleManager(this);
const cleanupInstancePageState = (prevPageState, shapesNoLongerInPage) => {
let nextPageState = null;
const selectedShapeIds = prevPageState.selectedShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (selectedShapeIds.length !== prevPageState.selectedShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.selectedShapeIds = selectedShapeIds;
}
const erasingShapeIds = prevPageState.erasingShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (erasingShapeIds.length !== prevPageState.erasingShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.erasingShapeIds = erasingShapeIds;
}
if (prevPageState.hoveredShapeId && shapesNoLongerInPage.has(prevPageState.hoveredShapeId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.hoveredShapeId = null;
}
if (prevPageState.editingShapeId && shapesNoLongerInPage.has(prevPageState.editingShapeId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.editingShapeId = null;
}
const hintingShapeIds = prevPageState.hintingShapeIds.filter(
(id) => !shapesNoLongerInPage.has(id)
);
if (hintingShapeIds.length !== prevPageState.hintingShapeIds.length) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.hintingShapeIds = hintingShapeIds;
}
if (prevPageState.focusedGroupId && shapesNoLongerInPage.has(prevPageState.focusedGroupId)) {
if (!nextPageState) nextPageState = { ...prevPageState };
nextPageState.focusedGroupId = null;
}
return nextPageState;
};
this.sideEffects = this.store.sideEffects;
let deletedBindings = /* @__PURE__ */ new Map();
const deletedShapeIds = /* @__PURE__ */ new Set();
const invalidParents = /* @__PURE__ */ new Set();
let invalidBindingTypes = /* @__PURE__ */ new Set();
this.disposables.add(
this.sideEffects.registerOperationCompleteHandler(() => {
deletedShapeIds.clear();
for (const parentId of invalidParents) {
invalidParents.delete(parentId);
const parent = this.getShape(parentId);
if (!parent) continue;
const util = this.getShapeUtil(parent);
const changes = util.onChildrenChange?.(parent);
if (changes?.length) {
this.updateShapes(changes);
}
}
if (invalidBindingTypes.size) {
const t = invalidBindingTypes;
invalidBindingTypes = /* @__PURE__ */ new Set();
for (const type of t) {
const util = this.getBindingUtil(type);
util.onOperationComplete?.();
}
}
if (deletedBindings.size) {
const t = deletedBindings;
deletedBindings = /* @__PURE__ */ new Map();
for (const opts of t.values()) {
this.getBindingUtil(opts.binding).onAfterDelete?.(opts);
}
}
this.emit("update");
})
);
this.disposables.add(
this.sideEffects.register({
shape: {
afterChange: (shapeBefore, shapeAfter) => {
for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
invalidBindingTypes.add(binding.type);
if (binding.fromId === shapeAfter.id) {
this.getBindingUtil(binding).onAfterChangeFromShape?.({
binding,
shapeBefore,
shapeAfter,
reason: "self"
});
}
if (binding.toId === shapeAfter.id) {
this.getBindingUtil(binding).onAfterChangeToShape?.({
binding,
shapeBefore,
shapeAfter,
reason: "self"
});
}
}
if (shapeBefore.parentId !== shapeAfter.parentId) {
const notifyBindingAncestryChange = (id) => {
const descendantShape = this.getShape(id);
if (!descendantShape) return;
for (const binding of this.getBindingsInvolvingShape(descendantShape)) {
invalidBindingTypes.add(binding.type);
if (binding.fromId === descendantShape.id) {
this.getBindingUtil(binding).onAfterChangeFromShape?.({
binding,
shapeBefore: descendantShape,
shapeAfter: descendantShape,
reason: "ancestry"
});
}
if (binding.toId === descendantShape.id) {
this.getBindingUtil(binding).onAfterChangeToShape?.({
binding,
shapeBefore: descendantShape,
shapeAfter: descendantShape,
reason: "ancestry"
});
}
}
};
notifyBindingAncestryChange(shapeAfter.id);
this.visitDescendants(shapeAfter.id, notifyBindingAncestryChange);
}
if (shapeBefore.parentId !== shapeAfter.parentId && isPageId(shapeAfter.parentId)) {
const allMovingIds = /* @__PURE__ */ new Set([shapeBefore.id]);
this.visitDescendants(shapeBefore.id, (id) => {
allMovingIds.add(id);
});
for (const instancePageState of this.getPageStates()) {
if (instancePageState.pageId === shapeAfter.parentId) continue;
const nextPageState = cleanupInstancePageState(instancePageState, allMovingIds);
if (nextPageState) {
this.store.put([nextPageState]);
}
}
}
if (shapeBefore.parentId && isShapeId(shapeBefore.parentId)) {
invalidParents.add(shapeBefore.parentId);
}
if (shapeAfter.parentId !== shapeBefore.parentId && isShapeId(shapeAfter.parentId)) {
invalidParents.add(shapeAfter.parentId);
}
},
beforeDelete: (shape) => {
if (deletedShapeIds.has(shape.id)) return;
if (shape.parentId && isShapeId(shape.parentId)) {
invalidParents.add(shape.parentId);
}
deletedShapeIds.add(shape.id);
const deleteBindingIds = [];
for (const binding of this.getBindingsInvolvingShape(shape)) {
invalidBindingTypes.add(binding.type);
deleteBindingIds.push(binding.id);
const util = this.getBindingUtil(binding);
if (binding.fromId === shape.id) {
util.onBeforeIsolateToShape?.({ binding, removedShape: shape });
util.onBeforeDeleteFromShape?.({ binding, shape });
} else {
util.onBeforeIsolateFromShape?.({ binding, removedShape: shape });
util.onBeforeDeleteToShape?.({ binding, shape });
}
}
if (deleteBindingIds.length) {
this.deleteBindings(deleteBindingIds);
}
const deletedIds = /* @__PURE__ */ new Set([shape.id]);
const updates = compact(
this.getPageStates().map((pageState) => {
return cleanupInstancePageState(pageState, deletedIds);
})
);
if (updates.length) {
this.store.put(updates);
}
}
},
binding: {
beforeCreate: (binding) => {
const next = this.getBindingUtil(binding).onBeforeCreate?.({ binding });
if (next) return next;
return binding;
},
afterCreate: (binding) => {
invalidBindingTypes.add(binding.type);
this.getBindingUtil(binding).onAfterCreate?.({ binding });
},
beforeChange: (bindingBefore, bindingAfter) => {
const updated = this.getBindingUtil(bindingAfter).onBeforeChange?.({
bindingBefore,
bindingAfter
});
if (updated) return updated;
return bindingAfter;
},
afterChange: (bindingBefore, bindingAfter) => {
invalidBindingTypes.add(bindingAfter.type);
this.getBindingUtil(bindingAfter).onAfterChange?.({ bindingBefore, bindingAfter });
},
beforeDelete: (binding) => {
this.getBindingUtil(binding).onBeforeDelete?.({ binding });
},
afterDelete: (binding) => {
this.getBindingUtil(binding).onAfterDelete?.({ binding });
invalidBindingTypes.add(binding.type);
}
},
page: {
afterCreate: (record) => {
const cameraId = CameraRecordType.createId(record.id);
const _pageStateId = InstancePageStateRecordType.createId(record.id);
if (!this.store.has(cameraId)) {
this.store.put([CameraRecordType.create({ id: cameraId })]);
}
if (!this.store.has(_pageStateId)) {
this.store.put([
InstancePageStateRecordType.create({ id: _pageStateId, pageId: record.id })
]);
}
},
afterDelete: (record, source) => {
if (this.getInstanceState()?.currentPageId === record.id) {
const backupPageId = this.getPages().find((p) => p.id !== record.id)?.id;
if (backupPageId) {
this.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }]);
} else if (source === "user") {
this.store.ensureStoreIsUsable();
}
}
const cameraId = CameraRecordType.createId(record.id);
const instance_PageStateId = InstancePageStateRecordType.createId(record.id);
this.store.remove([cameraId, instance_PageStateId]);
}
},
instance: {
afterChange: (prev, next, source) => {
if (!this.store.has(next.currentPageId)) {
const backupPageId = this.store.has(prev.currentPageId) ? prev.currentPageId : this.getPages()[0]?.id;
if (backupPageId) {
this.store.update(next.id, (instance) => ({
...instance,
currentPageId: backupPageId
}));
} else if (source === "user") {
this.store.ensureStoreIsUsable();
}
}
}
},
instance_page_state: {
afterChange: (prev, next) => {
if (prev?.selectedShapeIds !== next?.selectedShapeIds) {
const filtered = next.selectedShapeIds.filter((id) => {
let parentId = this.getShape(id)?.parentId;
while (isShapeId(parentId)) {
if (next.selectedShapeIds.includes(parentId)) {
return false;
}
parentId = this.getShape(parentId)?.parentId;
}
return true;
});
let nextFocusedGroupId = null;
if (filtered.length > 0) {
const commonGroupAncestor = this.findCommonAncestor(
compact(filtered.map((id) => this.getShape(id))),
(shape) => this.isShapeOfType(shape, "group")
);
if (commonGroupAncestor) {
nextFocusedGroupId = commonGroupAncestor;
}
} else {
if (next?.focusedGroupId) {
nextFocusedGroupId = next.focusedGroupId;
}
}
if (filtered.length !== next.selectedShapeIds.length || nextFocusedGroupId !== next.focusedGroupId) {
this.store.put([
{
...next,
selectedShapeIds: filtered,
focusedGroupId: nextFocusedGroupId ?? null
}
]);
}
}
}
}
})
);
this._currentPageShapeIds = deriveShapeIdsInCurrentPage(
this.store,
() => this.getCurrentPageId()
);
this._parentIdsToChildIds = parentsToChildren(this.store);
this.disposables.add(
this.store.listen((changes) => {
this.emit("change", changes);
})
);
this.disposables.add(this.history.dispose);
this.run(
() => {
this.store.ensureStoreIsUsable();
this._updateCurrentPageState({
editingShapeId: null,
hoveredShapeId: null,
erasingShapeIds: []
});
},
{ history: "ignore" }
);
if (initialState && this.root.children[initialState] === void 0) {
throw Error(`No state found for initialState "${initialState}".`);
}
this.root.enter(void 0, "initial");
this.edgeScrollManager = new EdgeScrollManager(this);
this.focusManager = new FocusManager(this, autoFocus);
this.disposables.add(this.focusManager.dispose.bind(this.focusManager));
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser();
}
this.on("tick", this._flushEventsForTick);
this.timers.requestAnimationFrame(() => {
this._tickManager.start();
});
this.performanceTracker = new PerformanceTracker();
if (this.store.props.collaboration?.mode) {
const mode = this.store.props.collaboration.mode;
this.disposables.add(
react("update collaboration mode", () => {
this.store.put([{ ...this.getInstanceState(), isReadonly: mode.get() === "readonly" }]);
})
);
}
}
getIsShapeHiddenCache() {
if (!this._getShapeVisibility) return null;
return this.store.createComputedCache("isShapeHidden", (shape) => {
const visibility = this._getShapeVisibility(shape, this);
const isParentHidden = PageRecordType.isId(shape.parentId) ? false : this.isShapeHidden(shape.parentId);
if (isParentHidden) return visibility !== "visible";
return visibility === "hidden";
});
}
isShapeHidden(shapeOrId) {
if (!this._getShapeVisibility) return false;
return !!this.getIsShapeHiddenCache().get(
typeof shapeOrId === "string" ? shapeOrId : shapeOrId.id
);
}
/**
* Dispose the editor.
*
* @public
*/
dispose() {
this.disposables.forEach((dispose) => dispose());
this.disposables.clear();
this.store.dispose();
this.isDisposed = true;
}
getShapeUtil(arg) {
const type = typeof arg === "string" ? arg : arg.type;
const shapeUtil = getOwnProperty(this.shapeUtils, type);
assert(shapeUtil, `No shape util found for type "${type}"`);
return shapeUtil;
}
hasShapeUtil(arg) {
const type = typeof arg === "string" ? arg : arg.type;
return hasOwnProperty(this.shapeUtils, type);
}
getBindingUtil(arg) {
const type = typeof arg === "string" ? arg : arg.type;
const bindingUtil = getOwnProperty(this.bindingUtils, type);
assert(bindingUtil, `No binding util found for type "${type}"`);
return bindingUtil;
}
/**
* Undo to the last mark.
*
* @example
* ```ts
* editor.undo()
* ```
*
* @public
*/
undo() {
this._flushEventsForTick(0);
this.complete();
this.history.undo();
return this;
}
getCanUndo() {
return this.history.getNumUndos() > 0;
}
/**
* Redo to the next mark.
*
* @example
* ```ts
* editor.redo()
* ```
*
* @public
*/
redo() {
this._flushEventsForTick(0);
this.complete();
this.history.redo();
return this;
}
clearHistory() {
this.history.clear();
return this;
}
getCanRedo() {
return this.history.getNumRedos() > 0;
}
/**
* Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
* any redos.
*
* @example
* ```ts
* editor.mark()
* editor.mark('flip shapes')
* ```
*
* @param markId - The mark's id, usually the reason for adding the mark.
*
* @public
* @deprecated use {@link Editor.markHistoryStoppingPoint} instead
*/
mark(markId) {
if (typeof markId === "string") {
console.warn(
`[tldraw] \`editor.history.mark("${markId}")\` is deprecated. Please use \`const myMarkId = editor.markHistoryStoppingPoint()\` instead.`
);
} else {
console.warn(
"[tldraw] `editor.mark()` is deprecated. Use `editor.markHistoryStoppingPoint()` instead."
);
}
this.history._mark(markId ?? uniqueId());
return this;
}
/**
* Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear
* any redos. You typically want to do this just before a user interaction begins or is handled.
*
* @example
* ```ts
* editor.markHistoryStoppingPoint()
* editor.flipShapes(editor.getSelectedShapes())
* ```
* @example
* ```ts
* const beginRotateMark = editor.markHistoryStoppingPoint()
* // if the use cancels the rotation, you can bail back to this mark
* editor.bailToMark(beginRotateMark)
* ```
*
* @public
* @param name - The name of the mark, useful for debugging the undo/redo stacks
* @returns a unique id for the mark that can be used with `squashToMark` or `bailToMark`.
*/
markHistoryStoppingPoint(name) {
const id = `[${name ?? "stop"}]_${uniqueId()}`;
this.history._mark(id);
return id;
}
/**
* @internal this is only used to implement some backwards-compatibility logic. Should be fine to delete after 6 months or whatever.
*/
getMarkIdMatching(idSubstring) {
return this.history.getMarkIdMatching(idSubstring);
}
/**
* Coalesces all changes since the given mark into a single change, removing any intermediate marks.
*
* This is useful if you need to 'compress' the recent history to simplify the undo/redo experience of a complex interaction.
*
* @example
* ```ts
* const bumpShapesMark = editor.markHistoryStoppingPoint()
* // ... some changes
* editor.squashToMark(bumpShapesMark)
* ```
*
* @param markId - The mark id to squash to.
*/
squashToMark(markId) {
this.history.squashToMark(markId);
return this;
}
/**
* Undo to the closest mark, discarding the changes so they cannot be redone.
*
* @example
* ```ts
* editor.bail()
* ```
*
* @public
*/
bail() {
this.history.bail();
return this;
}
/**
* Undo to the given mark, discarding the changes so they cannot be redone.
*
* @example
* ```ts
* const beginDrag = editor.markHistoryStoppingPoint()
* // ... some changes
* editor.bailToMark(beginDrag)
* ```
*
* @public
*/
bailToMark(id) {
this.history.bailToMark(id);
return this;
}
/**
* Run a function in a transaction with optional options for context.
* You can use the options to change the way that history is treated
* or allow changes to locked shapes.
*
* @example
* ```ts
* // updating with
* editor.run(() => {
* editor.updateShape({ ...myShape, x: 100 })
* }, { history: "ignore" })
*
* // forcing changes / deletions for locked shapes
* editor.toggleLock([myShape])
* editor.run(() => {
* editor.updateShape({ ...myShape, x: 100 })
* editor.deleteShape(myShape)
* }, { ignoreShapeLock: true }, )
* ```
*
* @param fn - The callback function to run.
* @param opts - The options for the batch.
*
*
* @public
*/
run(fn, opts) {
const previousIgnoreShapeLock = this._shouldIgnoreShapeLock;
this._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock;
try {
this.history.batch(fn, opts);
} finally {
this._shouldIgnoreShapeLock = previousIgnoreShapeLock;
}
return this;
}
/**
* @deprecated Use `Editor.run` instead.
*/
batch(fn, opts) {
return this.run(fn, opts);
}
/* --------------------- Errors --------------------- */
/** @internal */
annotateError(error, {
origin,
willCrashApp,
tags,
extras
}) {
const defaultAnnotations = this.createErrorAnnotations(origin, willCrashApp);
annotateError(error, {
tags: { ...defaultAnnotations.tags, ...tags },
extras: { ...defaultAnnotations.extras, ...extras }
});
if (willCrashApp) {
this.store.markAsPossiblyCorrupted();
}
return this;
}
/** @internal */
createErrorAnnotations(origin, willCrashApp) {
try {
const editingShapeId = this.getEditingShapeId();
return {
tags: {
origin,
willCrashApp
},
extras: {
activeStateNode: this.root.getPath(),
selectedShapes: this.getSelectedShapes().map((s) => {
const { props, ...rest } = s;
const { text: _text, richText: _richText, ...restProps } = props;
return {
...rest,
props: restProps
};
}),
selectionCount: this.getSelectedShapes().length,
editingShape: editingShapeId ? this.getShape(editingShapeId) : void 0,
inputs: this.inputs,
pageState: this.getCurrentPageState(),
instanceState: this.getInstanceState(),
collaboratorCount: this.getCollaboratorsOnCurrentPage().length
}
};
} catch {
return {
tags: {
origin,
willCrashApp
},
extras: {}
};
}
}
/**
* We can't use an `atom` here because there's a chance that when `crashAndReportError` is called,
* we're in a transaction that's about to be rolled back due to the same error we're currently
* reporting.
*
* Instead, to listen to changes to this value, you need to listen to app's `crash` event.
*
* @internal
*/
getCrashingError() {
return this._crashingError;
}
/** @internal */
crash(error) {
this._crashingError = error;
this.store.markAsPossiblyCorrupted();
this.emit("crash", { error });
return this;
}
getPath() {
return this.root.getPath().split("root.")[1];
}
/**
* Get whether a certain tool (or other state node) is currently active.
*
* @example
* ```ts
* editor.isIn('select')
* editor.isIn('select.brushing')
* ```
*
* @param path - The path of active states, separated by periods.
*
* @public
*/
isIn(path) {
const ids = path.split(".").reverse();
let state = this.root;
while (ids.length > 0) {
const id = ids.pop();
if (!id) return true;
const current = state.getCurrent();
if (current?.id === id) {
if (ids.length === 0) return true;
state = current;
continue;
} else return false;
}
return false;
}
/**
* Get whether the state node is in any of the given active paths.
*
* @example
* ```ts
* state.isInAny('select', 'erase')
* state.isInAny('select.brushing', 'erase.idle')
* ```
*
* @public
*/
isInAny(...paths) {
return paths.some((path) => this.isIn(path));
}
/**
* Set the selected tool.
*
* @example
* ```ts
* editor.setCurrentTool('hand')
* editor.setCurrentTool('hand', { date: Date.now() })
* ```
*
* @param id - The id of the tool to select.
* @param info - Arbitrary data to pass along into the transition.
*
* @public
*/
setCurrentTool(id, info = {}) {
this.root.transition(id, info);
return this;
}
getCurrentTool() {
return this.root.getCurrent();
}
getCurrentToolId() {
const currentTool = this.getCurrentTool();
if (!currentTool) return "";
return currentTool.getCurrentToolIdMask() ?? currentTool.id;
}
/**
* Get a descendant by its path.
*
* @example
* ```ts
* editor.getStateDescendant('select')
* editor.getStateDescendant('select.brushing')
* ```
*
* @param path - The descendant's path of state ids, separated by periods.
*
* @public
*/
getStateDescendant(path) {
const ids = path.split(".").reverse();
let state = this.root;
while (ids.length > 0) {
const id = ids.pop();
if (!id) return state;
const childState = state.children?.[id];
if (!childState) return void 0;
state = childState;
}
return state;
}
getDocumentSettings() {
return this.store.get(TLDOCUMENT_ID);
}
/**
* Update the global document settings that apply to all users.
*
* @public
**/
updateDocumentSettings(settings) {
this.run(
() => {
this.store.put([{ ...this.getDocumentSettings(), ...settings }]);
},
{ history: "ignore" }
);
return this;
}
getInstanceState() {
return this.store.get(TLINSTANCE_ID);
}
/**
* Update the instance's state.
*
* @param partial - A partial object to