UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

1,507 lines 294 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 __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 __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; 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"), 1); var import_createTLCurrentUser = require("../config/createTLCurrentUser"); var import_defaultAssets = require("../config/defaultAssets"); var import_defaultBindings = require("../config/defaultBindings"); var import_defaultShapes = require("../config/defaultShapes"); var import_TLEditorSnapshot = require("../config/TLEditorSnapshot"); var import_constants = require("../constants"); var import_domUtils = require("../exports/domUtils"); var import_exportToSvg = require("../exports/exportToSvg"); var import_getSvgAsImage = require("../exports/getSvgAsImage"); 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_easings = require("../primitives/easings"); var import_Group2d = require("../primitives/geometry/Group2d"); var import_intersect = require("../primitives/intersect"); var import_Mat = require("../primitives/Mat"); var import_utils2 = require("../primitives/utils"); var import_Vec = require("../primitives/Vec"); 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_reorderShapes = require("../utils/reorderShapes"); var import_reparenting = require("../utils/reparenting"); var import_rotation = require("../utils/rotation"); var import_SharedStylesMap = require("../utils/SharedStylesMap"); 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_CollaboratorsManager = require("./managers/CollaboratorsManager/CollaboratorsManager"); 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_InputsManager = require("./managers/InputsManager/InputsManager"); var import_PerformanceManager = require("./managers/PerformanceManager/PerformanceManager"); var import_ScribbleManager = require("./managers/ScribbleManager/ScribbleManager"); var import_SnapManager = require("./managers/SnapManager/SnapManager"); var import_SpatialIndexManager = require("./managers/SpatialIndexManager/SpatialIndexManager"); var import_TextManager = require("./managers/TextManager/TextManager"); var import_ThemeManager = require("./managers/ThemeManager/ThemeManager"); var import_TickManager = require("./managers/TickManager/TickManager"); var import_UserPreferencesManager = require("./managers/UserPreferencesManager/UserPreferencesManager"); var import_OverlayManager = require("./overlays/OverlayManager"); var import_RootState = require("./tools/RootState"); class Editor extends import_eventemitter3.default { id = (0, import_utils.uniqueId)(); constructor({ store, user, shapeUtils, bindingUtils, assetUtils: assetUtilConstructors, overlayUtils: overlayUtilConstructors, tools, getContainer, // needs to be here for backwards compatibility with TldrawEditor // eslint-disable-next-line @typescript-eslint/no-deprecated cameraOptions, initialState, autoFocus, options: _options, // needs to be here for backwards compatibility with TldrawEditor // eslint-disable-next-line @typescript-eslint/no-deprecated textOptions: _textOptions, getShapeVisibility, colorScheme, fontAssetUrls, themes, initialTheme }) { super(); this._getShapeVisibility = getShapeVisibility; const options = _textOptions ? { ..._options, text: _options?.text ?? _textOptions } : _options; 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._spatialIndex = new import_SpatialIndexManager.SpatialIndexManager(this); this.disposables.add(() => this._spatialIndex.dispose()); this.disposables.add(this.timers.dispose); this._cameraOptions.set({ ...import_constants.DEFAULT_CAMERA_OPTIONS, ...cameraOptions, ...options?.camera }); this.getContainer = getContainer; this._textOptions = (0, import_state.atom)("text options", options?.text ?? null); this.user = new import_UserPreferencesManager.UserPreferencesManager(user ?? (0, import_createTLCurrentUser.createTLCurrentUser)(), colorScheme ?? "light"); this.disposables.add(() => this.user.dispose()); this.textMeasure = new import_TextManager.TextManager(this); this.disposables.add(() => this.textMeasure.dispose()); this._themeManager = new import_ThemeManager.ThemeManager(this, { themes: (0, import_ThemeManager.resolveThemes)(themes), initial: initialTheme ?? "default" }); this.disposables.add(() => this._themeManager.dispose()); this._tickManager = new import_TickManager.TickManager(this); this.disposables.add(() => this._tickManager.dispose()); this.disposables.add(() => { this.off("tick", this._decayCameraStateTimeout); this._setCameraState("idle"); }); this.fonts = new import_FontManager.FontManager(this, fontAssetUrls); this.inputs = new import_InputsManager.InputsManager(this); this.performance = new import_PerformanceManager.PerformanceManager(this); this.disposables.add(() => this.performance.dispose()); this.collaborators = new import_CollaboratorsManager.CollaboratorsManager(this); class NewRoot extends import_RootState.RootState { static initial = initialState ?? ""; } this.root = new NewRoot(this); this.root.children = {}; this.markEventAsHandled = this.markEventAsHandled.bind(this); 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 _shapeUtilsByAssetType = {}; for (const Util of allShapeUtils) { const assetTypes = Util.handledAssetTypes; if (assetTypes) { for (const assetType of assetTypes) { _shapeUtilsByAssetType[assetType] = _shapeUtils[Util.type]; } } } this._shapeUtilsByAssetType = _shapeUtilsByAssetType; 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; if (assetUtilConstructors) { const allAssetUtils = (0, import_defaultAssets.checkAssets)(assetUtilConstructors); const _assetUtils = {}; for (const Util of allAssetUtils) { const util = new Util(this); _assetUtils[Util.type] = util; } this.assetUtils = _assetUtils; } 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); this.overlays = new import_OverlayManager.OverlayManager(this); if (overlayUtilConstructors) { for (const Util of overlayUtilConstructors) { const util = new Util(this); this.overlays.registerUtil(util); } } 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" }]); }) ); } this.disposables.add( (0, import_state.react)("sync current user record", () => { const user2 = this.store.props.users.currentUser.get(); if (user2) { this._ensureUserRecord(user2); } }) ); } _getShapeVisibility; 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 ); } options; contextId = (0, import_utils.uniqueId)(); /** * The editor's store * * @public */ store; /** * The root state of the statechart. * * @public */ root; /** * Set a tool. Useful if you need to add a tool to the state chart on demand, * after the editor has already been initialized. * * @param Tool - The tool to set. * @param parent - The parent state node to set the tool on. * * @public */ setTool(Tool, parent) { parent ??= this.root; if ((0, import_utils.hasOwnProperty)(parent.children, Tool.id)) { throw Error(`Can't override tool with id "${Tool.id}"`); } parent.children[Tool.id] = new Tool(this, parent); } /** * Remove a tool. Useful if you need to remove a tool from the state chart on demand, * after the editor has already been initialized. * * @param Tool - The tool to delete. * @param parent - The parent state node to remove the tool from. * * @public */ removeTool(Tool, parent) { parent ??= this.root; if ((0, import_utils.hasOwnProperty)(parent.children, Tool.id)) { delete parent.children[Tool.id]; } } /** * A set of functions to call when the editor is disposed. * * @public */ disposables = /* @__PURE__ */ new Set(); /** * Whether the editor is disposed. * * @public */ isDisposed = false; /** * A manager for the editor's tick events. * * @internal */ _tickManager; /** * A manager for the editor's input state. * * @public */ inputs; /** * A manager for the editor's snapping feature. * * @public */ snaps; /** * A manager for performance measurement hooks. * * @public */ performance; /** * A manager for the spatial index, tracking where shapes exist on the canvas. * * @internal */ _spatialIndex; /** * A manager for the any asynchronous events and making sure they're * cleaned up upon disposal. * * @public */ timers = import_time.tltime.forContext(this.contextId); /** * A manager for remote peer collaborators connected to this editor. * * @public */ collaborators; /** * A manager for the user and their preferences. * * @public */ user; /** * A manager for the editor's themes. * * @internal */ _themeManager; /** * A helper for measuring text. * * @public */ textMeasure; /** * A utility for managing the set of fonts that should be rendered in the document. * * @public */ fonts; /** * A manager for the editor's scribbles. * * @public */ scribbles; /** * A manager for canvas overlay UI elements (selection handles, shape handles, etc.). * * @public */ overlays; /** * A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details. * * @public */ sideEffects; /** * A manager for moving the camera when the mouse is at the edge of the screen. * * @public */ edgeScrollManager; /** * A manager for ensuring correct focus. See FocusManager for details. * * @internal */ focusManager; /** * The current HTML element containing the editor. * * @example * ```ts * const container = editor.getContainer() * ``` * * @public */ getContainer; /** * The document that the editor's container element belongs to. * Use this instead of the global `document` to support cross-window embedding. * * @internal */ getContainerDocument() { return this.getContainer().ownerDocument; } /** * The window that the editor's container element belongs to. * Use this instead of the global `window` to support cross-window embedding. * * @internal */ getContainerWindow() { return (0, import_domUtils.getOwnerWindow)(this.getContainer()); } /** * Dispose the editor. * * @public */ dispose() { this.stopCameraAnimation(); if (this.getInstanceState().followingUserId) { this.stopFollowingUser(); } this.disposables.forEach((dispose) => dispose()); this.disposables.clear(); this.menus.clearOpenMenus(); this.store.dispose(); this.isDisposed = true; this.emit("dispose"); } /* ------------------ Themes (shadowing the theme manager) ------------------ */ /** * Get the current color mode (`'light'` or `'dark'`), based on the user's dark mode preference. * * @public */ getColorMode() { return this._themeManager.getColorMode(); } /** * Set the color mode. Note that this is a convenience method that passes the mode to * `user.updateUserPreferences`, which is the source of truth for the user's color mode preference. * * @public */ setColorMode(mode) { this.user.updateUserPreferences({ colorScheme: mode }); return this; } /** * Get the id of the current theme. * * @public */ getCurrentThemeId() { return this._themeManager.getCurrentThemeId(); } /** * Get the current theme definition. * * @public */ getCurrentTheme() { return this._themeManager.getCurrentTheme(); } /** * Set the current theme by id. * * @public */ setCurrentTheme(id) { this._themeManager.setCurrentTheme(id); return this; } /** * Get all registered theme definitions. * * @public */ getThemes() { return this._themeManager.getThemes(); } /** * Get a single theme definition by id. * * @public */ getTheme(id) { return this._themeManager.getTheme(id); } /** * Replace all theme definitions, or update them via a callback that receives a deep copy. * The `'default'` theme must always be present in the result. * * @example * ```ts * // Replace all themes * editor.updateThemes({ default: myDefaultTheme, ocean: myOceanTheme }) * * // Update via callback * editor.updateThemes((themes) => { * delete themes.ocean * return themes * }) * ``` * * @public */ updateThemes(themes) { this._themeManager.updateThemes(themes); return this; } /** * Register or update a single theme definition. The theme is keyed by its `id` property. * * @example * ```ts * // Override a property on the default theme * editor.updateTheme({ ...editor.getTheme('default')!, fontSize: 24 }) * * // Register a new theme * editor.updateTheme({ id: 'ocean', ...myOceanTheme }) * ``` * * @public */ updateTheme(theme) { this._themeManager.updateTheme(theme); return this; } /* ------------------- Shape Utils ------------------ */ /** * A map of shape utility classes (TLShapeUtils) by shape type. * * @public */ shapeUtils; /** @internal */ _shapeUtilsByAssetType = {}; styleProps; 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); } /** * Get the shape util that handles the given asset type. * Returns the shape util whose {@link ShapeUtil.handledAssetTypes} includes * the given asset type, or undefined if none matches. * * @param assetType - The asset type string. * @public */ getShapeUtilForAssetType(assetType) { return (0, import_utils.getOwnProperty)(this._shapeUtilsByAssetType, assetType); } /* ------------------- Binding Utils ------------------ */ /** * A map of shape utility classes (TLShapeUtils) by shape type. * * @public */ bindingUtils; 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; } /* ------------------- Asset Utils ------------------ */ /** * A map of asset utility classes by asset type. * * @public */ assetUtils = {}; getAssetUtil(arg) { const type = typeof arg === "string" ? arg : arg.type; const assetUtil = (0, import_utils.getOwnProperty)(this.assetUtils, type); (0, import_utils.assert)(assetUtil, `No asset util found for type "${type}"`); return assetUtil; } /** * Returns true if the editor has an asset util for the given asset type. * * @public */ hasAssetUtil(arg) { const type = typeof arg === "string" ? arg : arg.type; return (0, import_utils.hasOwnProperty)(this.assetUtils, type); } /** * Get the asset util that accepts the given MIME type. * Returns null if no registered asset util accepts the MIME type. * * @public */ getAssetUtilForMimeType(mimeType) { for (const util of Object.values(this.assetUtils)) { if (util && util.acceptsMimeType(mimeType)) { return util; } } return null; } /* --------------------- History -------------------- */ /** * A manager for the editor's history. * * @readonly */ history; /** * Undo to the last mark. * * @example * ```ts * editor.undo() * ``` * * @public */ undo() { this._flushEventsForTick(0); this.complete(); this.history.undo(); this.performance._notifyUndoRedo("undo", this.history.getNumUndos(), this.history.getNumRedos()); return this; } canUndo() { return this.history.getNumUndos() > 0; } getCanUndo() { return this.canUndo(); } /** * Redo to the next mark. * * @example * ```ts * editor.redo() * ``` * * @public */ redo() { this._flushEventsForTick(0); this.complete(); this.history.redo(); this.performance._notifyUndoRedo("redo", this.history.getNumUndos(), this.history.getNumRedos()); return this; } canRedo() { return this.history.getNumRedos() > 0; } getCanRedo() { return this.canRedo(); } clearHistory() { this.history.clear(); 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; } _shouldIgnoreShapeLock = false; /** * 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; } /* --------------------- 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.toJson(), pageState: this.getCurrentPageState(), instanceState: this.getInstanceState(), collaboratorCount: this.getCollaboratorsOnCurrentPage().length } }; } catch { return { tags: { origin, willCrashApp }, extras: {} }; } } /** @internal */ _crashingError = null; /** * 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 editor'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(import_tlschema.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(import_tlschema.TLINSTANCE_ID); } /** * Update the instance's state. * * @param partial - A partial object to update the instance state with. * @param historyOptions - History batch options. * * @public */ updateInstanceState(partial, historyOptions) { this._updateInstanceState(partial, { history: "ignore", ...historyOptions }); if (partial.isChangingStyle !== void 0) { clearTimeout(this._isChangingStyleTimeout); if (partial.isChangingStyle === true) { this._isChangingStyleTimeout = this.timers.setTimeout(() => { this._updateInstanceState({ isChangingStyle: false }, { history: "ignore" }); }, 1e3); } } return this; } /** @internal */ _updateInstanceState(partial, opts) { this.run(() => { this.store.put([ { ...this.getInstanceState(), ...partial } ]); }, opts); } /** @internal */ _isChangingStyleTimeout = -1; // Menus menus = import_menus.tlmenus.forContext(this.contextId); /* --------------------- Cursor --------------------- */ /** * Set the cursor. * * No-op when the partial wouldn't change the current cursor — `setCursor` * is called from pointer-move hot paths (see `updateHoveredOverlayId`, * various tool states) and skipping redundant writes avoids needlessly * dirtying instance state. * * @param cursor - The cursor to set. * @public */ setCursor(cursor) { const current = this.getInstanceState().cursor; if ((cursor.type === void 0 || cursor.type === current.type) && (cursor.rotation === void 0 || cursor.rotation === current.rotation)) { return this; } this.updateInstanceState({ cursor: { ...current, ...cursor } }); return this; } getPageStates() { return this._getPageStatesQuery().get(); } _getPageStatesQuery() { return this.store.query.records("instance_page_state"); } getCurrentPageState() { return this.store.get(this._getCurrentPageStateId()); } _getCurrentPageStateId() { return import_tlschema.InstancePageStateRecordType.createId(this.getCurrentPageId()); } /** * Update this instance's page state. * * @example * ```ts * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }) * ``` * * @param partial - The partial of the page state object containing the changes. * * @public */ updateCurrentPageState(partial) { this._updateCurrentPageState(partial); return this; } _updateCurrentPageState(partial) { this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({ ...state, ...partial })); } getSelectedShapeIds() { return this.getCurrentPageState().selectedShapeIds; } getSelectedShapes() { return (0, import_utils.compact)(this.getSelectedShapeIds().map((id) => this.store.get(id))); } /** * Select one or more shapes. * * @example * ```ts * editor.setSelectedShapes(['id1']) * editor.setSelectedShapes(['id1', 'id2']) * ``` * * @param shapes - The shape (or shape ids) to select. * * @public */ setSelectedShapes(shapes) { return this.run( () => { const ids = shapes.map((shape) => typeof shape === "string" ? shape : shape.id); const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState(); const prevSet = new Set(prevSelectedShapeIds); if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null; this.store.put([{ ...this.getCurrentPageState(), selectedShapeIds: ids }]); }, { history: "record-preserveRedoStack" } ); } /** * Determine whether or not any of a shape's ancestors are selected. * * @param shape - The shape (or shape id) of the shape to check. * * @public */ isAncestorSelected(shape) { const id = typeof shape === "string" ? shape : shape?.id ?? null; const _shape = this.getShape(id); if (!_shape) return false; const selectedShapeIds = this.getSelectedShapeIds(); return !!this.findShapeAncestor(_shape, (parent) => selectedShapeIds.includes(parent.id)); } /** * Select one or more shapes. * * @example * ```ts * editor.select('id1') * editor.select('id1', 'id2') * ``` * * @param shapes - The shape (or the shape ids) to select. * * @public */ select(...shapes) { const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id); this.setSelectedShapes(ids); return this; } /** * Remove a shape from the existing set of selected shapes. * * @example * ```ts * editor.deselect(shape.id) * ``` * * @public */ deselect(...shapes) { const ids = typeof shapes[0] === "string" ? shapes : shapes.map((shape) => shape.id); const selectedShapeIds = this.getSelectedShapeIds(); if (selectedShapeIds.length > 0 && ids.length > 0) { this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id))); } return this; } /** * Select all shapes. If the user has selected shapes that share a parent, * select all shapes within that parent. If the user has not selected any