alex/no-batches: no batches

alex/no-batches
alex 2024-04-11 16:19:41 +01:00
rodzic 3448e53a64
commit 1ecf54f5f7
25 zmienionych plików z 2049 dodań i 2284 usunięć

Wyświetl plik

@ -592,7 +592,6 @@ export class Editor extends EventEmitter<TLEventMap> {
}): this; }): this;
bail(): this; bail(): this;
bailToMark(id: string): this; bailToMark(id: string): this;
batch(fn: () => void, opts?: TLHistoryBatchOptions): this;
bringForward(shapes: TLShape[] | TLShapeId[]): this; bringForward(shapes: TLShape[] | TLShapeId[]): this;
bringToFront(shapes: TLShape[] | TLShapeId[]): this; bringToFront(shapes: TLShape[] | TLShapeId[]): this;
cancel(): this; cancel(): this;
@ -893,7 +892,7 @@ export class Editor extends EventEmitter<TLEventMap> {
shapeUtils: { shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>; readonly [K in string]?: ShapeUtil<TLUnknownShape>;
}; };
readonly sideEffects: SideEffectManager<this>; readonly sideEffects: SideEffectManager<TLRecord>;
slideCamera(opts?: { slideCamera(opts?: {
speed: number; speed: number;
direction: VecLike; direction: VecLike;
@ -920,9 +919,9 @@ export class Editor extends EventEmitter<TLEventMap> {
updateAssets(assets: TLAssetPartial[]): this; updateAssets(assets: TLAssetPartial[]): 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)
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions) => void; _updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, options?: TLHistoryBatchOptions) => void;
updateDocumentSettings(settings: Partial<TLDocument>): this; updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: TLHistoryBatchOptions): this; updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, options?: TLHistoryBatchOptions): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>): this; updatePage(partial: RequiredKeys<TLPage, 'id'>): this;
// @internal // @internal
updateRenderingBounds(): this; updateRenderingBounds(): this;
@ -1202,8 +1201,6 @@ export class HistoryManager<R extends UnknownRecord> {
// (undocumented) // (undocumented)
bailToMark: (id: string) => this; bailToMark: (id: string) => this;
// (undocumented) // (undocumented)
batch: (fn: () => void, opts?: TLHistoryBatchOptions) => this;
// (undocumented)
clear(): void; clear(): void;
// @internal (undocumented) // @internal (undocumented)
debug(): { debug(): {
@ -1213,7 +1210,7 @@ export class HistoryManager<R extends UnknownRecord> {
diff: RecordsDiff<R>; diff: RecordsDiff<R>;
isEmpty: boolean; isEmpty: boolean;
}; };
state: HistoryRecorderState; state: TLHistoryMode;
}; };
// (undocumented) // (undocumented)
readonly dispose: () => void; readonly dispose: () => void;
@ -1223,14 +1220,16 @@ export class HistoryManager<R extends UnknownRecord> {
getNumUndos(): number; getNumUndos(): number;
// (undocumented) // (undocumented)
ignore(fn: () => void): this; ignore(fn: () => void): this;
// @internal (undocumented)
_isInBatch: boolean;
// (undocumented) // (undocumented)
mark: (id?: string) => string; mark: (id?: string) => string;
// (undocumented) // (undocumented)
onBatchComplete: () => void; record(fn: () => void): this;
// (undocumented)
recordPreservingRedoStack(fn: () => void): this;
// (undocumented) // (undocumented)
redo: () => this | undefined; redo: () => this | undefined;
// (undocumented)
runInMode(mode: null | TLHistoryMode | undefined, fn: () => void): this;
// @internal (undocumented) // @internal (undocumented)
stacks: Atom< { stacks: Atom< {
undos: Stack<TLHistoryEntry<R>>; undos: Stack<TLHistoryEntry<R>>;
@ -1745,45 +1744,42 @@ export class SharedStyleMap extends ReadonlySharedStyleMap {
export function shortAngleDist(a0: number, a1: number): number; export function shortAngleDist(a0: number, a1: number): number;
// @public // @public
export class SideEffectManager<CTX extends { export class SideEffectManager<R extends UnknownRecord> {
store: TLStore; constructor(store: Store<R>);
history: {
onBatchComplete: () => void;
};
}> {
constructor(editor: CTX);
// (undocumented)
editor: CTX;
// @internal // @internal
register(handlersByType: { register(handlersByType: {
[R in TLRecord as R['typeName']]?: { [T in R as T['typeName']]?: {
beforeCreate?: TLBeforeCreateHandler<R>; beforeCreate?: TLBeforeCreateHandler<T>;
afterCreate?: TLAfterCreateHandler<R>; afterCreate?: TLAfterCreateHandler<T>;
beforeChange?: TLBeforeChangeHandler<R>; beforeChange?: TLBeforeChangeHandler<T>;
afterChange?: TLAfterChangeHandler<R>; afterChange?: TLAfterChangeHandler<T>;
beforeDelete?: TLBeforeDeleteHandler<R>; beforeDelete?: TLBeforeDeleteHandler<T>;
afterDelete?: TLAfterDeleteHandler<R>; afterDelete?: TLAfterDeleteHandler<T>;
}; };
} & {
complete?: TLCompleteHandler;
}): () => void; }): () => void;
registerAfterChangeHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLAfterChangeHandler<TLRecord & { registerAfterChangeHandler<T extends R['typeName']>(typeName: T, handler: TLAfterChangeHandler<R & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerAfterCreateHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLAfterCreateHandler<TLRecord & { registerAfterCreateHandler<T extends R['typeName']>(typeName: T, handler: TLAfterCreateHandler<R & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerAfterDeleteHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLAfterDeleteHandler<TLRecord & { registerAfterDeleteHandler<T extends R['typeName']>(typeName: T, handler: TLAfterDeleteHandler<R & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerBatchCompleteHandler(handler: TLBatchCompleteHandler): () => void; registerBeforeChangeHandler<T extends R['typeName']>(typeName: T, handler: TLBeforeChangeHandler<R & {
registerBeforeChangeHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLBeforeChangeHandler<TLRecord & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerBeforeCreateHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLBeforeCreateHandler<TLRecord & { registerBeforeCreateHandler<T extends R['typeName']>(typeName: T, handler: TLBeforeCreateHandler<R & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerBeforeDeleteHandler<T extends TLRecord['typeName']>(typeName: T, handler: TLBeforeDeleteHandler<TLRecord & { registerBeforeDeleteHandler<T extends R['typeName']>(typeName: T, handler: TLBeforeDeleteHandler<R & {
typeName: T; typeName: T;
}>): () => void; }>): () => void;
registerCompleteHandler(handler: TLCompleteHandler): () => void;
// (undocumented)
readonly store: Store<R>;
} }
export { Signal } export { Signal }
@ -1945,13 +1941,13 @@ export interface SvgExportDef {
export const TAB_ID: string; export const TAB_ID: string;
// @public (undocumented) // @public (undocumented)
export type TLAfterChangeHandler<R extends TLRecord> = (prev: R, next: R, source: 'remote' | 'user') => void; export type TLAfterChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: 'remote' | 'user') => void;
// @public (undocumented) // @public (undocumented)
export type TLAfterCreateHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => void; export type TLAfterCreateHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => void;
// @public (undocumented) // @public (undocumented)
export type TLAfterDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => void; export type TLAfterDeleteHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => void;
// @public (undocumented) // @public (undocumented)
export type TLAnimationOptions = Partial<{ export type TLAnimationOptions = Partial<{
@ -2022,16 +2018,13 @@ export interface TLBaseEventInfo {
} }
// @public (undocumented) // @public (undocumented)
export type TLBatchCompleteHandler = () => void; export type TLBeforeChangeHandler<R extends UnknownRecord> = (prev: R, next: R, source: 'remote' | 'user') => R;
// @public (undocumented) // @public (undocumented)
export type TLBeforeChangeHandler<R extends TLRecord> = (prev: R, next: R, source: 'remote' | 'user') => R; export type TLBeforeCreateHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => R;
// @public (undocumented) // @public (undocumented)
export type TLBeforeCreateHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => R; export type TLBeforeDeleteHandler<R extends UnknownRecord> = (record: R, source: 'remote' | 'user') => false | void;
// @public (undocumented)
export type TLBeforeDeleteHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => false | void;
// @public (undocumented) // @public (undocumented)
export type TLBrushProps = { export type TLBrushProps = {
@ -2232,8 +2225,6 @@ export interface TLEventMap {
mount: []; mount: [];
// (undocumented) // (undocumented)
tick: [number]; tick: [number];
// (undocumented)
update: [];
} }
// @public (undocumented) // @public (undocumented)

Wyświetl plik

@ -139,7 +139,6 @@ export type {
TLAfterChangeHandler, TLAfterChangeHandler,
TLAfterCreateHandler, TLAfterCreateHandler,
TLAfterDeleteHandler, TLAfterDeleteHandler,
TLBatchCompleteHandler,
TLBeforeChangeHandler, TLBeforeChangeHandler,
TLBeforeCreateHandler, TLBeforeCreateHandler,
TLBeforeDeleteHandler, TLBeforeDeleteHandler,

Wyświetl plik

@ -61,14 +61,12 @@ function createCounterHistoryManager() {
} }
const setAge = (age = 35) => { const setAge = (age = 35) => {
manager.batch(() => _setAge(age), { history: 'record-preserveRedoStack' }) manager.recordPreservingRedoStack(() => _setAge(age))
} }
const incrementTwice = () => { const incrementTwice = () => {
manager.batch(() => { increment()
increment() increment()
increment()
})
} }
return { return {
@ -290,12 +288,12 @@ describe('history options', () => {
return { a: store.get(ids.a)!.value as number, b: store.get(ids.b)!.value as number } return { a: store.get(ids.a)!.value as number, b: store.get(ids.b)!.value as number }
} }
setA = (n: number, historyOptions?: TLHistoryBatchOptions) => { setA = (n: number, opts?: TLHistoryBatchOptions) => {
manager.batch(() => store.update(ids.a, (s) => ({ ...s, value: n })), historyOptions) manager.runInMode(opts?.history, () => store.update(ids.a, (s) => ({ ...s, value: n })))
} }
setB = (n: number, historyOptions?: TLHistoryBatchOptions) => { setB = (n: number, opts?: TLHistoryBatchOptions) => {
manager.batch(() => store.update(ids.b, (s) => ({ ...s, value: n })), historyOptions) manager.runInMode(opts?.history, () => store.update(ids.b, (s) => ({ ...s, value: n })))
} }
}) })
@ -398,14 +396,11 @@ describe('history options', () => {
it('nested ignore', () => { it('nested ignore', () => {
manager.mark() manager.mark()
manager.batch( manager.ignore(() => {
() => { setA(1)
setA(1) manager.record(() => setB(1))
manager.batch(() => setB(1), { history: 'record' }) setA(2)
setA(2) })
},
{ history: 'ignore' }
)
expect(getState()).toMatchObject({ a: 2, b: 1 }) expect(getState()).toMatchObject({ a: 2, b: 1 })
// changes to A were ignore, but changes to B were recorded: // changes to A were ignore, but changes to B were recorded:
@ -413,13 +408,10 @@ describe('history options', () => {
expect(getState()).toMatchObject({ a: 2, b: 0 }) expect(getState()).toMatchObject({ a: 2, b: 0 })
manager.mark() manager.mark()
manager.batch( manager.recordPreservingRedoStack(() => {
() => { setA(3)
setA(3) manager.ignore(() => setB(2))
manager.batch(() => setB(2), { history: 'ignore' }) })
},
{ history: 'record-preserveRedoStack' }
)
expect(getState()).toMatchObject({ a: 3, b: 2 }) expect(getState()).toMatchObject({ a: 3, b: 2 })
// changes to A were recorded, but changes to B were ignore: // changes to A were recorded, but changes to B were ignore:

Wyświetl plik

@ -10,22 +10,16 @@ import {
} from '@tldraw/store' } from '@tldraw/store'
import { exhaustiveSwitchError, noop } from '@tldraw/utils' import { exhaustiveSwitchError, noop } from '@tldraw/utils'
import { uniqueId } from '../../utils/uniqueId' import { uniqueId } from '../../utils/uniqueId'
import { TLHistoryBatchOptions, TLHistoryEntry } from '../types/history-types' import { TLHistoryEntry, TLHistoryMode } from '../types/history-types'
import { stack } from './Stack' import { stack } from './Stack'
enum HistoryRecorderState {
Recording = 'recording',
RecordingPreserveRedoStack = 'recordingPreserveRedoStack',
Paused = 'paused',
}
/** @public */ /** @public */
export class HistoryManager<R extends UnknownRecord> { export class HistoryManager<R extends UnknownRecord> {
private readonly store: Store<R> private readonly store: Store<R>
readonly dispose: () => void readonly dispose: () => void
private state: HistoryRecorderState = HistoryRecorderState.Recording private mode: TLHistoryMode = 'record'
private readonly pendingDiff = new PendingDiff<R>() private readonly pendingDiff = new PendingDiff<R>()
/** @internal */ /** @internal */
stacks = atom( stacks = atom(
@ -47,18 +41,18 @@ export class HistoryManager<R extends UnknownRecord> {
this.dispose = this.store.addHistoryInterceptor((entry, source) => { this.dispose = this.store.addHistoryInterceptor((entry, source) => {
if (source !== 'user') return if (source !== 'user') return
switch (this.state) { switch (this.mode) {
case HistoryRecorderState.Recording: case 'record':
this.pendingDiff.apply(entry.changes) this.pendingDiff.apply(entry.changes)
this.stacks.update(({ undos }) => ({ undos, redos: stack() })) this.stacks.update(({ undos }) => ({ undos, redos: stack() }))
break break
case HistoryRecorderState.RecordingPreserveRedoStack: case 'record-preserveRedoStack':
this.pendingDiff.apply(entry.changes) this.pendingDiff.apply(entry.changes)
break break
case HistoryRecorderState.Paused: case 'ignore':
break break
default: default:
exhaustiveSwitchError(this.state) exhaustiveSwitchError(this.mode)
} }
}) })
} }
@ -73,8 +67,6 @@ export class HistoryManager<R extends UnknownRecord> {
})) }))
} }
onBatchComplete: () => void = () => void null
getNumUndos() { getNumUndos() {
return this.stacks.get().undos.length + (this.pendingDiff.isEmpty() ? 0 : 1) return this.stacks.get().undos.length + (this.pendingDiff.isEmpty() ? 0 : 1)
} }
@ -82,39 +74,34 @@ export class HistoryManager<R extends UnknownRecord> {
return this.stacks.get().redos.length return this.stacks.get().redos.length
} }
/** @internal */ runInMode(mode: TLHistoryMode | undefined | null, fn: () => void) {
_isInBatch = false if (!mode) {
batch = (fn: () => void, opts?: TLHistoryBatchOptions) => { fn()
const previousState = this.state return this
this.state = opts?.history ? modeToState[opts.history] : this.state }
const previousMode = this.mode
this.mode = mode
try { try {
if (this._isInBatch) { transact(fn)
fn()
return this
}
this._isInBatch = true
try {
transact(() => {
fn()
this.onBatchComplete()
})
} catch (error) {
this.annotateError(error)
throw error
} finally {
this._isInBatch = false
}
return this return this
} finally { } finally {
this.state = previousState this.mode = previousMode
} }
} }
ignore(fn: () => void) { ignore(fn: () => void) {
return this.batch(fn, { history: 'ignore' }) return this.runInMode('ignore', fn)
}
record(fn: () => void) {
return this.runInMode('record', fn)
}
recordPreservingRedoStack(fn: () => void) {
return this.runInMode('record-preserveRedoStack', fn)
} }
// History // History
@ -125,8 +112,8 @@ export class HistoryManager<R extends UnknownRecord> {
pushToRedoStack: boolean pushToRedoStack: boolean
toMark?: string toMark?: string
}) => { }) => {
const previousState = this.state const previousState = this.mode
this.state = HistoryRecorderState.Paused this.mode = 'ignore'
try { try {
let { undos, redos } = this.stacks.get() let { undos, redos } = this.stacks.get()
@ -183,7 +170,7 @@ export class HistoryManager<R extends UnknownRecord> {
this.store.ensureStoreIsUsable() this.store.ensureStoreIsUsable()
this.stacks.set({ undos, redos }) this.stacks.set({ undos, redos })
} finally { } finally {
this.state = previousState this.mode = previousState
} }
return this return this
@ -196,8 +183,8 @@ export class HistoryManager<R extends UnknownRecord> {
} }
redo = () => { redo = () => {
const previousState = this.state const previousState = this.mode
this.state = HistoryRecorderState.Paused this.mode = 'ignore'
try { try {
this.flushPendingDiff() this.flushPendingDiff()
@ -231,7 +218,7 @@ export class HistoryManager<R extends UnknownRecord> {
this.store.ensureStoreIsUsable() this.store.ensureStoreIsUsable()
this.stacks.set({ undos, redos }) this.stacks.set({ undos, redos })
} finally { } finally {
this.state = previousState this.mode = previousState
} }
return this return this
@ -270,17 +257,11 @@ export class HistoryManager<R extends UnknownRecord> {
undos: undos.toArray(), undos: undos.toArray(),
redos: redos.toArray(), redos: redos.toArray(),
pendingDiff: this.pendingDiff.debug(), pendingDiff: this.pendingDiff.debug(),
state: this.state, state: this.mode,
} }
} }
} }
const modeToState = {
record: HistoryRecorderState.Recording,
'record-preserveRedoStack': HistoryRecorderState.RecordingPreserveRedoStack,
ignore: HistoryRecorderState.Paused,
} as const
class PendingDiff<R extends UnknownRecord> { class PendingDiff<R extends UnknownRecord> {
private diff = createEmptyRecordsDiff<R>() private diff = createEmptyRecordsDiff<R>()
private isEmptyAtom = atom('PendingDiff.isEmpty', true) private isEmptyAtom = atom('PendingDiff.isEmpty', true)

Wyświetl plik

@ -86,96 +86,94 @@ export class ScribbleManager {
*/ */
tick = (elapsed: number) => { tick = (elapsed: number) => {
if (this.scribbleItems.size === 0) return if (this.scribbleItems.size === 0) return
this.editor.batch(() => { this.scribbleItems.forEach((item) => {
this.scribbleItems.forEach((item) => { // let the item get at least eight points before
// let the item get at least eight points before // switching from starting to active
// switching from starting to active if (item.scribble.state === 'starting') {
if (item.scribble.state === 'starting') { const { next, prev } = item
const { next, prev } = item if (next && next !== prev) {
item.prev = next
item.scribble.points.push(next)
}
if (item.scribble.points.length > 8) {
item.scribble.state = 'active'
}
return
}
if (item.delayRemaining > 0) {
item.delayRemaining = Math.max(0, item.delayRemaining - elapsed)
}
item.timeoutMs += elapsed
if (item.timeoutMs >= 16) {
item.timeoutMs = 0
}
const { delayRemaining, timeoutMs, prev, next, scribble } = item
switch (scribble.state) {
case 'active': {
if (next && next !== prev) { if (next && next !== prev) {
item.prev = next item.prev = next
item.scribble.points.push(next) scribble.points.push(next)
}
if (item.scribble.points.length > 8) { // If we've run out of delay, then shrink the scribble from the start
item.scribble.state = 'active' if (delayRemaining === 0) {
} if (scribble.points.length > 8) {
return
}
if (item.delayRemaining > 0) {
item.delayRemaining = Math.max(0, item.delayRemaining - elapsed)
}
item.timeoutMs += elapsed
if (item.timeoutMs >= 16) {
item.timeoutMs = 0
}
const { delayRemaining, timeoutMs, prev, next, scribble } = item
switch (scribble.state) {
case 'active': {
if (next && next !== prev) {
item.prev = next
scribble.points.push(next)
// If we've run out of delay, then shrink the scribble from the start
if (delayRemaining === 0) {
if (scribble.points.length > 8) {
scribble.points.shift()
}
}
} else {
// While not moving, shrink the scribble from the start
if (timeoutMs === 0) {
if (scribble.points.length > 1) {
scribble.points.shift()
} else {
// Reset the item's delay
item.delayRemaining = scribble.delay
}
}
}
break
}
case 'stopping': {
if (item.delayRemaining === 0) {
if (timeoutMs === 0) {
// If the scribble is down to one point, we're done!
if (scribble.points.length === 1) {
this.scribbleItems.delete(item.id) // Remove the scribble
return
}
if (scribble.shrink) {
// Drop the scribble's size as it shrinks
scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink))
}
// Drop the scribble's first point (its tail)
scribble.points.shift() scribble.points.shift()
} }
} }
break } else {
} // While not moving, shrink the scribble from the start
case 'paused': { if (timeoutMs === 0) {
// Nothing to do while paused. if (scribble.points.length > 1) {
break scribble.points.shift()
} else {
// Reset the item's delay
item.delayRemaining = scribble.delay
}
}
} }
break
} }
}) case 'stopping': {
if (item.delayRemaining === 0) {
if (timeoutMs === 0) {
// If the scribble is down to one point, we're done!
if (scribble.points.length === 1) {
this.scribbleItems.delete(item.id) // Remove the scribble
return
}
// The object here will get frozen into the record, so we need to if (scribble.shrink) {
// create a copies of the parts that what we'll be mutating later. // Drop the scribble's size as it shrinks
this.editor.updateInstanceState({ scribble.size = Math.max(1, scribble.size * (1 - scribble.shrink))
scribbles: Array.from(this.scribbleItems.values()) }
.map(({ scribble }) => ({
...scribble, // Drop the scribble's first point (its tail)
points: [...scribble.points], scribble.points.shift()
})) }
.slice(-5), // limit to three as a minor sanity check }
}) break
}
case 'paused': {
// Nothing to do while paused.
break
}
}
})
// The object here will get frozen into the record, so we need to
// create a copies of the parts that what we'll be mutating later.
this.editor.updateInstanceState({
scribbles: Array.from(this.scribbleItems.values())
.map(({ scribble }) => ({
...scribble,
points: [...scribble.points],
}))
.slice(-5), // limit to three as a minor sanity check
}) })
} }
} }

Wyświetl plik

@ -1,36 +1,38 @@
import { TLRecord, TLStore } from '@tldraw/tlschema' import { Store, UnknownRecord } from '@tldraw/store'
/** @public */ /** @public */
export type TLBeforeCreateHandler<R extends TLRecord> = (record: R, source: 'remote' | 'user') => R export type TLBeforeCreateHandler<R extends UnknownRecord> = (
record: R,
source: 'remote' | 'user'
) => R
/** @public */ /** @public */
export type TLAfterCreateHandler<R extends TLRecord> = ( export type TLAfterCreateHandler<R extends UnknownRecord> = (
record: R, record: R,
source: 'remote' | 'user' source: 'remote' | 'user'
) => void ) => void
/** @public */ /** @public */
export type TLBeforeChangeHandler<R extends TLRecord> = ( export type TLBeforeChangeHandler<R extends UnknownRecord> = (
prev: R, prev: R,
next: R, next: R,
source: 'remote' | 'user' source: 'remote' | 'user'
) => R ) => R
/** @public */ /** @public */
export type TLAfterChangeHandler<R extends TLRecord> = ( export type TLAfterChangeHandler<R extends UnknownRecord> = (
prev: R, prev: R,
next: R, next: R,
source: 'remote' | 'user' source: 'remote' | 'user'
) => void ) => void
/** @public */ /** @public */
export type TLBeforeDeleteHandler<R extends TLRecord> = ( export type TLBeforeDeleteHandler<R extends UnknownRecord> = (
record: R, record: R,
source: 'remote' | 'user' source: 'remote' | 'user'
) => void | false ) => void | false
/** @public */ /** @public */
export type TLAfterDeleteHandler<R extends TLRecord> = ( export type TLAfterDeleteHandler<R extends UnknownRecord> = (
record: R, record: R,
source: 'remote' | 'user' source: 'remote' | 'user'
) => void ) => void
/** @public */ export type TLCompleteHandler = (source: 'remote' | 'user') => void
export type TLBatchCompleteHandler = () => void
/** /**
* The side effect manager (aka a "correct state enforcer") is responsible * The side effect manager (aka a "correct state enforcer") is responsible
@ -40,17 +42,10 @@ export type TLBatchCompleteHandler = () => void
* *
* @public * @public
*/ */
export class SideEffectManager< export class SideEffectManager<R extends UnknownRecord> {
CTX extends { constructor(public readonly store: Store<R>) {
store: TLStore store.onBeforeCreate = (record, source) => {
history: { onBatchComplete: () => void } const handlers = this._beforeCreateHandlers[record.typeName as R['typeName']]
},
> {
constructor(public editor: CTX) {
editor.store.onBeforeCreate = (record, source) => {
const handlers = this._beforeCreateHandlers[
record.typeName
] as TLBeforeCreateHandler<TLRecord>[]
if (handlers) { if (handlers) {
let r = record let r = record
for (const handler of handlers) { for (const handler of handlers) {
@ -62,10 +57,8 @@ export class SideEffectManager<
return record return record
} }
editor.store.onAfterCreate = (record, source) => { store.onAfterCreate = (record, source) => {
const handlers = this._afterCreateHandlers[ const handlers = this._afterCreateHandlers[record.typeName as R['typeName']]
record.typeName
] as TLAfterCreateHandler<TLRecord>[]
if (handlers) { if (handlers) {
for (const handler of handlers) { for (const handler of handlers) {
handler(record, source) handler(record, source)
@ -73,10 +66,8 @@ export class SideEffectManager<
} }
} }
editor.store.onBeforeChange = (prev, next, source) => { store.onBeforeChange = (prev, next, source) => {
const handlers = this._beforeChangeHandlers[ const handlers = this._beforeChangeHandlers[next.typeName as R['typeName']]
next.typeName
] as TLBeforeChangeHandler<TLRecord>[]
if (handlers) { if (handlers) {
let r = next let r = next
for (const handler of handlers) { for (const handler of handlers) {
@ -88,8 +79,8 @@ export class SideEffectManager<
return next return next
} }
editor.store.onAfterChange = (prev, next, source) => { store.onAfterChange = (prev, next, source) => {
const handlers = this._afterChangeHandlers[next.typeName] as TLAfterChangeHandler<TLRecord>[] const handlers = this._afterChangeHandlers[next.typeName as R['typeName']]
if (handlers) { if (handlers) {
for (const handler of handlers) { for (const handler of handlers) {
handler(prev, next, source) handler(prev, next, source)
@ -97,10 +88,8 @@ export class SideEffectManager<
} }
} }
editor.store.onBeforeDelete = (record, source) => { store.onBeforeDelete = (record, source) => {
const handlers = this._beforeDeleteHandlers[ const handlers = this._beforeDeleteHandlers[record.typeName as R['typeName']]
record.typeName
] as TLBeforeDeleteHandler<TLRecord>[]
if (handlers) { if (handlers) {
for (const handler of handlers) { for (const handler of handlers) {
if (handler(record, source) === false) { if (handler(record, source) === false) {
@ -110,10 +99,8 @@ export class SideEffectManager<
} }
} }
editor.store.onAfterDelete = (record, source) => { store.onAfterDelete = (record, source) => {
const handlers = this._afterDeleteHandlers[ const handlers = this._afterDeleteHandlers[record.typeName as R['typeName']]
record.typeName
] as TLAfterDeleteHandler<TLRecord>[]
if (handlers) { if (handlers) {
for (const handler of handlers) { for (const handler of handlers) {
handler(record, source) handler(record, source)
@ -121,49 +108,56 @@ export class SideEffectManager<
} }
} }
editor.history.onBatchComplete = () => { store.onAfterAtomic = (source) => {
this._batchCompleteHandlers.forEach((fn) => fn()) const handlers = this._completeHandlers
if (handlers) {
for (const handler of handlers) {
handler(source)
}
}
} }
} }
private _beforeCreateHandlers: Partial<{ private _beforeCreateHandlers: Partial<{
[K in TLRecord['typeName']]: TLBeforeCreateHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLBeforeCreateHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _afterCreateHandlers: Partial<{ private _afterCreateHandlers: Partial<{
[K in TLRecord['typeName']]: TLAfterCreateHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLAfterCreateHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _beforeChangeHandlers: Partial<{ private _beforeChangeHandlers: Partial<{
[K in TLRecord['typeName']]: TLBeforeChangeHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLBeforeChangeHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _afterChangeHandlers: Partial<{ private _afterChangeHandlers: Partial<{
[K in TLRecord['typeName']]: TLAfterChangeHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLAfterChangeHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _beforeDeleteHandlers: Partial<{ private _beforeDeleteHandlers: Partial<{
[K in TLRecord['typeName']]: TLBeforeDeleteHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLBeforeDeleteHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _afterDeleteHandlers: Partial<{ private _afterDeleteHandlers: Partial<{
[K in TLRecord['typeName']]: TLAfterDeleteHandler<TLRecord & { typeName: K }>[] [K in R['typeName']]: TLAfterDeleteHandler<R & { typeName: K }>[]
}> = {} }> = {}
private _completeHandlers: TLCompleteHandler[] = []
private _batchCompleteHandlers: TLBatchCompleteHandler[] = []
/** /**
* Internal helper for registering a bunch of side effects at once and keeping them organized. * Internal helper for registering a bunch of side effects at once and keeping them organized.
* @internal * @internal
*/ */
register(handlersByType: { register(
[R in TLRecord as R['typeName']]?: { handlersByType: {
beforeCreate?: TLBeforeCreateHandler<R> [T in R as T['typeName']]?: {
afterCreate?: TLAfterCreateHandler<R> beforeCreate?: TLBeforeCreateHandler<T>
beforeChange?: TLBeforeChangeHandler<R> afterCreate?: TLAfterCreateHandler<T>
afterChange?: TLAfterChangeHandler<R> beforeChange?: TLBeforeChangeHandler<T>
beforeDelete?: TLBeforeDeleteHandler<R> afterChange?: TLAfterChangeHandler<T>
afterDelete?: TLAfterDeleteHandler<R> beforeDelete?: TLBeforeDeleteHandler<T>
} afterDelete?: TLAfterDeleteHandler<T>
}) { }
} & { complete?: TLCompleteHandler }
) {
const disposes: (() => void)[] = [] const disposes: (() => void)[] = []
if (handlersByType.complete) {
this._completeHandlers.push(handlersByType.complete)
}
for (const [type, handlers] of Object.entries(handlersByType) as any) { for (const [type, handlers] of Object.entries(handlersByType) as any) {
if (handlers?.beforeCreate) { if (handlers?.beforeCreate) {
disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate)) disposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate))
@ -216,9 +210,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerBeforeCreateHandler<T extends TLRecord['typeName']>( registerBeforeCreateHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLBeforeCreateHandler<TLRecord & { typeName: T }> handler: TLBeforeCreateHandler<R & { typeName: T }>
) { ) {
const handlers = this._beforeCreateHandlers[typeName] as TLBeforeCreateHandler<any>[] const handlers = this._beforeCreateHandlers[typeName] as TLBeforeCreateHandler<any>[]
if (!handlers) this._beforeCreateHandlers[typeName] = [] if (!handlers) this._beforeCreateHandlers[typeName] = []
@ -246,9 +240,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerAfterCreateHandler<T extends TLRecord['typeName']>( registerAfterCreateHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLAfterCreateHandler<TLRecord & { typeName: T }> handler: TLAfterCreateHandler<R & { typeName: T }>
) { ) {
const handlers = this._afterCreateHandlers[typeName] as TLAfterCreateHandler<any>[] const handlers = this._afterCreateHandlers[typeName] as TLAfterCreateHandler<any>[]
if (!handlers) this._afterCreateHandlers[typeName] = [] if (!handlers) this._afterCreateHandlers[typeName] = []
@ -280,9 +274,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerBeforeChangeHandler<T extends TLRecord['typeName']>( registerBeforeChangeHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLBeforeChangeHandler<TLRecord & { typeName: T }> handler: TLBeforeChangeHandler<R & { typeName: T }>
) { ) {
const handlers = this._beforeChangeHandlers[typeName] as TLBeforeChangeHandler<any>[] const handlers = this._beforeChangeHandlers[typeName] as TLBeforeChangeHandler<any>[]
if (!handlers) this._beforeChangeHandlers[typeName] = [] if (!handlers) this._beforeChangeHandlers[typeName] = []
@ -309,9 +303,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerAfterChangeHandler<T extends TLRecord['typeName']>( registerAfterChangeHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLAfterChangeHandler<TLRecord & { typeName: T }> handler: TLAfterChangeHandler<R & { typeName: T }>
) { ) {
const handlers = this._afterChangeHandlers[typeName] as TLAfterChangeHandler<any>[] const handlers = this._afterChangeHandlers[typeName] as TLAfterChangeHandler<any>[]
if (!handlers) this._afterChangeHandlers[typeName] = [] if (!handlers) this._afterChangeHandlers[typeName] = []
@ -340,9 +334,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerBeforeDeleteHandler<T extends TLRecord['typeName']>( registerBeforeDeleteHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLBeforeDeleteHandler<TLRecord & { typeName: T }> handler: TLBeforeDeleteHandler<R & { typeName: T }>
) { ) {
const handlers = this._beforeDeleteHandlers[typeName] as TLBeforeDeleteHandler<any>[] const handlers = this._beforeDeleteHandlers[typeName] as TLBeforeDeleteHandler<any>[]
if (!handlers) this._beforeDeleteHandlers[typeName] = [] if (!handlers) this._beforeDeleteHandlers[typeName] = []
@ -372,9 +366,9 @@ export class SideEffectManager<
* @param typeName - The type of record to listen for * @param typeName - The type of record to listen for
* @param handler - The handler to call * @param handler - The handler to call
*/ */
registerAfterDeleteHandler<T extends TLRecord['typeName']>( registerAfterDeleteHandler<T extends R['typeName']>(
typeName: T, typeName: T,
handler: TLAfterDeleteHandler<TLRecord & { typeName: T }> handler: TLAfterDeleteHandler<R & { typeName: T }>
) { ) {
const handlers = this._afterDeleteHandlers[typeName] as TLAfterDeleteHandler<any>[] const handlers = this._afterDeleteHandlers[typeName] as TLAfterDeleteHandler<any>[]
if (!handlers) this._afterDeleteHandlers[typeName] = [] if (!handlers) this._afterDeleteHandlers[typeName] = []
@ -383,7 +377,7 @@ export class SideEffectManager<
} }
/** /**
* Register a handler to be called when a store completes a batch. * Register a handler to be called when the store completes an operation.
* *
* @example * @example
* ```ts * ```ts
@ -394,7 +388,7 @@ export class SideEffectManager<
* editor.selectAll() * editor.selectAll()
* expect(count).toBe(1) * expect(count).toBe(1)
* *
* editor.batch(() => { * editor.store.atomic(() => {
* editor.selectNone() * editor.selectNone()
* editor.selectAll() * editor.selectAll()
* }) * })
@ -406,9 +400,9 @@ export class SideEffectManager<
* *
* @public * @public
*/ */
registerBatchCompleteHandler(handler: TLBatchCompleteHandler) { registerCompleteHandler(handler: TLCompleteHandler) {
this._batchCompleteHandlers.push(handler) this._completeHandlers.push(handler)
return () => remove(this._batchCompleteHandlers, handler) return () => remove(this._completeHandlers, handler)
} }
} }

Wyświetl plik

@ -8,7 +8,6 @@ export interface TLEventMap {
mount: [] mount: []
'max-shapes': [{ name: string; pageId: TLPageId; count: number }] 'max-shapes': [{ name: string; pageId: TLPageId; count: number }]
change: [HistoryEntry<TLRecord>] change: [HistoryEntry<TLRecord>]
update: []
crash: [{ error: unknown }] crash: [{ error: unknown }]
'stop-camera-animation': [] 'stop-camera-animation': []
'stop-following': [] 'stop-following': []

Wyświetl plik

@ -17,11 +17,13 @@ export type TLHistoryEntry<R extends UnknownRecord> = TLHistoryMark | TLHistoryD
/** @public */ /** @public */
export interface TLHistoryBatchOptions { export interface TLHistoryBatchOptions {
/** history?: TLHistoryMode
* How should this change interact with the history stack?
* - record: Add to the undo stack and clear the redo stack
* - record-preserveRedoStack: Add to the undo stack but do not clear the redo stack
* - ignore: Do not add to the undo stack or the redo stack
*/
history?: 'record' | 'record-preserveRedoStack' | 'ignore'
} }
/**
* How should this change interact with the history stack?
* - record: Add to the undo stack and clear the redo stack
* - record-preserveRedoStack: Add to the undo stack but do not clear the redo stack
* - ignore: Do not add to the undo stack or the redo stack
*/
export type TLHistoryMode = 'record' | 'record-preserveRedoStack' | 'ignore'

Wyświetl plik

@ -265,6 +265,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
markAsPossiblyCorrupted(): void; markAsPossiblyCorrupted(): void;
mergeRemoteChanges: (fn: () => void) => void; mergeRemoteChanges: (fn: () => void) => void;
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>; migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>;
onAfterAtomic?: (source: 'remote' | 'user') => void;
onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void; onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void;
onAfterCreate?: (record: R, source: 'remote' | 'user') => void; onAfterCreate?: (record: R, source: 'remote' | 'user') => void;
onAfterDelete?: (prev: R, source: 'remote' | 'user') => void; onAfterDelete?: (prev: R, source: 'remote' | 'user') => void;

Wyświetl plik

@ -4108,6 +4108,36 @@
"isAbstract": false, "isAbstract": false,
"name": "migrateSnapshot" "name": "migrateSnapshot"
}, },
{
"kind": "Property",
"canonicalReference": "@tldraw/store!Store#onAfterAtomic:member",
"docComment": "/**\n * A callback fired after an atomic operation is completed.\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "onAfterAtomic?: "
},
{
"kind": "Content",
"text": "(source: 'remote' | 'user') => void"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onAfterAtomic",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{ {
"kind": "Property", "kind": "Property",
"canonicalReference": "@tldraw/store!Store#onAfterChange:member", "canonicalReference": "@tldraw/store!Store#onAfterChange:member",

Wyświetl plik

@ -340,6 +340,11 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
*/ */
onAfterDelete?: (prev: R, source: 'remote' | 'user') => void onAfterDelete?: (prev: R, source: 'remote' | 'user') => void
/**
* A callback fired after an atomic operation is completed.
*/
onAfterAtomic?: (source: 'remote' | 'user') => void
// used to avoid running callbacks when rolling back changes in sync client // used to avoid running callbacks when rolling back changes in sync client
private _runCallbacks = true private _runCallbacks = true
@ -682,6 +687,10 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* @public * @public
*/ */
mergeRemoteChanges = (fn: () => void) => { mergeRemoteChanges = (fn: () => void) => {
if (this._isInAtomicOp) {
throw new Error('Cannot call `mergeRemoteChanges` from within an atomic operation')
}
if (this.isMergingRemoteChanges) { if (this.isMergingRemoteChanges) {
return fn() return fn()
} }
@ -827,6 +836,9 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
} }
private flushAtomicCallbacks() { private flushAtomicCallbacks() {
let updateDepth = 0 let updateDepth = 0
let didAnythingHappen = false
// first, we fire any pending after events:
while (this.pendingAfterEvents) { while (this.pendingAfterEvents) {
const events = this.pendingAfterEvents const events = this.pendingAfterEvents
this.pendingAfterEvents = null this.pendingAfterEvents = null
@ -840,15 +852,27 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
for (const { before, after, source } of events.values()) { for (const { before, after, source } of events.values()) {
if (before && after) { if (before && after) {
didAnythingHappen = true
this.onAfterChange?.(before, after, source) this.onAfterChange?.(before, after, source)
} else if (before && !after) { } else if (before && !after) {
didAnythingHappen = true
this.onAfterDelete?.(before, source) this.onAfterDelete?.(before, source)
} else if (!before && after) { } else if (!before && after) {
didAnythingHappen = true
this.onAfterCreate?.(after, source) this.onAfterCreate?.(after, source)
} }
} }
} }
// then we fire the atomic callback
if (didAnythingHappen) {
this.onAfterAtomic?.(this.isMergingRemoteChanges ? 'remote' : 'user')
// that might have caused more changes, so we need to flush again:
this.flushAtomicCallbacks()
}
} }
private _isInAtomicOp = false private _isInAtomicOp = false
/** @internal */ /** @internal */
atomic<T>(fn: () => T, runCallbacks = true): T { atomic<T>(fn: () => T, runCallbacks = true): T {

Wyświetl plik

@ -377,21 +377,19 @@ export function registerDefaultExternalContentHandlers(
} }
} }
editor.batch(() => { if (shouldAlsoCreateAsset) {
if (shouldAlsoCreateAsset) { editor.createAssets([asset])
editor.createAssets([asset]) }
}
editor.updateShapes([ editor.updateShapes([
{ {
id: shape.id, id: shape.id,
type: shape.type, type: shape.type,
props: { props: {
assetId: asset.id, assetId: asset.id,
},
}, },
]) },
}) ])
}) })
} }
@ -459,19 +457,17 @@ export async function createShapesForAssets(
} }
} }
editor.batch(() => { // Create any assets
// Create any assets const assetsToCreate = assets.filter((asset) => !editor.getAsset(asset.id))
const assetsToCreate = assets.filter((asset) => !editor.getAsset(asset.id)) if (assetsToCreate.length) {
if (assetsToCreate.length) { editor.createAssets(assetsToCreate)
editor.createAssets(assetsToCreate) }
}
// Create the shapes // Create the shapes
editor.createShapes(partials).select(...partials.map((p) => p.id)) editor.createShapes(partials).select(...partials.map((p) => p.id))
// Re-position shapes so that the center of the group is at the provided point // Re-position shapes so that the center of the group is at the provided point
centerSelectionAroundPoint(editor, position) centerSelectionAroundPoint(editor, position)
})
return partials.map((p) => p.id) return partials.map((p) => p.id)
} }
@ -522,10 +518,8 @@ export function createEmptyBookmarkShape(
}, },
} }
editor.batch(() => { editor.createShapes([partial]).select(partial.id)
editor.createShapes([partial]).select(partial.id) centerSelectionAroundPoint(editor, position)
centerSelectionAroundPoint(editor, position)
})
return editor.getShape(partial.id) as TLBookmarkShape return editor.getShape(partial.id) as TLBookmarkShape
} }

Wyświetl plik

@ -180,17 +180,15 @@ const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TL
return return
} }
editor.batch(() => { // Create the new asset
// Create the new asset editor.createAssets([asset])
editor.createAssets([asset])
// And update the shape // And update the shape
editor.updateShapes<TLBookmarkShape>([ editor.updateShapes<TLBookmarkShape>([
{ {
id: shape.id, id: shape.id,
type: shape.type, type: shape.type,
props: { assetId: asset.id }, props: { assetId: asset.id },
}, },
]) ])
})
}, 500) }, 500)

Wyświetl plik

@ -32,9 +32,7 @@ export class DragAndDropManager {
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) { private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
this.droppingNodeTimer = setTimeout(() => { this.droppingNodeTimer = setTimeout(() => {
this.editor.batch(() => { this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
})
this.droppingNodeTimer = null this.droppingNodeTimer = null
}, duration) }, duration)
} }

Wyświetl plik

@ -146,20 +146,18 @@ export class PointingShape extends StateNode {
labelGeometry.bounds.containsPoint(pointInShapeSpace, 0) && labelGeometry.bounds.containsPoint(pointInShapeSpace, 0) &&
labelGeometry.hitTestPoint(pointInShapeSpace) labelGeometry.hitTestPoint(pointInShapeSpace)
) { ) {
this.editor.batch(() => { this.editor.mark('editing on pointer up')
this.editor.mark('editing on pointer up') this.editor.select(selectingShape.id)
this.editor.select(selectingShape.id)
const util = this.editor.getShapeUtil(selectingShape) const util = this.editor.getShapeUtil(selectingShape)
if (this.editor.getInstanceState().isReadonly) { if (this.editor.getInstanceState().isReadonly) {
if (!util.canEditInReadOnly(selectingShape)) { if (!util.canEditInReadOnly(selectingShape)) {
return return
}
} }
}
this.editor.setEditingShape(selectingShape.id) this.editor.setEditingShape(selectingShape.id)
this.editor.setCurrentTool('select.editing_shape') this.editor.setCurrentTool('select.editing_shape')
})
return return
} }
} }

Wyświetl plik

@ -294,7 +294,5 @@ function createNShapes(editor: Editor, n: number) {
} }
} }
editor.batch(() => { editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
})
} }

Wyświetl plik

@ -251,13 +251,11 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
const handleCreatePageClick = useCallback(() => { const handleCreatePageClick = useCallback(() => {
if (isReadonlyMode) return if (isReadonlyMode) return
editor.batch(() => { editor.mark('creating page')
editor.mark('creating page') const newPageId = PageRecordType.createId()
const newPageId = PageRecordType.createId() editor.createPage({ name: msg('page-menu.new-page-initial-name'), id: newPageId })
editor.createPage({ name: msg('page-menu.new-page-initial-name'), id: newPageId }) editor.setCurrentPage(newPageId)
editor.setCurrentPage(newPageId) setIsEditing(true)
setIsEditing(true)
})
}, [editor, msg, isReadonlyMode]) }, [editor, msg, isReadonlyMode])
return ( return (
@ -400,10 +398,8 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
editor.renamePage(page.id, name) editor.renamePage(page.id, name)
} }
} else { } else {
editor.batch(() => { setIsEditing(true)
setIsEditing(true) editor.setCurrentPage(page.id)
editor.setCurrentPage(page.id)
})
} }
}} }}
/> />

Wyświetl plik

@ -78,13 +78,11 @@ function useStyleChangeCallback() {
return React.useMemo( return React.useMemo(
() => () =>
function handleStyleChange<T>(style: StyleProp<T>, value: T) { function handleStyleChange<T>(style: StyleProp<T>, value: T) {
editor.batch(() => { if (editor.isIn('select')) {
if (editor.isIn('select')) { editor.setStyleForSelectedShapes(style, value)
editor.setStyleForSelectedShapes(style, value) }
} editor.setStyleForNextShapes(style, value)
editor.setStyleForNextShapes(style, value) editor.updateInstanceState({ isChangingStyle: true })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: style.id, value: value as string }) trackEvent('set-style', { source: 'style-panel', id: style.id, value: value as string })
}, },
@ -327,13 +325,11 @@ export function OpacitySlider() {
const handleOpacityValueChange = React.useCallback( const handleOpacityValueChange = React.useCallback(
(value: number) => { (value: number) => {
const item = tldrawSupportedOpacities[value] const item = tldrawSupportedOpacities[value]
editor.batch(() => { if (editor.isIn('select')) {
if (editor.isIn('select')) { editor.setOpacityForSelectedShapes(item)
editor.setOpacityForSelectedShapes(item) }
} editor.setOpacityForNextShapes(item)
editor.setOpacityForNextShapes(item) editor.updateInstanceState({ isChangingStyle: true })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source: 'style-panel', id: 'opacity', value }) trackEvent('set-style', { source: 'style-panel', id: 'opacity', value })
}, },

Wyświetl plik

@ -386,40 +386,38 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
if (!canApplySelectionAction()) return if (!canApplySelectionAction()) return
if (mustGoBackToSelectToolFirst()) return if (mustGoBackToSelectToolFirst()) return
editor.batch(() => { trackEvent('convert-to-bookmark', { source })
trackEvent('convert-to-bookmark', { source }) const shapes = editor.getSelectedShapes()
const shapes = editor.getSelectedShapes()
const createList: TLShapePartial[] = [] const createList: TLShapePartial[] = []
const deleteList: TLShapeId[] = [] const deleteList: TLShapeId[] = []
for (const shape of shapes) { for (const shape of shapes) {
if (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed') || !shape.props.url) if (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed') || !shape.props.url)
continue continue
const newPos = new Vec(shape.x, shape.y) const newPos = new Vec(shape.x, shape.y)
newPos.rot(-shape.rotation) newPos.rot(-shape.rotation)
newPos.add(new Vec(shape.props.w / 2 - 300 / 2, shape.props.h / 2 - 320 / 2)) // see bookmark shape util newPos.add(new Vec(shape.props.w / 2 - 300 / 2, shape.props.h / 2 - 320 / 2)) // see bookmark shape util
newPos.rot(shape.rotation) newPos.rot(shape.rotation)
const partial: TLShapePartial<TLBookmarkShape> = { const partial: TLShapePartial<TLBookmarkShape> = {
id: createShapeId(), id: createShapeId(),
type: 'bookmark', type: 'bookmark',
rotation: shape.rotation, rotation: shape.rotation,
x: newPos.x, x: newPos.x,
y: newPos.y, y: newPos.y,
opacity: 1, opacity: 1,
props: { props: {
url: shape.props.url, url: shape.props.url,
}, },
}
createList.push(partial)
deleteList.push(shape.id)
} }
editor.mark('convert shapes to bookmark') createList.push(partial)
editor.deleteShapes(deleteList) deleteList.push(shape.id)
editor.createShapes(createList) }
})
editor.mark('convert shapes to bookmark')
editor.deleteShapes(deleteList)
editor.createShapes(createList)
}, },
}, },
{ {
@ -431,50 +429,48 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
trackEvent('convert-to-embed', { source }) trackEvent('convert-to-embed', { source })
editor.batch(() => { const ids = editor.getSelectedShapeIds()
const ids = editor.getSelectedShapeIds() const shapes = compact(ids.map((id) => editor.getShape(id)))
const shapes = compact(ids.map((id) => editor.getShape(id)))
const createList: TLShapePartial[] = [] const createList: TLShapePartial[] = []
const deleteList: TLShapeId[] = [] const deleteList: TLShapeId[] = []
for (const shape of shapes) { for (const shape of shapes) {
if (!editor.isShapeOfType<TLBookmarkShape>(shape, 'bookmark')) continue if (!editor.isShapeOfType<TLBookmarkShape>(shape, 'bookmark')) continue
const { url } = shape.props const { url } = shape.props
const embedInfo = getEmbedInfo(shape.props.url) const embedInfo = getEmbedInfo(shape.props.url)
if (!embedInfo) continue if (!embedInfo) continue
if (!embedInfo.definition) continue if (!embedInfo.definition) continue
const { width, height } = embedInfo.definition const { width, height } = embedInfo.definition
const newPos = new Vec(shape.x, shape.y) const newPos = new Vec(shape.x, shape.y)
newPos.rot(-shape.rotation) newPos.rot(-shape.rotation)
newPos.add(new Vec(shape.props.w / 2 - width / 2, shape.props.h / 2 - height / 2)) newPos.add(new Vec(shape.props.w / 2 - width / 2, shape.props.h / 2 - height / 2))
newPos.rot(shape.rotation) newPos.rot(shape.rotation)
const shapeToCreate: TLShapePartial<TLEmbedShape> = { const shapeToCreate: TLShapePartial<TLEmbedShape> = {
id: createShapeId(), id: createShapeId(),
type: 'embed', type: 'embed',
x: newPos.x, x: newPos.x,
y: newPos.y, y: newPos.y,
rotation: shape.rotation, rotation: shape.rotation,
props: { props: {
url: url, url: url,
w: width, w: width,
h: height, h: height,
}, },
}
createList.push(shapeToCreate)
deleteList.push(shape.id)
} }
editor.mark('convert shapes to embed') createList.push(shapeToCreate)
editor.deleteShapes(deleteList) deleteList.push(shape.id)
editor.createShapes(createList) }
})
editor.mark('convert shapes to embed')
editor.deleteShapes(deleteList)
editor.createShapes(createList)
}, },
}, },
{ {
@ -921,14 +917,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '$a', kbd: '$a',
readonlyOk: true, readonlyOk: true,
onSelect(source) { onSelect(source) {
editor.batch(() => { if (mustGoBackToSelectToolFirst()) return
if (mustGoBackToSelectToolFirst()) return
trackEvent('select-all-shapes', { source }) trackEvent('select-all-shapes', { source })
editor.mark('select all kbd') editor.mark('select all kbd')
editor.selectAll() editor.selectAll()
})
}, },
}, },
{ {
@ -1177,12 +1171,10 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
// this needs to be deferred because it causes the menu // this needs to be deferred because it causes the menu
// UI to unmount which puts us in a dodgy state // UI to unmount which puts us in a dodgy state
requestAnimationFrame(() => { requestAnimationFrame(() => {
editor.batch(() => { trackEvent('toggle-focus-mode', { source })
trackEvent('toggle-focus-mode', { source }) clearDialogs()
clearDialogs() clearToasts()
clearToasts() editor.updateInstanceState({ isFocusMode: !editor.getInstanceState().isFocusMode })
editor.updateInstanceState({ isFocusMode: !editor.getInstanceState().isFocusMode })
})
}) })
}, },
}, },
@ -1271,11 +1263,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
onSelect(source) { onSelect(source) {
const newPageId = PageRecordType.createId() const newPageId = PageRecordType.createId()
const ids = editor.getSelectedShapeIds() const ids = editor.getSelectedShapeIds()
editor.batch(() => { editor.mark('move_shapes_to_page')
editor.mark('move_shapes_to_page') editor.createPage({ name: msg('page-menu.new-page-initial-name'), id: newPageId })
editor.createPage({ name: msg('page-menu.new-page-initial-name'), id: newPageId }) editor.moveShapesToPage(ids, newPageId)
editor.moveShapesToPage(ids, newPageId)
})
trackEvent('new-page', { source }) trackEvent('new-page', { source })
}, },
}, },
@ -1285,14 +1275,12 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
kbd: '?t', kbd: '?t',
onSelect(source) { onSelect(source) {
const style = DefaultColorStyle const style = DefaultColorStyle
editor.batch(() => { editor.mark('change-color')
editor.mark('change-color') if (editor.isIn('select')) {
if (editor.isIn('select')) { editor.setStyleForSelectedShapes(style, 'white')
editor.setStyleForSelectedShapes(style, 'white') }
} editor.setStyleForNextShapes(style, 'white')
editor.setStyleForNextShapes(style, 'white') editor.updateInstanceState({ isChangingStyle: true })
editor.updateInstanceState({ isChangingStyle: true })
})
trackEvent('set-style', { source, id: style.id, value: 'white' }) trackEvent('set-style', { source, id: style.id, value: 'white' })
}, },
}, },

