UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

1,134 lines • 284 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; 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 __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 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 Editor_exports = {}; __export(Editor_exports, { Editor: () => Editor }); module.exports = __toCommonJS(Editor_exports); var import_state = require("@tldraw/state"); var import_store = require("@tldraw/store"); var import_tlschema = require("@tldraw/tlschema"); var import_utils = require("@tldraw/utils"); var import_eventemitter3 = __toESM(require("eventemitter3")); var import_TLEditorSnapshot = require("../config/TLEditorSnapshot"); var import_createTLUser = require("../config/createTLUser"); var import_defaultBindings = require("../config/defaultBindings"); var import_defaultShapes = require("../config/defaultShapes"); var import_constants = require("../constants"); var import_exportToSvg = require("../exports/exportToSvg"); var import_getSvgAsImage = require("../exports/getSvgAsImage"); var import_environment = require("../globals/environment"); var import_menus = require("../globals/menus"); var import_time = require("../globals/time"); var import_options = require("../options"); var import_Box = require("../primitives/Box"); var import_Mat = require("../primitives/Mat"); var import_Vec = require("../primitives/Vec"); var import_easings = require("../primitives/easings"); var import_Group2d = require("../primitives/geometry/Group2d"); var import_intersect = require("../primitives/intersect"); var import_utils2 = require("../primitives/utils"); var import_SharedStylesMap = require("../utils/SharedStylesMap"); var import_areShapesContentEqual = require("../utils/areShapesContentEqual"); var import_assets = require("../utils/assets"); var import_debug_flags = require("../utils/debug-flags"); var import_deepLinks = require("../utils/deepLinks"); var import_getIncrementedName = require("../utils/getIncrementedName"); var import_keyboard = require("../utils/keyboard"); var import_reorderShapes = require("../utils/reorderShapes"); var import_rotation = require("../utils/rotation"); var import_bindingsIndex = require("./derivations/bindingsIndex"); var import_notVisibleShapes = require("./derivations/notVisibleShapes"); var import_parentsToChildren = require("./derivations/parentsToChildren"); var import_shapeIdsInCurrentPage = require("./derivations/shapeIdsInCurrentPage"); var import_ClickManager = require("./managers/ClickManager/ClickManager"); var import_EdgeScrollManager = require("./managers/EdgeScrollManager/EdgeScrollManager"); var import_FocusManager = require("./managers/FocusManager/FocusManager"); var import_FontManager = require("./managers/FontManager/FontManager"); var import_HistoryManager = require("./managers/HistoryManager/HistoryManager"); var import_ScribbleManager = require("./managers/ScribbleManager/ScribbleManager"); var import_SnapManager = require("./managers/SnapManager/SnapManager"); var import_TextManager = require("./managers/TextManager/TextManager"); var import_TickManager = require("./managers/TickManager/TickManager"); var import_UserPreferencesManager = require("./managers/UserPreferencesManager/UserPreferencesManager"); var import_RootState = require("./tools/RootState"); 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; class Editor extends (_a = import_eventemitter3.default, _getIsShapeHiddenCache_dec = [import_state.computed], _getCanUndo_dec = [import_state.computed], _getCanRedo_dec = [import_state.computed], _getPath_dec = [import_state.computed], _getCurrentTool_dec = [import_state.computed], _getCurrentToolId_dec = [import_state.computed], _getDocumentSettings_dec = [import_state.computed], _getInstanceState_dec = [import_state.computed], _getOpenMenus_dec = [import_state.computed], _getIsMenuOpen_dec = [import_state.computed], _getPageStates_dec = [import_state.computed], __getPageStatesQuery_dec = [import_state.computed], _getCurrentPageState_dec = [import_state.computed], __getCurrentPageStateId_dec = [import_state.computed], _getSelectedShapeIds_dec = [import_state.computed], _getSelectedShapes_dec = [import_state.computed], _getCurrentPageShapesInReadingOrder_dec = [import_state.computed], _getOnlySelectedShapeId_dec = [import_state.computed], _getOnlySelectedShape_dec = [import_state.computed], _getSelectionPageBounds_dec = [import_state.computed], _getSelectionRotation_dec = [import_state.computed], _getSelectionRotatedPageBounds_dec = [import_state.computed], _getSelectionRotatedScreenBounds_dec = [import_state.computed], _getFocusedGroupId_dec = [import_state.computed], _getFocusedGroup_dec = [import_state.computed], _getEditingShapeId_dec = [import_state.computed], _getEditingShape_dec = [import_state.computed], _getRichTextEditor_dec = [import_state.computed], _getHoveredShapeId_dec = [import_state.computed], _getHoveredShape_dec = [import_state.computed], _getHintingShapeIds_dec = [import_state.computed], _getHintingShape_dec = [import_state.computed], _getErasingShapeIds_dec = [import_state.computed], _getErasingShapes_dec = [import_state.computed], __unsafe_getCameraId_dec = [import_state.computed], _getCamera_dec = [import_state.computed], _getViewportPageBoundsForFollowing_dec = [import_state.computed], _getCameraForFollowing_dec = [import_state.computed], _getZoomLevel_dec = [import_state.computed], _getViewportScreenBounds_dec = [import_state.computed], _getViewportScreenCenter_dec = [import_state.computed], _getViewportPageBounds_dec = [import_state.computed], __getCollaboratorsQuery_dec = [import_state.computed], _getCollaborators_dec = [import_state.computed], _getCollaboratorsOnCurrentPage_dec = [import_state.computed], _getRenderingShapes_dec = [import_state.computed], __getAllPagesQuery_dec = [import_state.computed], _getPages_dec = [import_state.computed], _getCurrentPageId_dec = [import_state.computed], _getCurrentPageShapeIdsSorted_dec = [import_state.computed], __getAllAssetsQuery_dec = [import_state.computed], __getShapeHandlesCache_dec = [import_state.computed], __getShapePageTransformCache_dec = [import_state.computed], __getShapePageBoundsCache_dec = [import_state.computed], __getShapeClipPathCache_dec = [import_state.computed], __getShapeMaskCache_dec = [import_state.computed], __getShapeMaskedPageBoundsCache_dec = [import_state.computed], _getNotVisibleShapes_dec = [import_state.computed], _getCulledShapes_dec = [import_state.computed], _getCurrentPageBounds_dec = [import_state.computed], _getCurrentPageShapes_dec = [import_state.computed], _getCurrentPageShapesSorted_dec = [import_state.computed], _getCurrentPageRenderingShapesSorted_dec = [import_state.computed], __getBindingsIndexCache_dec = [import_state.computed], __getSelectionSharedStyles_dec = [import_state.computed], _getSharedStyles_dec = [(0, import_state.computed)({ isEqual: (a, b) => a.equals(b) })], _getSharedOpacity_dec = [import_state.computed], _getIsFocused_dec = [import_state.computed], _getIsReadonly_dec = [import_state.computed], __setShiftKeyTimeout_dec = [import_utils.bind], __setAltKeyTimeout_dec = [import_utils.bind], __setCtrlKeyTimeout_dec = [import_utils.bind], __setMetaKeyTimeout_dec = [import_utils.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", (0, import_utils.uniqueId)()); __publicField(this, "_getShapeVisibility"); __publicField(this, "options"); __publicField(this, "contextId", (0, import_utils.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", import_time.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", import_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", import_menus.tlmenus.forContext(this.contextId)); // Rich text editor __publicField(this, "_currentRichTextEditor", (0, import_state.atom)("rich text editor", null)); __publicField(this, "_textOptions"); __publicField(this, "_cameraOptions", (0, import_state.atom)("camera options", import_constants.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", (0, import_state.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", (0, import_state.atom)("camera state", "idle")); __publicField(this, "_cameraStateTimeoutRemaining", 0); /* @internal */ __publicField(this, "_currentPageShapeIds"); /* --------------------- Shapes --------------------- */ __publicField(this, "_shapeGeometryCaches", {}); __publicField(this, "_notVisibleShapes", (0, import_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 import_Vec.Vec(), /** The most recent pointer down's position in screen space. */ originScreenPoint: new import_Vec.Vec(), /** The previous pointer position in the current page space. */ previousPagePoint: new import_Vec.Vec(), /** The previous pointer position in screen space. */ previousScreenPoint: new import_Vec.Vec(), /** The most recent pointer position in the current page space. */ currentPagePoint: new import_Vec.Vec(), /** The most recent pointer position in screen space. */ currentScreenPoint: new import_Vec.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 import_Vec.Vec() }); /** * A manager for recording multiple click events. * * @internal */ __publicField(this, "_clickManager", new import_ClickManager.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", []); (0, import_utils.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 = { ...import_options.defaultTldrawOptions, ...options }; this.store = store; this.history = new import_HistoryManager.HistoryManager({ store, annotateError: (error) => { this.annotateError(error, { origin: "history.batch", willCrashApp: true }); this.crash(error); } }); this.snaps = new import_SnapManager.SnapManager(this); this.disposables.add(this.timers.dispose); this._cameraOptions.set({ ...import_constants.DEFAULT_CAMERA_OPTIONS, ...cameraOptions }); this._textOptions = (0, import_state.atom)("text options", textOptions ?? null); this.user = new import_UserPreferencesManager.UserPreferencesManager(user ?? (0, import_createTLUser.createTLUser)(), inferDarkMode ?? false); this.disposables.add(() => this.user.dispose()); this.getContainer = getContainer; this.textMeasure = new import_TextManager.TextManager(this); this.disposables.add(() => this.textMeasure.dispose()); this.fonts = new import_FontManager.FontManager(this, fontAssetUrls); this._tickManager = new import_TickManager.TickManager(this); class NewRoot extends import_RootState.RootState { static initial = initialState ?? ""; } this.root = new NewRoot(this); this.root.children = {}; const allShapeUtils = (0, import_defaultShapes.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 = (0, import_tlschema.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 = (0, import_defaultBindings.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 ((0, import_utils.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 import_ScribbleManager.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 && (0, import_tlschema.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 && (0, import_tlschema.isShapeId)(shapeBefore.parentId)) { invalidParents.add(shapeBefore.parentId); } if (shapeAfter.parentId !== shapeBefore.parentId && (0, import_tlschema.isShapeId)(shapeAfter.parentId)) { invalidParents.add(shapeAfter.parentId); } }, beforeDelete: (shape) => { if (deletedShapeIds.has(shape.id)) return; if (shape.parentId && (0, import_tlschema.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 = (0, import_utils.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 = import_tlschema.CameraRecordType.createId(record.id); const _pageStateId = import_tlschema.InstancePageStateRecordType.createId(record.id); if (!this.store.has(cameraId)) { this.store.put([import_tlschema.CameraRecordType.create({ id: cameraId })]); } if (!this.store.has(_pageStateId)) { this.store.put([ import_tlschema.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 = import_tlschema.CameraRecordType.createId(record.id); const instance_PageStateId = import_tlschema.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 ((0, import_tlschema.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( (0, import_utils.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 = (0, import_shapeIdsInCurrentPage.deriveShapeIdsInCurrentPage)( this.store, () => this.getCurrentPageId() ); this._parentIdsToChildIds = (0, import_parentsToChildren.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 import_EdgeScrollManager.EdgeScrollManager(this); this.focusManager = new import_FocusManager.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 import_utils.PerformanceTracker(); if (this.store.props.collaboration?.mode) { const mode = this.store.props.collaboration.mode; this.disposables.add( (0, import_state.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 = import_tlschema.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 = (0, import_utils.getOwnProperty)(this.shapeUtils, type); (0, import_utils.assert)(shapeUtil, `No shape util found for type "${type}"`); return shapeUtil; } hasShapeUtil(arg) { const type = typeof arg === "string" ? arg : arg.type; return (0, import_utils.hasOwnProperty)(this.shapeUtils, type); } getBindingUtil(arg) { const type = typeof arg === "string" ? arg : arg.type; const bindingUtil = (0, import_utils.getOwnProperty)(this.bindingUtils, type); (0, import_utils.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 ?? (0, import_utils.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"}]_${(0, import_utils.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); (0, import_utils.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