UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

1,281 lines • 277 kB
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