kopia lustrzana https://github.com/Tldraw/Tldraw
alex/no-batches: no batches
rodzic
3448e53a64
commit
1ecf54f5f7
|
@ -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)
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -139,7 +139,6 @@ export type {
|
||||||
TLAfterChangeHandler,
|
TLAfterChangeHandler,
|
||||||
TLAfterCreateHandler,
|
TLAfterCreateHandler,
|
||||||
TLAfterDeleteHandler,
|
TLAfterDeleteHandler,
|
||||||
TLBatchCompleteHandler,
|
|
||||||
TLBeforeChangeHandler,
|
TLBeforeChangeHandler,
|
||||||
TLBeforeCreateHandler,
|
TLBeforeCreateHandler,
|
||||||
TLBeforeDeleteHandler,
|
TLBeforeDeleteHandler,
|
||||||
|
|
Plik diff jest za duży
Load Diff
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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': []
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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 })
|
||||||
},
|
},
|
||||||
|
|
|
@ -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' })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}` })
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Plik diff jest za duży
Load Diff
Ładowanie…
Reference in New Issue