Wyświetl plik

@ -12,18 +12,16 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
(isOpen: boolean) => { (isOpen: boolean) => {
rIsOpen.current = isOpen rIsOpen.current = isOpen
editor.batch(() => { if (isOpen) {
if (isOpen) { editor.complete()
editor.complete() editor.addOpenMenu(id)
editor.addOpenMenu(id) } else {
} else { editor.updateInstanceState({
editor.updateInstanceState({ openMenus: editor.getOpenMenus().filter((m) => !m.startsWith(id)),
openMenus: editor.getOpenMenus().filter((m) => !m.startsWith(id)), })
}) }
}
cb?.(isOpen) cb?.(isOpen)
})
}, },
[editor, id, cb] [editor, id, cb]
) )

Wyświetl plik

@ -101,11 +101,9 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
kbd: id === 'rectangle' ? 'r' : id === 'ellipse' ? 'o' : undefined, kbd: id === 'rectangle' ? 'r' : id === 'ellipse' ? 'o' : undefined,
icon: ('geo-' + id) as TLUiIconType, icon: ('geo-' + id) as TLUiIconType,
onSelect(source: TLUiEventSource) { onSelect(source: TLUiEventSource) {
editor.batch(() => { editor.setStyleForNextShapes(GeoShapeGeoStyle, id)
editor.setStyleForNextShapes(GeoShapeGeoStyle, id) editor.setCurrentTool('geo')
editor.setCurrentTool('geo') trackEvent('select-tool', { source, id: `geo-${id}` })
trackEvent('select-tool', { source, id: `geo-${id}` })
})
}, },
})), })),
{ {

Wyświetl plik

@ -17,17 +17,15 @@ export function removeFrame(editor: Editor, ids: TLShapeId[]) {
if (!frames.length) return if (!frames.length) return
const allChildren: TLShapeId[] = [] const allChildren: TLShapeId[] = []
editor.batch(() => { frames.map((frame) => {
frames.map((frame) => { const children = editor.getSortedChildIdsForParent(frame.id)
const children = editor.getSortedChildIdsForParent(frame.id) if (children.length) {
if (children.length) { editor.reparentShapes(children, frame.parentId, frame.index)
editor.reparentShapes(children, frame.parentId, frame.index) allChildren.push(...children)
allChildren.push(...children) }
}
})
editor.setSelectedShapes(allChildren)
editor.deleteShapes(ids)
}) })
editor.setSelectedShapes(allChildren)
editor.deleteShapes(ids)
} }
/** @internal */ /** @internal */
@ -66,28 +64,26 @@ export function fitFrameToContent(editor: Editor, id: TLShapeId, opts = {} as {
if (dx === 0 && dy === 0 && frame.props.w === w && frame.props.h === h) return if (dx === 0 && dy === 0 && frame.props.w === w && frame.props.h === h) return
const diff = new Vec(dx, dy).rot(frame.rotation) const diff = new Vec(dx, dy).rot(frame.rotation)
editor.batch(() => { const changes: TLShapePartial[] = childIds.map((child) => {
const changes: TLShapePartial[] = childIds.map((child) => { const shape = editor.getShape(child)!
const shape = editor.getShape(child)! return {
return { id: shape.id,
id: shape.id, type: shape.type,
type: shape.type, x: shape.x + dx,
x: shape.x + dx, y: shape.y + dy,
y: shape.y + dy, }
}
})
changes.push({
id: frame.id,
type: frame.type,
x: frame.x - diff.x,
y: frame.y - diff.y,
props: {
w,
h,
},
})
editor.updateShapes(changes)
}) })
changes.push({
id: frame.id,
type: frame.type,
x: frame.x - diff.x,
y: frame.y - diff.y,
props: {
w,
h,
},
})
editor.updateShapes(changes)
} }