Bindings documentation (#3812)
Adds docs (reference material and a guide) for the bindings API. Also, the unbind reason enum is now a union of strings. ### Change Type - [x] `docs` — Changes to the documentation, examples, or templates. - [x] `improvement` — Improving existing features
This commit is contained in:
parent
ccb6b918c5
commit
f5a6ed7b91
6 changed files with 352 additions and 62 deletions
|
@ -387,6 +387,108 @@ The camera may be in two states, `idle` or `moving`.
|
||||||
|
|
||||||
You can get the current camera state with [Editor#getCameraState](?).
|
You can get the current camera state with [Editor#getCameraState](?).
|
||||||
|
|
||||||
|
# Bindings
|
||||||
|
|
||||||
|
A binding is a relationship from one [shape](/docs/shapes) to another. They're used to connect
|
||||||
|
shapes so they can update together or depend on one and other. For example: tldraw's default arrow
|
||||||
|
shape uses bindings to connect the ends of the arrows to the shapes they're pointing to, one binding
|
||||||
|
for each end of the arrow.
|
||||||
|
|
||||||
|
You can create different types of binding that do all sorts of different things with relationships
|
||||||
|
between shapes. For example, you could create a [sticker
|
||||||
|
shape](/examples/shapes/tools/sticker-bindings) that sticks to any other shape it's dropped onto.
|
||||||
|
|
||||||
|
## The binding object
|
||||||
|
|
||||||
|
Bindings are records (JSON objects) that live in the [store](/docs/editor#store). For example,
|
||||||
|
here's a binding record for one end of an arrow shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "binding:someId",
|
||||||
|
"typeName": "binding"
|
||||||
|
"type": "arrow",
|
||||||
|
"fromId": "shape:arrowId",
|
||||||
|
"toId": "shape:someOtherShapeId",
|
||||||
|
"props": {
|
||||||
|
"terminal": "end"
|
||||||
|
"isPrecise": true,
|
||||||
|
"isExact": false,
|
||||||
|
"normalizedAnchor": {
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.5
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"meta": {},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every binding contains some base information - its ID & the type of binding, as well as the ID of
|
||||||
|
the shape that a binding comes _from_ and the ID of the shape that the binding goes _to_. These two
|
||||||
|
properties work the same way, but it's often useful to have an explicit direction in a binding
|
||||||
|
relationship. For example, an arrow binding always goes _from_ an arrow _to_ another shape.
|
||||||
|
|
||||||
|
Bindings contain their own type-specific information in the `props` object. Each type of binding can
|
||||||
|
have different props.
|
||||||
|
|
||||||
|
Bindings also have a `meta` property which can be used by your application to add data to bindings
|
||||||
|
you haven't built yourself. You can read more about the meta property
|
||||||
|
[here](/docs/shapes#Meta-information).
|
||||||
|
|
||||||
|
## Custom bindings
|
||||||
|
|
||||||
|
To create a binding of your own, you can define a custom binding.
|
||||||
|
|
||||||
|
### The binding type
|
||||||
|
|
||||||
|
First, you need to create a type that describes what the binding object will look like:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TLBaseBinding } from 'tldraw'
|
||||||
|
|
||||||
|
type StickerBinding = TLBaseBinding<'sticker', { x: number; y: number }>
|
||||||
|
```
|
||||||
|
|
||||||
|
With [TLBaseBinding](?) we define the binding's type (`sticker`) and `props` property (`{x: number,
|
||||||
|
y: number}`). The type can be any string, and the props must be a plain JSON object (ie not a class
|
||||||
|
instance).
|
||||||
|
|
||||||
|
The [TLBaseBinding](?) helper adds the other base properties like `id`, `toId`, and `fromId`.
|
||||||
|
|
||||||
|
### The `BindingUtil` class
|
||||||
|
|
||||||
|
While bindings themselves are plain JSON objects, we use
|
||||||
|
[`BindingUtil`](/reference/editor/BindingUtil) classes to define how bindings should work. They tell
|
||||||
|
tldraw the structure of a binding, and what to do when it or the shapes it involves are created,
|
||||||
|
updated, or deleted.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { BindingUtil } from 'tldraw'
|
||||||
|
|
||||||
|
class StickerBindingUtil extends BindingUtil<StickerBinding> {
|
||||||
|
static override type = 'sticker' as const
|
||||||
|
|
||||||
|
override getDefaultProps() {
|
||||||
|
return { x: 0.5, y: 0.5 }
|
||||||
|
}
|
||||||
|
|
||||||
|
override onAfterChangeToShape({ binding }) {
|
||||||
|
const sticker = this.editor.getShape(binding.fromShape)
|
||||||
|
|
||||||
|
// move the sticker so it stays attached to the to shape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each binding util needs a `type` and `getDefaultProps` to define its basic structure. You can also
|
||||||
|
define a number of different callbacks describing how the binding should behave. See the
|
||||||
|
[BindingUtil](?) reference or a [complete example of a
|
||||||
|
binding](/examples/shapes/tools/sticker-bindings) for details.
|
||||||
|
|
||||||
|
If the strucutre of your binding needs to change over time, you can provide
|
||||||
|
[migrations](/docs/persistence#Shape-props-migrations) describing how old stored bindings can be
|
||||||
|
brought up to date.
|
||||||
|
|
||||||
# Common things to do with the editor
|
# Common things to do with the editor
|
||||||
|
|
||||||
### Create a shape id
|
### Create a shape id
|
||||||
|
|
|
@ -175,50 +175,39 @@ export abstract class BaseBoxShapeUtil<Shape extends TLBaseBoxShape> extends Sha
|
||||||
onResize: TLOnResizeHandler<any>;
|
onResize: TLOnResizeHandler<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
bindingAfter: Binding;
|
bindingAfter: Binding;
|
||||||
// (undocumented)
|
|
||||||
bindingBefore: Binding;
|
bindingBefore: Binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
binding: Binding;
|
binding: Binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
binding: Binding;
|
binding: Binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
binding: Binding;
|
binding: Binding;
|
||||||
// (undocumented)
|
|
||||||
shapeAfter: TLShape;
|
shapeAfter: TLShape;
|
||||||
// (undocumented)
|
|
||||||
shapeBefore: TLShape;
|
shapeBefore: TLShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
binding: Binding;
|
binding: Binding;
|
||||||
// (undocumented)
|
|
||||||
shape: TLShape;
|
shape: TLShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export interface BindingOnShapeIsolateOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeIsolateOptions<Binding extends TLUnknownBinding> {
|
||||||
// (undocumented)
|
|
||||||
binding: Binding;
|
binding: Binding;
|
||||||
// (undocumented)
|
removedShape: TLShape;
|
||||||
shape: TLShape;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -229,31 +218,18 @@ export abstract class BindingUtil<Binding extends TLUnknownBinding = TLUnknownBi
|
||||||
abstract getDefaultProps(): Partial<Binding['props']>;
|
abstract getDefaultProps(): Partial<Binding['props']>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static migrations?: TLPropsMigrations;
|
static migrations?: TLPropsMigrations;
|
||||||
// (undocumented)
|
|
||||||
onAfterChange?(options: BindingOnChangeOptions<Binding>): void;
|
onAfterChange?(options: BindingOnChangeOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void;
|
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void;
|
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void;
|
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void;
|
||||||
// (undocumented)
|
|
||||||
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void;
|
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void;
|
||||||
// (undocumented)
|
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): void;
|
||||||
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): Binding | void;
|
|
||||||
// (undocumented)
|
|
||||||
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onBeforeIsolateFromShape?(options: BindingOnShapeIsolateOptions<Binding>): void;
|
onBeforeIsolateFromShape?(options: BindingOnShapeIsolateOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onBeforeIsolateToShape?(options: BindingOnShapeIsolateOptions<Binding>): void;
|
onBeforeIsolateToShape?(options: BindingOnShapeIsolateOptions<Binding>): void;
|
||||||
// (undocumented)
|
|
||||||
onOperationComplete?(): void;
|
onOperationComplete?(): void;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
static props?: RecordProps<TLUnknownBinding>;
|
static props?: RecordProps<TLUnknownBinding>;
|
||||||
|
@ -790,9 +766,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
crash(error: unknown): this;
|
crash(error: unknown): this;
|
||||||
createAssets(assets: TLAsset[]): this;
|
createAssets(assets: TLAsset[]): this;
|
||||||
// (undocumented)
|
|
||||||
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>): this;
|
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>): this;
|
||||||
// (undocumented)
|
|
||||||
createBindings(partials: TLBindingCreate[]): this;
|
createBindings(partials: TLBindingCreate[]): this;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): {
|
||||||
|
@ -811,9 +785,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
||||||
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
||||||
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
|
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
|
||||||
// (undocumented)
|
|
||||||
deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters<this['deleteBindings']>[1]): this;
|
deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters<this['deleteBindings']>[1]): this;
|
||||||
// (undocumented)
|
|
||||||
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes }?: {
|
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes }?: {
|
||||||
isolateShapes?: boolean | undefined;
|
isolateShapes?: boolean | undefined;
|
||||||
}): this;
|
}): this;
|
||||||
|
@ -860,13 +832,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
|
||||||
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[];
|
||||||
getBaseZoom(): number;
|
getBaseZoom(): number;
|
||||||
// (undocumented)
|
|
||||||
getBinding(id: TLBindingId): TLBinding | undefined;
|
getBinding(id: TLBindingId): TLBinding | undefined;
|
||||||
// (undocumented)
|
|
||||||
getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type: Binding['type']): Binding[];
|
getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type: Binding['type']): Binding[];
|
||||||
// (undocumented)
|
|
||||||
getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type?: Binding['type']): Binding[];
|
getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type?: Binding['type']): Binding[];
|
||||||
// (undocumented)
|
|
||||||
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type: Binding['type']): Binding[];
|
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(shape: TLShape | TLShapeId, type: Binding['type']): Binding[];
|
||||||
getBindingUtil<S extends TLUnknownBinding>(binding: {
|
getBindingUtil<S extends TLUnknownBinding>(binding: {
|
||||||
type: S['type'];
|
type: S['type'];
|
||||||
|
@ -1141,9 +1109,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
select: boolean;
|
select: boolean;
|
||||||
}>): this;
|
}>): this;
|
||||||
updateAssets(assets: TLAssetPartial[]): this;
|
updateAssets(assets: TLAssetPartial[]): this;
|
||||||
// (undocumented)
|
|
||||||
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>): this;
|
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>): this;
|
||||||
// (undocumented)
|
|
||||||
updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this;
|
updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this;
|
||||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
|
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -2181,9 +2147,7 @@ export interface TLBaseEventInfo {
|
||||||
export interface TLBindingUtilConstructor<T extends TLUnknownBinding, U extends BindingUtil<T> = BindingUtil<T>> {
|
export interface TLBindingUtilConstructor<T extends TLUnknownBinding, U extends BindingUtil<T> = BindingUtil<T>> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
new (editor: Editor): U;
|
new (editor: Editor): U;
|
||||||
// (undocumented)
|
|
||||||
migrations?: TLPropsMigrations;
|
migrations?: TLPropsMigrations;
|
||||||
// (undocumented)
|
|
||||||
props?: RecordProps<T>;
|
props?: RecordProps<T>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
type: T['type'];
|
type: T['type'];
|
||||||
|
|
|
@ -492,10 +492,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
deleteBindingIds.push(binding.id)
|
deleteBindingIds.push(binding.id)
|
||||||
const util = this.getBindingUtil(binding)
|
const util = this.getBindingUtil(binding)
|
||||||
if (binding.fromId === shape.id) {
|
if (binding.fromId === shape.id) {
|
||||||
util.onBeforeIsolateToShape?.({ binding, shape })
|
util.onBeforeIsolateToShape?.({ binding, removedShape: shape })
|
||||||
util.onBeforeDeleteFromShape?.({ binding, shape })
|
util.onBeforeDeleteFromShape?.({ binding, shape })
|
||||||
} else {
|
} else {
|
||||||
util.onBeforeIsolateFromShape?.({ binding, shape })
|
util.onBeforeIsolateFromShape?.({ binding, removedShape: shape })
|
||||||
util.onBeforeDeleteToShape?.({ binding, shape })
|
util.onBeforeDeleteToShape?.({ binding, shape })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5152,10 +5152,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a binding from the store by its ID if it exists.
|
||||||
|
*/
|
||||||
getBinding(id: TLBindingId): TLBinding | undefined {
|
getBinding(id: TLBindingId): TLBinding | undefined {
|
||||||
return this.store.get(id) as TLBinding | undefined
|
return this.store.get(id) as TLBinding | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bindings of a certain type _from_ a particular shape. These are the bindings whose
|
||||||
|
* `fromId` matched the shape's ID.
|
||||||
|
*/
|
||||||
getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(
|
getBindingsFromShape<Binding extends TLUnknownBinding = TLBinding>(
|
||||||
shape: TLShape | TLShapeId,
|
shape: TLShape | TLShapeId,
|
||||||
type: Binding['type']
|
type: Binding['type']
|
||||||
|
@ -5165,6 +5172,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
(b) => b.fromId === id && b.type === type
|
(b) => b.fromId === id && b.type === type
|
||||||
) as Binding[]
|
) as Binding[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bindings of a certain type _to_ a particular shape. These are the bindings whose
|
||||||
|
* `toId` matches the shape's ID.
|
||||||
|
*/
|
||||||
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(
|
getBindingsToShape<Binding extends TLUnknownBinding = TLBinding>(
|
||||||
shape: TLShape | TLShapeId,
|
shape: TLShape | TLShapeId,
|
||||||
type: Binding['type']
|
type: Binding['type']
|
||||||
|
@ -5174,6 +5186,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
(b) => b.toId === id && b.type === type
|
(b) => b.toId === id && b.type === type
|
||||||
) as Binding[]
|
) as Binding[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bindings involving a particular shape. This includes bindings where the shape is the
|
||||||
|
* `fromId` or `toId`. If a type is provided, only bindings of that type are returned.
|
||||||
|
*/
|
||||||
getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(
|
getBindingsInvolvingShape<Binding extends TLUnknownBinding = TLBinding>(
|
||||||
shape: TLShape | TLShapeId,
|
shape: TLShape | TLShapeId,
|
||||||
type?: Binding['type']
|
type?: Binding['type']
|
||||||
|
@ -5184,6 +5201,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return result.filter((b) => b.type === type) as Binding[]
|
return result.filter((b) => b.type === type) as Binding[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create bindings from a list of partial bindings. You can omit the ID and most props of a
|
||||||
|
* binding, but the `type`, `toId`, and `fromId` must all be provided.
|
||||||
|
*/
|
||||||
createBindings(partials: TLBindingCreate[]) {
|
createBindings(partials: TLBindingCreate[]) {
|
||||||
const bindings: TLBinding[] = []
|
const bindings: TLBinding[] = []
|
||||||
for (const partial of partials) {
|
for (const partial of partials) {
|
||||||
|
@ -5209,10 +5230,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
this.store.put(bindings)
|
this.store.put(bindings)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single binding from a partial. You can omit the ID and most props of a binding, but
|
||||||
|
* the `type`, `toId`, and `fromId` must all be provided.
|
||||||
|
*/
|
||||||
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>) {
|
createBinding<B extends TLBinding = TLBinding>(partial: TLBindingCreate<B>) {
|
||||||
return this.createBindings([partial])
|
return this.createBindings([partial])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update bindings from a list of partial bindings. Each partial must include an ID, which will
|
||||||
|
* be used to match the binding to it's existing record. If there is no existing record, that
|
||||||
|
* binding is skipped. The changes from the partial are merged into the existing record.
|
||||||
|
*/
|
||||||
updateBindings(partials: (TLBindingUpdate | null | undefined)[]) {
|
updateBindings(partials: (TLBindingUpdate | null | undefined)[]) {
|
||||||
const updated: TLBinding[] = []
|
const updated: TLBinding[] = []
|
||||||
|
|
||||||
|
@ -5238,10 +5269,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a binding from a partial binding. Each partial must include an ID, which will be used
|
||||||
|
* to match the binding to it's existing record. If there is no existing record, that binding is
|
||||||
|
* skipped. The changes from the partial are merged into the existing record.
|
||||||
|
*/
|
||||||
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>) {
|
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>) {
|
||||||
return this.updateBindings([partial])
|
return this.updateBindings([partial])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored.
|
||||||
|
*/
|
||||||
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) {
|
||||||
const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id))
|
const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id))
|
||||||
if (isolateShapes) {
|
if (isolateShapes) {
|
||||||
|
@ -5250,8 +5289,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
const binding = this.getBinding(id)
|
const binding = this.getBinding(id)
|
||||||
if (!binding) continue
|
if (!binding) continue
|
||||||
const util = this.getBindingUtil(binding)
|
const util = this.getBindingUtil(binding)
|
||||||
util.onBeforeIsolateFromShape?.({ binding, shape: this.getShape(binding.fromId)! })
|
util.onBeforeIsolateFromShape?.({ binding, removedShape: this.getShape(binding.toId)! })
|
||||||
util.onBeforeIsolateToShape?.({ binding, shape: this.getShape(binding.toId)! })
|
util.onBeforeIsolateToShape?.({ binding, removedShape: this.getShape(binding.fromId)! })
|
||||||
this.store.remove([id])
|
this.store.remove([id])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -5260,6 +5299,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Delete a binding by its ID. If the binding doesn't exist, it's ignored.
|
||||||
|
*/
|
||||||
deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters<this['deleteBindings']>[1]) {
|
deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters<this['deleteBindings']>[1]) {
|
||||||
return this.deleteBindings([binding], opts)
|
return this.deleteBindings([binding], opts)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,42 +8,110 @@ export interface TLBindingUtilConstructor<
|
||||||
> {
|
> {
|
||||||
new (editor: Editor): U
|
new (editor: Editor): U
|
||||||
type: T['type']
|
type: T['type']
|
||||||
|
/** Validations for this binding's props. */
|
||||||
props?: RecordProps<T>
|
props?: RecordProps<T>
|
||||||
|
/** Migrations for this binding's props. */
|
||||||
migrations?: TLPropsMigrations
|
migrations?: TLPropsMigrations
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onBeforeCreate} and {@link BindingUtil.onAfterCreate},
|
||||||
|
* describing a the creating a binding.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnCreateOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding being created. */
|
||||||
binding: Binding
|
binding: Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onBeforeChange} and {@link BindingUtil.onAfterChange},
|
||||||
|
* describing the data associated with a binding being changed.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding record before the change is made. */
|
||||||
bindingBefore: Binding
|
bindingBefore: Binding
|
||||||
|
/** The binding record after the change is made. */
|
||||||
bindingAfter: Binding
|
bindingAfter: Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onBeforeDelete} and {@link BindingUtil.onAfterDelete},
|
||||||
|
* describing a binding being deleted.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding being deleted. */
|
||||||
binding: Binding
|
binding: Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onAfterChangeFromShape} and
|
||||||
|
* {@link BindingUtil.onAfterChangeToShape}, describing a bound shape being changed.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding record linking these two shapes. */
|
||||||
binding: Binding
|
binding: Binding
|
||||||
|
/** The shape record before the change is made. */
|
||||||
shapeBefore: TLShape
|
shapeBefore: TLShape
|
||||||
|
/** The shape record after the change is made. */
|
||||||
shapeAfter: TLShape
|
shapeAfter: TLShape
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onBeforeIsolateFromShape} and
|
||||||
|
* {@link BindingUtil.onBeforeIsolateToShape}, describing a shape that is about to be isolated from
|
||||||
|
* the one that it's bound to.
|
||||||
|
*
|
||||||
|
* Isolation happens whenever two bound shapes are separated. For example
|
||||||
|
* 1. One is deleted, but the other is not.
|
||||||
|
* 1. One is copied, but the other is not.
|
||||||
|
* 1. One is duplicated, but the other is not.
|
||||||
|
*
|
||||||
|
* In each of these cases, if the remaining shape depends on the binding for its rendering, it may
|
||||||
|
* now be in an inconsistent state. For example, tldraw's arrow shape depends on the binding to know
|
||||||
|
* where the end of the arrow is. If we removed the binding without doing anything else, the arrow
|
||||||
|
* would suddenly be pointing to the wrong location. Instead, when the shape the arrow is pointing
|
||||||
|
* to is deleted, or the arrow is copied/duplicated, we use an isolation callback. The callback
|
||||||
|
* updates the arrow based on the binding that's about to be removed, so it doesn't end up pointing
|
||||||
|
* to the wrong place.
|
||||||
|
*
|
||||||
|
* For this style of consistency update, use isolation callbacks. For actions specific to deletion
|
||||||
|
* (like deleting a sticker when the shape it's bound to is removed), use the delete callbacks
|
||||||
|
* ({@link BindingUtil.onBeforeDeleteFromShape} and {@link BindingUtil.onBeforeDeleteToShape})
|
||||||
|
* instead.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnShapeIsolateOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeIsolateOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding record that refers to the shape in question. */
|
||||||
binding: Binding
|
binding: Binding
|
||||||
shape: TLShape
|
/**
|
||||||
|
* The shape being removed. For deletion, this is the deleted shape. For copy/duplicate, this is
|
||||||
|
* the shape that _isn't_ being copied/duplicated and is getting left behind.
|
||||||
|
*/
|
||||||
|
removedShape: TLShape
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* Options passed to {@link BindingUtil.onBeforeDeleteFromShape} and
|
||||||
|
* {@link BindingUtil.onBeforeDeleteToShape}, describing a bound shape that is about to be deleted.
|
||||||
|
*
|
||||||
|
* See {@link BindingOnShapeIsolateOptions} for discussion on when to use the delete vs. the isolate
|
||||||
|
* callbacks.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
export interface BindingOnShapeDeleteOptions<Binding extends TLUnknownBinding> {
|
||||||
|
/** The binding record that refers to the shape in question. */
|
||||||
binding: Binding
|
binding: Binding
|
||||||
|
/** The shape that is about to be deleted. */
|
||||||
shape: TLShape
|
shape: TLShape
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,22 +135,132 @@ export abstract class BindingUtil<Binding extends TLUnknownBinding = TLUnknownBi
|
||||||
*/
|
*/
|
||||||
abstract getDefaultProps(): Partial<Binding['props']>
|
abstract getDefaultProps(): Partial<Binding['props']>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever a store operation involving this binding type has completed. This is useful
|
||||||
|
* for working with networks of related bindings that may need to update together.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* class MyBindingUtil extends BindingUtil<MyBinding> {
|
||||||
|
* changedBindingIds = new Set<TLBindingId>()
|
||||||
|
*
|
||||||
|
* onOperationComplete() {
|
||||||
|
* doSomethingWithChangedBindings(this.changedBindingIds)
|
||||||
|
* this.changedBindingIds.clear()
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* onAfterChange({ bindingAfter }: BindingOnChangeOptions<MyBinding>) {
|
||||||
|
* this.changedBindingIds.add(bindingAfter.id)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onOperationComplete?(): void
|
onOperationComplete?(): void
|
||||||
|
|
||||||
// self lifecycle hooks
|
/**
|
||||||
|
* Called when a binding is about to be created. See {@link BindingOnCreateOptions} for details.
|
||||||
|
*
|
||||||
|
* You can optionally return a new binding to replace the one being created - for example, to
|
||||||
|
* set different initial props.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void
|
onBeforeCreate?(options: BindingOnCreateOptions<Binding>): Binding | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after a binding has been created. See {@link BindingOnCreateOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void
|
onAfterCreate?(options: BindingOnCreateOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a binding is about to be changed. See {@link BindingOnChangeOptions} for details.
|
||||||
|
*
|
||||||
|
* Note that this only fires when the binding record is changing, not when the shapes
|
||||||
|
* associated change. Use {@link BindingUtil.onAfterChangeFromShape} and
|
||||||
|
* {@link BindingUtil.onAfterChangeToShape} for that.
|
||||||
|
*
|
||||||
|
* You can optionally return a new binding to replace the one being changed - for example, to
|
||||||
|
* enforce constraints on the binding's props.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void
|
onBeforeChange?(options: BindingOnChangeOptions<Binding>): Binding | void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after a binding has been changed. See {@link BindingOnChangeOptions} for details.
|
||||||
|
*
|
||||||
|
* Note that this only fires when the binding record is changing, not when the shapes
|
||||||
|
* associated change. Use {@link BindingUtil.onAfterChangeFromShape} and
|
||||||
|
* {@link BindingUtil.onAfterChangeToShape} for that.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onAfterChange?(options: BindingOnChangeOptions<Binding>): void
|
onAfterChange?(options: BindingOnChangeOptions<Binding>): void
|
||||||
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): Binding | void
|
|
||||||
|
/**
|
||||||
|
* Called when a binding is about to be deleted. See {@link BindingOnDeleteOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
onBeforeDelete?(options: BindingOnDeleteOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after a binding has been deleted. See {@link BindingOnDeleteOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void
|
onAfterDelete?(options: BindingOnDeleteOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the shape referenced in a binding's `fromId` is changed. Use this to propagate
|
||||||
|
* any changes to the binding itself or the other shape as needed. See
|
||||||
|
* {@link BindingOnShapeChangeOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
onAfterChangeFromShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after the shape referenced in a binding's `toId` is changed. Use this to propagate any
|
||||||
|
* changes to the binding itself or the other shape as needed. See
|
||||||
|
* {@link BindingOnShapeChangeOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
onAfterChangeToShape?(options: BindingOnShapeChangeOptions<Binding>): void
|
||||||
|
|
||||||
onBeforeIsolateFromShape?(options: BindingOnShapeIsolateOptions<Binding>): void
|
/**
|
||||||
onBeforeIsolateToShape?(options: BindingOnShapeIsolateOptions<Binding>): void
|
* Called before the shape referenced in a binding's `fromId` is about to be deleted. Use this
|
||||||
|
* with care - you may want to use {@link BindingUtil.onBeforeIsolateToShape} instead. See
|
||||||
|
* {@link BindingOnShapeDeleteOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
||||||
|
/**
|
||||||
|
* Called before the shape referenced in a binding's `toId` is about to be deleted. Use this
|
||||||
|
* with care - you may want to use {@link BindingUtil.onBeforeIsolateFromShape} instead. See
|
||||||
|
* {@link BindingOnShapeDeleteOptions} for details.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the shape referenced in a binding's `fromId` is about to be isolated from the
|
||||||
|
* shape referenced in `toId`. See {@link BindingOnShapeIsolateOptions} for discussion on what
|
||||||
|
* isolation means, and when/how to use this callback.
|
||||||
|
*/
|
||||||
|
onBeforeIsolateFromShape?(options: BindingOnShapeIsolateOptions<Binding>): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before the shape referenced in a binding's `toId` is about to be isolated from the
|
||||||
|
* shape referenced in `fromId`. See {@link BindingOnShapeIsolateOptions} for discussion on what
|
||||||
|
* isolation means, and when/how to use this callback.
|
||||||
|
*/
|
||||||
|
onBeforeIsolateToShape?(options: BindingOnShapeIsolateOptions<Binding>): void
|
||||||
}
|
}
|
||||||
|
|
|
@ -959,7 +959,7 @@ export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
|
||||||
typeName?: T['typeName'];
|
typeName?: T['typeName'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public
|
||||||
export type TLBindingId = RecordId<TLUnknownBinding>;
|
export type TLBindingId = RecordId<TLUnknownBinding>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
|
|
@ -55,7 +55,11 @@ export type TLBindingCreate<T extends TLBinding = TLBinding> = Expand<{
|
||||||
meta?: Partial<T['meta']>
|
meta?: Partial<T['meta']>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
/** @public */
|
/**
|
||||||
|
* An ID for a {@link TLBinding}.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
export type TLBindingId = RecordId<TLUnknownBinding>
|
export type TLBindingId = RecordId<TLUnknownBinding>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
Loading…
Reference in a new issue