UNPKG

@tldraw/store

Version:

tldraw infinite canvas SDK (store).

369 lines (368 loc) • 12.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var StoreSideEffects_exports = {}; __export(StoreSideEffects_exports, { StoreSideEffects: () => StoreSideEffects }); module.exports = __toCommonJS(StoreSideEffects_exports); class StoreSideEffects { constructor(store) { this.store = store; } _beforeCreateHandlers = {}; _afterCreateHandlers = {}; _beforeChangeHandlers = {}; _afterChangeHandlers = {}; _beforeDeleteHandlers = {}; _afterDeleteHandlers = {}; _operationCompleteHandlers = []; _isEnabled = true; /** @internal */ isEnabled() { return this._isEnabled; } /** @internal */ setIsEnabled(enabled) { this._isEnabled = enabled; } /** @internal */ handleBeforeCreate(record, source) { if (!this._isEnabled) return record; const handlers = this._beforeCreateHandlers[record.typeName]; if (handlers) { let r = record; for (const handler of handlers) { r = handler(r, source); } return r; } return record; } /** @internal */ handleAfterCreate(record, source) { if (!this._isEnabled) return; const handlers = this._afterCreateHandlers[record.typeName]; if (handlers) { for (const handler of handlers) { handler(record, source); } } } /** @internal */ handleBeforeChange(prev, next, source) { if (!this._isEnabled) return next; const handlers = this._beforeChangeHandlers[next.typeName]; if (handlers) { let r = next; for (const handler of handlers) { r = handler(prev, r, source); } return r; } return next; } /** @internal */ handleAfterChange(prev, next, source) { if (!this._isEnabled) return; const handlers = this._afterChangeHandlers[next.typeName]; if (handlers) { for (const handler of handlers) { handler(prev, next, source); } } } /** @internal */ handleBeforeDelete(record, source) { if (!this._isEnabled) return true; const handlers = this._beforeDeleteHandlers[record.typeName]; if (handlers) { for (const handler of handlers) { if (handler(record, source) === false) { return false; } } } return true; } /** @internal */ handleAfterDelete(record, source) { if (!this._isEnabled) return; const handlers = this._afterDeleteHandlers[record.typeName]; if (handlers) { for (const handler of handlers) { handler(record, source); } } } /** @internal */ handleOperationComplete(source) { if (!this._isEnabled) return; for (const handler of this._operationCompleteHandlers) { handler(source); } } /** * Internal helper for registering a bunch of side effects at once and keeping them organized. * @internal */ register(handlersByType) { const disposes = []; for (const [type, handlers] of Object.entries(handlersByType)) { if (handlers?.beforeCreate) { disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate)); } if (handlers?.afterCreate) { disposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate)); } if (handlers?.beforeChange) { disposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange)); } if (handlers?.afterChange) { disposes.push(this.registerAfterChangeHandler(type, handlers.afterChange)); } if (handlers?.beforeDelete) { disposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete)); } if (handlers?.afterDelete) { disposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete)); } } return () => { for (const dispose of disposes) dispose(); }; } /** * Register a handler to be called before a record of a certain type is created. Return a * modified record from the handler to change the record that will be created. * * Use this handle only to modify the creation of the record itself. If you want to trigger a * side-effect on a different record (for example, moving one shape when another is created), * use {@link StoreSideEffects.registerAfterCreateHandler} instead. * * @example * ```ts * editor.sideEffects.registerBeforeCreateHandler('shape', (shape, source) => { * // only modify shapes created by the user * if (source !== 'user') return shape * * //by default, arrow shapes have no label. Let's make sure they always have a label. * if (shape.type === 'arrow') { * return {...shape, props: {...shape.props, text: 'an arrow'}} * } * * // other shapes get returned unmodified * return shape * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerBeforeCreateHandler(typeName, handler) { const handlers = this._beforeCreateHandlers[typeName]; if (!handlers) this._beforeCreateHandlers[typeName] = []; this._beforeCreateHandlers[typeName].push(handler); return () => remove(this._beforeCreateHandlers[typeName], handler); } /** * Register a handler to be called after a record is created. This is useful for side-effects * that would update _other_ records. If you want to modify the record being created use * {@link StoreSideEffects.registerBeforeCreateHandler} instead. * * @example * ```ts * editor.sideEffects.registerAfterCreateHandler('page', (page, source) => { * // Automatically create a shape when a page is created * editor.createShape<TLTextShape>({ * id: createShapeId(), * type: 'text', * props: { richText: toRichText(page.name) }, * }) * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerAfterCreateHandler(typeName, handler) { const handlers = this._afterCreateHandlers[typeName]; if (!handlers) this._afterCreateHandlers[typeName] = []; this._afterCreateHandlers[typeName].push(handler); return () => remove(this._afterCreateHandlers[typeName], handler); } /** * Register a handler to be called before a record is changed. The handler is given the old and * new record - you can return a modified record to apply a different update, or the old record * to block the update entirely. * * Use this handler only for intercepting updates to the record itself. If you want to update * other records in response to a change, use * {@link StoreSideEffects.registerAfterChangeHandler} instead. * * @example * ```ts * editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => { * if (next.isLocked && !prev.isLocked) { * // prevent shapes from ever being locked: * return prev * } * // other types of change are allowed * return next * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerBeforeChangeHandler(typeName, handler) { const handlers = this._beforeChangeHandlers[typeName]; if (!handlers) this._beforeChangeHandlers[typeName] = []; this._beforeChangeHandlers[typeName].push(handler); return () => remove(this._beforeChangeHandlers[typeName], handler); } /** * Register a handler to be called after a record is changed. This is useful for side-effects * that would update _other_ records - if you want to modify the record being changed, use * {@link StoreSideEffects.registerBeforeChangeHandler} instead. * * @example * ```ts * editor.sideEffects.registerAfterChangeHandler('shape', (prev, next, source) => { * if (next.props.color === 'red') { * // there can only be one red shape at a time: * const otherRedShapes = editor.getCurrentPageShapes().filter(s => s.props.color === 'red' && s.id !== next.id) * editor.updateShapes(otherRedShapes.map(s => ({...s, props: {...s.props, color: 'blue'}}))) * } * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerAfterChangeHandler(typeName, handler) { const handlers = this._afterChangeHandlers[typeName]; if (!handlers) this._afterChangeHandlers[typeName] = []; this._afterChangeHandlers[typeName].push(handler); return () => remove(this._afterChangeHandlers[typeName], handler); } /** * Register a handler to be called before a record is deleted. The handler can return `false` to * prevent the deletion. * * Use this handler only for intercepting deletions of the record itself. If you want to do * something to other records in response to a deletion, use * {@link StoreSideEffects.registerAfterDeleteHandler} instead. * * @example * ```ts * editor.sideEffects.registerBeforeDeleteHandler('shape', (shape, source) => { * if (shape.props.color === 'red') { * // prevent red shapes from being deleted * return false * } * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerBeforeDeleteHandler(typeName, handler) { const handlers = this._beforeDeleteHandlers[typeName]; if (!handlers) this._beforeDeleteHandlers[typeName] = []; this._beforeDeleteHandlers[typeName].push(handler); return () => remove(this._beforeDeleteHandlers[typeName], handler); } /** * Register a handler to be called after a record is deleted. This is useful for side-effects * that would update _other_ records - if you want to block the deletion of the record itself, * use {@link StoreSideEffects.registerBeforeDeleteHandler} instead. * * @example * ```ts * editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => { * // if the last shape in a frame is deleted, delete the frame too: * const parentFrame = editor.getShape(shape.parentId) * if (!parentFrame || parentFrame.type !== 'frame') return * * const siblings = editor.getSortedChildIdsForParent(parentFrame) * if (siblings.length === 0) { * editor.deleteShape(parentFrame.id) * } * }) * ``` * * @param typeName - The type of record to listen for * @param handler - The handler to call * * @returns A callback that removes the handler. */ registerAfterDeleteHandler(typeName, handler) { const handlers = this._afterDeleteHandlers[typeName]; if (!handlers) this._afterDeleteHandlers[typeName] = []; this._afterDeleteHandlers[typeName].push(handler); return () => remove(this._afterDeleteHandlers[typeName], handler); } /** * Register a handler to be called when a store completes an atomic operation. * * @example * ```ts * let count = 0 * * editor.sideEffects.registerOperationCompleteHandler(() => count++) * * editor.selectAll() * expect(count).toBe(1) * * editor.store.atomic(() => { * editor.selectNone() * editor.selectAll() * }) * * expect(count).toBe(2) * ``` * * @param handler - The handler to call * * @returns A callback that removes the handler. * * @public */ registerOperationCompleteHandler(handler) { this._operationCompleteHandlers.push(handler); return () => remove(this._operationCompleteHandlers, handler); } } function remove(array, item) { const index = array.indexOf(item); if (index >= 0) { array.splice(index, 1); } } //# sourceMappingURL=StoreSideEffects.js.map