kopia lustrzana https://github.com/Tldraw/Tldraw
415 wiersze
13 KiB
TypeScript
415 wiersze
13 KiB
TypeScript
import { Store, UnknownRecord } from '@tldraw/store'
|
|
|
|
/** @public */
|
|
export type TLBeforeCreateHandler<R extends UnknownRecord> = (
|
|
record: R,
|
|
source: 'remote' | 'user'
|
|
) => R
|
|
/** @public */
|
|
export type TLAfterCreateHandler<R extends UnknownRecord> = (
|
|
record: R,
|
|
source: 'remote' | 'user'
|
|
) => void
|
|
/** @public */
|
|
export type TLBeforeChangeHandler<R extends UnknownRecord> = (
|
|
prev: R,
|
|
next: R,
|
|
source: 'remote' | 'user'
|
|
) => R
|
|
/** @public */
|
|
export type TLAfterChangeHandler<R extends UnknownRecord> = (
|
|
prev: R,
|
|
next: R,
|
|
source: 'remote' | 'user'
|
|
) => void
|
|
/** @public */
|
|
export type TLBeforeDeleteHandler<R extends UnknownRecord> = (
|
|
record: R,
|
|
source: 'remote' | 'user'
|
|
) => void | false
|
|
/** @public */
|
|
export type TLAfterDeleteHandler<R extends UnknownRecord> = (
|
|
record: R,
|
|
source: 'remote' | 'user'
|
|
) => void
|
|
export type TLCompleteHandler = (source: 'remote' | 'user') => void
|
|
|
|
/**
|
|
* The side effect manager (aka a "correct state enforcer") is responsible
|
|
* for making sure that the editor's state is always correct. This includes
|
|
* things like: deleting a shape if its parent is deleted; unbinding
|
|
* arrows when their binding target is deleted; etc.
|
|
*
|
|
* @public
|
|
*/
|
|
export class SideEffectManager<R extends UnknownRecord> {
|
|
constructor(public readonly store: Store<R>) {
|
|
store.onBeforeCreate = (record, source) => {
|
|
const handlers = this._beforeCreateHandlers[record.typeName as R['typeName']]
|
|
if (handlers) {
|
|
let r = record
|
|
for (const handler of handlers) {
|
|
r = handler(r, source)
|
|
}
|
|
return r
|
|
}
|
|
|
|
return record
|
|
}
|
|
|
|
store.onAfterCreate = (record, source) => {
|
|
const handlers = this._afterCreateHandlers[record.typeName as R['typeName']]
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
handler(record, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
store.onBeforeChange = (prev, next, source) => {
|
|
const handlers = this._beforeChangeHandlers[next.typeName as R['typeName']]
|
|
if (handlers) {
|
|
let r = next
|
|
for (const handler of handlers) {
|
|
r = handler(prev, r, source)
|
|
}
|
|
return r
|
|
}
|
|
|
|
return next
|
|
}
|
|
|
|
store.onAfterChange = (prev, next, source) => {
|
|
const handlers = this._afterChangeHandlers[next.typeName as R['typeName']]
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
handler(prev, next, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
store.onBeforeDelete = (record, source) => {
|
|
const handlers = this._beforeDeleteHandlers[record.typeName as R['typeName']]
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
if (handler(record, source) === false) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
store.onAfterDelete = (record, source) => {
|
|
const handlers = this._afterDeleteHandlers[record.typeName as R['typeName']]
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
handler(record, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
store.onAfterAtomic = (source) => {
|
|
const handlers = this._completeHandlers
|
|
if (handlers) {
|
|
for (const handler of handlers) {
|
|
handler(source)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _beforeCreateHandlers: Partial<{
|
|
[K in R['typeName']]: TLBeforeCreateHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _afterCreateHandlers: Partial<{
|
|
[K in R['typeName']]: TLAfterCreateHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _beforeChangeHandlers: Partial<{
|
|
[K in R['typeName']]: TLBeforeChangeHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _afterChangeHandlers: Partial<{
|
|
[K in R['typeName']]: TLAfterChangeHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _beforeDeleteHandlers: Partial<{
|
|
[K in R['typeName']]: TLBeforeDeleteHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _afterDeleteHandlers: Partial<{
|
|
[K in R['typeName']]: TLAfterDeleteHandler<R & { typeName: K }>[]
|
|
}> = {}
|
|
private _completeHandlers: TLCompleteHandler[] = []
|
|
|
|
/**
|
|
* Internal helper for registering a bunch of side effects at once and keeping them organized.
|
|
* @internal
|
|
*/
|
|
register(
|
|
handlersByType: {
|
|
[T in R as T['typeName']]?: {
|
|
beforeCreate?: TLBeforeCreateHandler<T>
|
|
afterCreate?: TLAfterCreateHandler<T>
|
|
beforeChange?: TLBeforeChangeHandler<T>
|
|
afterChange?: TLAfterChangeHandler<T>
|
|
beforeDelete?: TLBeforeDeleteHandler<T>
|
|
afterDelete?: TLAfterDeleteHandler<T>
|
|
}
|
|
} & { complete?: TLCompleteHandler }
|
|
) {
|
|
const disposes: (() => void)[] = []
|
|
if (handlersByType.complete) {
|
|
this._completeHandlers.push(handlersByType.complete)
|
|
}
|
|
for (const [type, handlers] of Object.entries(handlersByType) as any) {
|
|
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 SideEffectManager.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
|
|
*/
|
|
registerBeforeCreateHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLBeforeCreateHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._beforeCreateHandlers[typeName] as TLBeforeCreateHandler<any>[]
|
|
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 SideEffectManager.registerBeforeCreateHandler} instead.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* editor.sideEffects.registerAfterCreateHandler('page', (page, source) => {
|
|
* // Automatically create a shape when a page is created
|
|
* editor.createShape({
|
|
* id: createShapeId(),
|
|
* type: 'text',
|
|
* props: { text: page.name },
|
|
* })
|
|
* })
|
|
* ```
|
|
*
|
|
* @param typeName - The type of record to listen for
|
|
* @param handler - The handler to call
|
|
*/
|
|
registerAfterCreateHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLAfterCreateHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._afterCreateHandlers[typeName] as TLAfterCreateHandler<any>[]
|
|
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 SideEffectManager.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
|
|
*/
|
|
registerBeforeChangeHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLBeforeChangeHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._beforeChangeHandlers[typeName] as TLBeforeChangeHandler<any>[]
|
|
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 SideEffectManager.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
|
|
*/
|
|
registerAfterChangeHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLAfterChangeHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._afterChangeHandlers[typeName] as TLAfterChangeHandler<any>[]
|
|
if (!handlers) this._afterChangeHandlers[typeName] = []
|
|
this._afterChangeHandlers[typeName]!.push(handler as TLAfterChangeHandler<any>)
|
|
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 SideEffectManager.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
|
|
*/
|
|
registerBeforeDeleteHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLBeforeDeleteHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._beforeDeleteHandlers[typeName] as TLBeforeDeleteHandler<any>[]
|
|
if (!handlers) this._beforeDeleteHandlers[typeName] = []
|
|
this._beforeDeleteHandlers[typeName]!.push(handler as TLBeforeDeleteHandler<any>)
|
|
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 SideEffectManager.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
|
|
*/
|
|
registerAfterDeleteHandler<T extends R['typeName']>(
|
|
typeName: T,
|
|
handler: TLAfterDeleteHandler<R & { typeName: T }>
|
|
) {
|
|
const handlers = this._afterDeleteHandlers[typeName] as TLAfterDeleteHandler<any>[]
|
|
if (!handlers) this._afterDeleteHandlers[typeName] = []
|
|
this._afterDeleteHandlers[typeName]!.push(handler as TLAfterDeleteHandler<any>)
|
|
return () => remove(this._afterDeleteHandlers[typeName]!, handler)
|
|
}
|
|
|
|
/**
|
|
* Register a handler to be called when the store completes an operation.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* let count = 0
|
|
*
|
|
* editor.cleanup.registerBatchCompleteHandler(() => count++)
|
|
*
|
|
* editor.selectAll()
|
|
* expect(count).toBe(1)
|
|
*
|
|
* editor.store.atomic(() => {
|
|
* editor.selectNone()
|
|
* editor.selectAll()
|
|
* })
|
|
*
|
|
* expect(count).toBe(2)
|
|
* ```
|
|
*
|
|
* @param handler - The handler to call
|
|
*
|
|
* @public
|
|
*/
|
|
registerCompleteHandler(handler: TLCompleteHandler) {
|
|
this._completeHandlers.push(handler)
|
|
return () => remove(this._completeHandlers, handler)
|
|
}
|
|
}
|
|
|
|
function remove(array: any[], item: any) {
|
|
const index = array.indexOf(item)
|
|
if (index >= 0) {
|
|
array.splice(index, 1)
|
|
}
|
|
}
|