Editor.run, locked shapes improvements (#4042)
This PR: - creates `Editor.run` (previously `Editor.batch`) - deprecates `Editor.batch` - introduces a `ignoreShapeLock` option top the `Editor.run` method that allows the editor to update and delete locked shapes - fixes a bug with `updateShapes` that allowed updating locked shapes - fixes a bug with `ungroupShapes` that allowed ungrouping locked shapes - makes `Editor.history` private - adds `Editor.squashToMark` - adds `Editor.clearHistory` - removes `History.ignore` - removes `History.onBatchComplete` - makes `_updateCurrentPageState` private ```ts editor.run(() => { editor.updateShape({ ...myLockedShape }) editor.deleteShape(myLockedShape) }, { ignoreShapeLock: true }) ``` It also: ## How it works Normally `updateShape`/`updateShapes` and `deleteShape`/`deleteShapes` do not effect locked shapes. ```ts const myLockedShape = editor.getShape(myShapeId)! // no change from update editor.updateShape({ ...myLockedShape, x: 100 }) expect(editor.getShape(myShapeId)).toMatchObject(myLockedShape) // no change from delete editor.deleteShapes([myLockedShape]) expect(editor.getShape(myShapeId)).toMatchObject(myLockedShape) ``` The new `run` method adds the option to ignore shape lock. ```ts const myLockedShape = editor.getShape(myShapeId)! // update works editor.run(() => { editor.updateShape({ ...myLockedShape, x: 100 }) }, { ignoreShapeLock: true }) expect(editor.getShape(myShapeId)).toMatchObject({ ...myLockedShape, x: 100 }) // delete works editor.run(() => { editor.deleteShapes([myLockedShape]), { ignoreShapeLock: true }) expect(editor.getShape(myShapeId)).toBeUndefined() ``` ## History changes This is a related but not entirely related change in this PR. Previously, we had a few ways to run code that ignored the history. - `editor.history.ignore(() => { ... })` - `editor.batch(() => { ... }, { history: "ignore" })` - `editor.history.batch(() => { ... }, { history: "ignore" })` - `editor.updateCurrentPageState(() => { ... }, { history: "ignore" })` We now have one way to run code that ignores history: - `editor.run(() => { ... }, { history: "ignore" })` ## Design notes We want a user to be able to update or delete locked shapes programmatically. ### Callback vs. method options? We could have added a `{ force: boolean }` property to the `updateShapes` / `deleteShapes` methods, however there are places where those methods are called from other methods (such as `distributeShapes`). If we wanted to make these work, we would have also had to provide a `force` option / bag to those methods. Using a wrapper callback allows for "regular" tldraw editor code to work while allowing for updates and deletes. ### Interaction logic? We don't want this change to effect any of our interaction logic. A lot of our interaction logic depends on identifying which shapes are locked and which shapes aren't. For example, clicking on a locked shape will go to the `pointing_canvas` state rather than the `pointing_shape`. This PR has no effect on that part of the library. It only effects the updateShapes and deleteShapes methods. As an example of this, when `_force` is set to true by default, the only tests that should fail are in `lockedShapes.test.ts`. The "user land" experience of locked shapes is identical to what it is now. ### Change type - [x] `bugfix` - [ ] `improvement` - [x] `feature` - [x] `api` - [ ] `other` ### Test plan 1. Create a shape 2. Lock it 3. From the console, update it 4. From the console, delete it - [x] Unit tests ### Release notes - SDK: Adds `Editor.force()` to permit updating / deleting locked shapes - Fixed a bug that would allow locked shapes to be updated programmatically - Fixed a bug that would allow locked group shapes to be ungrouped programmatically --------- Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
parent
f4ceb581dd
commit
01bc73e750
30 changed files with 572 additions and 286 deletions
|
@ -56,9 +56,12 @@ export function useUrlState(onChangeUrl: (params: UrlStateParams) => void) {
|
||||||
url.searchParams.get(PARAMS.page) ?? 'page:' + url.searchParams.get(PARAMS.p)
|
url.searchParams.get(PARAMS.page) ?? 'page:' + url.searchParams.get(PARAMS.p)
|
||||||
if (newPageId) {
|
if (newPageId) {
|
||||||
if (editor.store.has(newPageId as TLPageId)) {
|
if (editor.store.has(newPageId as TLPageId)) {
|
||||||
editor.history.ignore(() => {
|
editor.run(
|
||||||
editor.setCurrentPage(newPageId as TLPageId)
|
() => {
|
||||||
})
|
editor.setCurrentPage(newPageId as TLPageId)
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ export function useFileSystem({ isMultiplayer }: { isMultiplayer: boolean }): TL
|
||||||
|
|
||||||
editor.store.clear()
|
editor.store.clear()
|
||||||
editor.store.ensureStoreIsUsable()
|
editor.store.ensureStoreIsUsable()
|
||||||
editor.history.clear()
|
editor.clearHistory()
|
||||||
// Put the old bounds back in place
|
// Put the old bounds back in place
|
||||||
editor.updateViewportScreenBounds(bounds)
|
editor.updateViewportScreenBounds(bounds)
|
||||||
editor.updateInstanceState({ isFocused })
|
editor.updateInstanceState({ isFocused })
|
||||||
|
|
|
@ -144,7 +144,7 @@ const CameraOptionsControlPanel = track(() => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
editor.setCameraOptions(cameraOptions)
|
editor.setCameraOptions(cameraOptions)
|
||||||
editor.setCamera(editor.getCamera(), {
|
editor.setCamera(editor.getCamera(), {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
|
|
@ -115,7 +115,7 @@ export function ImageAnnotationEditor({
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset the history
|
// Reset the history
|
||||||
editor.history.clear()
|
editor.clearHistory()
|
||||||
setImageShapeId(shapeId)
|
setImageShapeId(shapeId)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -816,7 +816,8 @@ 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;
|
// @deprecated (undocumented)
|
||||||
|
batch(fn: () => void, opts?: TLEditorRunOptions): this;
|
||||||
bindingUtils: {
|
bindingUtils: {
|
||||||
readonly [K in string]?: BindingUtil<TLUnknownBinding>;
|
readonly [K in string]?: BindingUtil<TLUnknownBinding>;
|
||||||
};
|
};
|
||||||
|
@ -842,6 +843,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
capturedPointerId: null | number;
|
capturedPointerId: null | number;
|
||||||
centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this;
|
centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this;
|
||||||
|
// (undocumented)
|
||||||
|
clearHistory(): this;
|
||||||
clearOpenMenus(): this;
|
clearOpenMenus(): this;
|
||||||
// @internal
|
// @internal
|
||||||
protected _clickManager: ClickManager;
|
protected _clickManager: ClickManager;
|
||||||
|
@ -1076,7 +1079,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
hasExternalAssetHandler(type: TLExternalAssetContent['type']): boolean;
|
hasExternalAssetHandler(type: TLExternalAssetContent['type']): boolean;
|
||||||
readonly history: HistoryManager<TLRecord>;
|
protected readonly history: HistoryManager<TLRecord>;
|
||||||
inputs: {
|
inputs: {
|
||||||
buttons: Set<number>;
|
buttons: Set<number>;
|
||||||
keys: Set<string>;
|
keys: Set<string>;
|
||||||
|
@ -1150,6 +1153,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}): Promise<null | string>;
|
}): Promise<null | string>;
|
||||||
readonly root: StateNode;
|
readonly root: StateNode;
|
||||||
rotateShapesBy(shapes: TLShape[] | TLShapeId[], delta: number): this;
|
rotateShapesBy(shapes: TLShape[] | TLShapeId[], delta: number): this;
|
||||||
|
run(fn: () => void, opts?: TLEditorRunOptions): this;
|
||||||
screenToPage(point: VecLike): Vec;
|
screenToPage(point: VecLike): Vec;
|
||||||
readonly scribbles: ScribbleManager;
|
readonly scribbles: ScribbleManager;
|
||||||
select(...shapes: TLShape[] | TLShapeId[]): this;
|
select(...shapes: TLShape[] | TLShapeId[]): this;
|
||||||
|
@ -1184,6 +1188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
speedThreshold?: number | undefined;
|
speedThreshold?: number | undefined;
|
||||||
}): this;
|
}): this;
|
||||||
readonly snaps: SnapManager;
|
readonly snaps: SnapManager;
|
||||||
|
squashToMark(markId: string): this;
|
||||||
stackShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
|
stackShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical', gap: number): this;
|
||||||
startFollowingUser(userId: string): this;
|
startFollowingUser(userId: string): this;
|
||||||
stopCameraAnimation(): this;
|
stopCameraAnimation(): this;
|
||||||
|
@ -1208,9 +1213,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
updateAssets(assets: TLAssetPartial[]): this;
|
updateAssets(assets: TLAssetPartial[]): this;
|
||||||
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>): this;
|
updateBinding<B extends TLBinding = TLBinding>(partial: TLBindingUpdate<B>): this;
|
||||||
updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this;
|
updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this;
|
||||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: TLHistoryBatchOptions): this;
|
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>): this;
|
||||||
// (undocumented)
|
|
||||||
_updateCurrentPageState: (partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>, historyOptions?: 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'>>, historyOptions?: TLHistoryBatchOptions): this;
|
||||||
updatePage(partial: RequiredKeys<Partial<TLPage>, 'id'>): this;
|
updatePage(partial: RequiredKeys<Partial<TLPage>, 'id'>): this;
|
||||||
|
@ -1562,15 +1565,11 @@ export class HistoryManager<R extends UnknownRecord> {
|
||||||
getNumRedos(): number;
|
getNumRedos(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getNumUndos(): number;
|
getNumUndos(): number;
|
||||||
// (undocumented)
|
|
||||||
ignore(fn: () => void): this;
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
_isInBatch: boolean;
|
_isInBatch: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
mark: (id?: string) => string;
|
mark: (id?: string) => string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
onBatchComplete: () => void;
|
|
||||||
// (undocumented)
|
|
||||||
redo: () => this;
|
redo: () => this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
squashToMark: (id: string) => this;
|
squashToMark: (id: string) => this;
|
||||||
|
@ -2688,6 +2687,12 @@ export interface TLEditorOptions {
|
||||||
user?: TLUser;
|
user?: TLUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public
|
||||||
|
export interface TLEditorRunOptions extends TLHistoryBatchOptions {
|
||||||
|
// (undocumented)
|
||||||
|
ignoreShapeLock?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface TLEditorSnapshot {
|
export interface TLEditorSnapshot {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -134,7 +134,12 @@ export { createTLUser, type TLUser } from './lib/config/createTLUser'
|
||||||
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
|
export { type TLAnyBindingUtilConstructor } from './lib/config/defaultBindings'
|
||||||
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
||||||
export { DEFAULT_ANIMATION_OPTIONS, DEFAULT_CAMERA_OPTIONS, SIDES } from './lib/constants'
|
export { DEFAULT_ANIMATION_OPTIONS, DEFAULT_CAMERA_OPTIONS, SIDES } from './lib/constants'
|
||||||
export { Editor, type TLEditorOptions, type TLResizeShapeOptions } from './lib/editor/Editor'
|
export {
|
||||||
|
Editor,
|
||||||
|
type TLEditorOptions,
|
||||||
|
type TLEditorRunOptions,
|
||||||
|
type TLResizeShapeOptions,
|
||||||
|
} from './lib/editor/Editor'
|
||||||
export {
|
export {
|
||||||
BindingUtil,
|
BindingUtil,
|
||||||
type BindingOnChangeOptions,
|
type BindingOnChangeOptions,
|
||||||
|
|
|
@ -497,10 +497,15 @@ export function useOnMount(onMount?: TLOnMountHandler) {
|
||||||
|
|
||||||
const onMountEvent = useEvent((editor: Editor) => {
|
const onMountEvent = useEvent((editor: Editor) => {
|
||||||
let teardown: (() => void) | void = undefined
|
let teardown: (() => void) | void = undefined
|
||||||
editor.history.ignore(() => {
|
// If the user wants to do something when the editor mounts, we make sure it doesn't effect the history.
|
||||||
teardown = onMount?.(editor)
|
// todo: is this reeeeally what we want to do, or should we leave it up to the caller?
|
||||||
editor.emit('mount')
|
editor.run(
|
||||||
})
|
() => {
|
||||||
|
teardown = onMount?.(editor)
|
||||||
|
editor.emit('mount')
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
window.tldrawReady = true
|
window.tldrawReady = true
|
||||||
return teardown
|
return teardown
|
||||||
})
|
})
|
||||||
|
|
|
@ -218,6 +218,14 @@ export interface TLEditorOptions {
|
||||||
licenseKey?: string
|
licenseKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for {@link Editor.(run:1)}.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface TLEditorRunOptions extends TLHistoryBatchOptions {
|
||||||
|
ignoreShapeLock?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export class Editor extends EventEmitter<TLEventMap> {
|
export class Editor extends EventEmitter<TLEventMap> {
|
||||||
constructor({
|
constructor({
|
||||||
|
@ -671,16 +679,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
)
|
)
|
||||||
this.disposables.add(this.history.dispose)
|
this.disposables.add(this.history.dispose)
|
||||||
|
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
this.store.ensureStoreIsUsable()
|
() => {
|
||||||
|
this.store.ensureStoreIsUsable()
|
||||||
|
|
||||||
// clear ephemeral state
|
// clear ephemeral state
|
||||||
this._updateCurrentPageState({
|
this._updateCurrentPageState({
|
||||||
editingShapeId: null,
|
editingShapeId: null,
|
||||||
hoveredShapeId: null,
|
hoveredShapeId: null,
|
||||||
erasingShapeIds: [],
|
erasingShapeIds: [],
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
|
|
||||||
if (initialState && this.root.children[initialState] === undefined) {
|
if (initialState && this.root.children[initialState] === undefined) {
|
||||||
throw Error(`No state found for initialState "${initialState}".`)
|
throw Error(`No state found for initialState "${initialState}".`)
|
||||||
|
@ -913,7 +924,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*
|
*
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
readonly history: HistoryManager<TLRecord>
|
protected readonly history: HistoryManager<TLRecord>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo to the last mark.
|
* Undo to the last mark.
|
||||||
|
@ -958,6 +969,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearHistory() {
|
||||||
|
this.history.clear()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the app can redo.
|
* Whether the app can redo.
|
||||||
*
|
*
|
||||||
|
@ -986,6 +1002,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squash the history to the given mark id.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* editor.mark('bump shapes')
|
||||||
|
* // ... some changes
|
||||||
|
* editor.squashToMark('bump shapes')
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param markId - The mark id to squash to.
|
||||||
|
*/
|
||||||
|
squashToMark(markId: string): this {
|
||||||
|
this.history.squashToMark(markId)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all marks in the undo stack back to the next mark.
|
* Clear all marks in the undo stack back to the next mark.
|
||||||
*
|
*
|
||||||
|
@ -1016,16 +1049,53 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _shouldIgnoreShapeLock = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a function in a batch.
|
* Run a function in a transaction with optional options for context.
|
||||||
|
* You can use the options to change the way that history is treated
|
||||||
|
* or allow changes to locked shapes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // updating with
|
||||||
|
* editor.run({ history: "ignore" }, () => {
|
||||||
|
* editor.updateShape({ ...myShape, x: 100 })
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // forcing changes / deletions for locked shapes
|
||||||
|
* editor.toggleLock([myShape])
|
||||||
|
* editor.run({ ignoreShapeLock: true }, () => {
|
||||||
|
* editor.updateShape({ ...myShape, x: 100 })
|
||||||
|
* editor.deleteShape(myShape)
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
* @param opts - The options for the batch.
|
||||||
|
* @param fn - The callback function to run.
|
||||||
|
*
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
batch(fn: () => void, opts?: TLHistoryBatchOptions): this {
|
run(fn: () => void, opts?: TLEditorRunOptions): this {
|
||||||
this.history.batch(fn, opts)
|
const previousIgnoreShapeLock = this._shouldIgnoreShapeLock
|
||||||
|
this._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.history.batch(fn, opts)
|
||||||
|
} finally {
|
||||||
|
this._shouldIgnoreShapeLock = previousIgnoreShapeLock
|
||||||
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `Editor.run` instead.
|
||||||
|
*/
|
||||||
|
batch(fn: () => void, opts?: TLEditorRunOptions): this {
|
||||||
|
return this.run(fn, opts)
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------- Errors --------------------- */
|
/* --------------------- Errors --------------------- */
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -1258,9 +1328,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
**/
|
**/
|
||||||
updateDocumentSettings(settings: Partial<TLDocument>): this {
|
updateDocumentSettings(settings: Partial<TLDocument>): this {
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
this.store.put([{ ...this.getDocumentSettings(), ...settings }])
|
() => {
|
||||||
})
|
this.store.put([{ ...this.getDocumentSettings(), ...settings }])
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1306,7 +1379,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
partial: Partial<Omit<TLInstance, 'currentPageId'>>,
|
partial: Partial<Omit<TLInstance, 'currentPageId'>>,
|
||||||
opts?: TLHistoryBatchOptions
|
opts?: TLHistoryBatchOptions
|
||||||
) => {
|
) => {
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
this.store.put([
|
this.store.put([
|
||||||
{
|
{
|
||||||
...this.getInstanceState(),
|
...this.getInstanceState(),
|
||||||
|
@ -1456,39 +1529,27 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' })
|
* editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' })
|
||||||
* editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }, { ephemeral: true })
|
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param partial - The partial of the page state object containing the changes.
|
* @param partial - The partial of the page state object containing the changes.
|
||||||
* @param historyOptions - The history options for the change.
|
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
updateCurrentPageState(
|
updateCurrentPageState(
|
||||||
partial: Partial<
|
partial: Partial<
|
||||||
Omit<TLInstancePageState, 'selectedShapeIds' | 'editingShapeId' | 'pageId' | 'focusedGroupId'>
|
Omit<TLInstancePageState, 'selectedShapeIds' | 'editingShapeId' | 'pageId' | 'focusedGroupId'>
|
||||||
>,
|
>
|
||||||
historyOptions?: TLHistoryBatchOptions
|
|
||||||
): this {
|
): this {
|
||||||
this._updateCurrentPageState(partial, historyOptions)
|
this._updateCurrentPageState(partial)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
_updateCurrentPageState = (
|
private _updateCurrentPageState = (
|
||||||
partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>,
|
partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>
|
||||||
historyOptions?: TLHistoryBatchOptions
|
|
||||||
) => {
|
) => {
|
||||||
this.batch(
|
this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({
|
||||||
() => {
|
...state,
|
||||||
this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({
|
...partial,
|
||||||
...state,
|
}))
|
||||||
...partial,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
history: 'ignore',
|
|
||||||
...historyOptions,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1525,7 +1586,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
setSelectedShapes(shapes: TLShapeId[] | TLShape[]): this {
|
setSelectedShapes(shapes: TLShapeId[] | TLShape[]): this {
|
||||||
return this.batch(
|
return this.run(
|
||||||
() => {
|
() => {
|
||||||
const ids = shapes.map((shape) => (typeof shape === 'string' ? shape : shape.id))
|
const ids = shapes.map((shape) => (typeof shape === 'string' ? shape : shape.id))
|
||||||
const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState()
|
const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState()
|
||||||
|
@ -1804,7 +1865,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (id === this.getFocusedGroupId()) return this
|
if (id === this.getFocusedGroupId()) return this
|
||||||
|
|
||||||
return this.batch(
|
return this.run(
|
||||||
() => {
|
() => {
|
||||||
this.store.update(this.getCurrentPageState().id, (s) => ({ ...s, focusedGroupId: id }))
|
this.store.update(this.getCurrentPageState().id, (s) => ({ ...s, focusedGroupId: id }))
|
||||||
},
|
},
|
||||||
|
@ -1875,13 +1936,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
if (id) {
|
if (id) {
|
||||||
const shape = this.getShape(id)
|
const shape = this.getShape(id)
|
||||||
if (shape && this.getShapeUtil(shape).canEdit(shape)) {
|
if (shape && this.getShapeUtil(shape).canEdit(shape)) {
|
||||||
this._updateCurrentPageState({ editingShapeId: id })
|
this.run(
|
||||||
|
() => {
|
||||||
|
this._updateCurrentPageState({ editingShapeId: id })
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either we just set the editing id to null, or the shape was missing or not editable
|
// Either we just set the editing id to null, or the shape was missing or not editable
|
||||||
this._updateCurrentPageState({ editingShapeId: null })
|
this.run(
|
||||||
|
() => {
|
||||||
|
this._updateCurrentPageState({ editingShapeId: null })
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -1923,7 +1994,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
setHoveredShape(shape: TLShapeId | TLShape | null): this {
|
setHoveredShape(shape: TLShapeId | TLShape | null): this {
|
||||||
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
if (id === this.getHoveredShapeId()) return this
|
if (id === this.getHoveredShapeId()) return this
|
||||||
this.updateCurrentPageState({ hoveredShapeId: id }, { history: 'ignore' })
|
this.run(
|
||||||
|
() => {
|
||||||
|
this.updateCurrentPageState({ hoveredShapeId: id })
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1966,7 +2042,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
? (shapes as TLShapeId[])
|
? (shapes as TLShapeId[])
|
||||||
: (shapes as TLShape[]).map((shape) => shape.id)
|
: (shapes as TLShape[]).map((shape) => shape.id)
|
||||||
// always ephemeral
|
// always ephemeral
|
||||||
this.updateCurrentPageState({ hintingShapeIds: dedupe(ids) }, { history: 'ignore' })
|
this.run(
|
||||||
|
() => {
|
||||||
|
this._updateCurrentPageState({ hintingShapeIds: dedupe(ids) })
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2011,22 +2092,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
: (shapes as TLShape[]).map((shape) => shape.id)
|
: (shapes as TLShape[]).map((shape) => shape.id)
|
||||||
ids.sort() // sort the incoming ids
|
ids.sort() // sort the incoming ids
|
||||||
const erasingShapeIds = this.getErasingShapeIds()
|
const erasingShapeIds = this.getErasingShapeIds()
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
if (ids.length === erasingShapeIds.length) {
|
() => {
|
||||||
// if the new ids are the same length as the current ids, they might be the same.
|
if (ids.length === erasingShapeIds.length) {
|
||||||
// presuming the current ids are also sorted, check each item to see if it's the same;
|
// if the new ids are the same length as the current ids, they might be the same.
|
||||||
// if we find any unequal, then we know the new ids are different.
|
// presuming the current ids are also sorted, check each item to see if it's the same;
|
||||||
for (let i = 0; i < ids.length; i++) {
|
// if we find any unequal, then we know the new ids are different.
|
||||||
if (ids[i] !== erasingShapeIds[i]) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
this._updateCurrentPageState({ erasingShapeIds: ids })
|
if (ids[i] !== erasingShapeIds[i]) {
|
||||||
break
|
this._updateCurrentPageState({ erasingShapeIds: ids })
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// if the ids are a different length, then we know they're different.
|
||||||
|
this._updateCurrentPageState({ erasingShapeIds: ids })
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
// if the ids are a different length, then we know they're different.
|
{ history: 'ignore' }
|
||||||
this._updateCurrentPageState({ erasingShapeIds: ids })
|
)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -2059,15 +2143,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
setCroppingShape(shape: TLShapeId | TLShape | null): this {
|
setCroppingShape(shape: TLShapeId | TLShape | null): this {
|
||||||
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||||
if (id !== this.getCroppingShapeId()) {
|
if (id !== this.getCroppingShapeId()) {
|
||||||
if (!id) {
|
this.run(
|
||||||
this.updateCurrentPageState({ croppingShapeId: null })
|
() => {
|
||||||
} else {
|
if (!id) {
|
||||||
const shape = this.getShape(id)!
|
this.updateCurrentPageState({ croppingShapeId: null })
|
||||||
const util = this.getShapeUtil(shape)
|
} else {
|
||||||
if (shape && util.canCrop(shape)) {
|
const shape = this.getShape(id)!
|
||||||
this.updateCurrentPageState({ croppingShapeId: id })
|
const util = this.getShapeUtil(shape)
|
||||||
}
|
if (shape && util.canCrop(shape)) {
|
||||||
}
|
this.updateCurrentPageState({ croppingShapeId: id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -2459,11 +2548,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
this.batch(() => {
|
transact(() => {
|
||||||
const camera = { ...currentCamera, x, y, z }
|
const camera = { ...currentCamera, x, y, z }
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
this.store.put([camera]) // include id and meta here
|
() => {
|
||||||
})
|
this.store.put([camera]) // include id and meta here
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
|
|
||||||
// Dispatch a new pointer move because the pointer's page will have changed
|
// Dispatch a new pointer move because the pointer's page will have changed
|
||||||
// (its screen position will compute to a new page position given the new camera position)
|
// (its screen position will compute to a new page position given the new camera position)
|
||||||
|
@ -2986,7 +3078,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (!presence) return this
|
if (!presence) return this
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
// If we're following someone, stop following them
|
// If we're following someone, stop following them
|
||||||
if (this.getInstanceState().followingUserId !== null) {
|
if (this.getInstanceState().followingUserId !== null) {
|
||||||
this.stopFollowingUser()
|
this.stopFollowingUser()
|
||||||
|
@ -3284,13 +3376,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
this.getPage(leaderPresence.currentPageId)
|
this.getPage(leaderPresence.currentPageId)
|
||||||
) {
|
) {
|
||||||
// if the page changed, switch page
|
// if the page changed, switch page
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
// sneaky store.put here, we can't go through setCurrentPage because it calls stopFollowingUser
|
() => {
|
||||||
this.store.put([
|
// sneaky store.put here, we can't go through setCurrentPage because it calls stopFollowingUser
|
||||||
{ ...this.getInstanceState(), currentPageId: leaderPresence.currentPageId },
|
this.store.put([
|
||||||
])
|
{ ...this.getInstanceState(), currentPageId: leaderPresence.currentPageId },
|
||||||
this._isLockedOnFollowingUser.set(true)
|
])
|
||||||
})
|
this._isLockedOnFollowingUser.set(true)
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3384,14 +3479,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
stopFollowingUser(): this {
|
stopFollowingUser(): this {
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
// commit the current camera to the store
|
() => {
|
||||||
this.store.put([this.getCamera()])
|
// commit the current camera to the store
|
||||||
// this must happen after the camera is committed
|
this.store.put([this.getCamera()])
|
||||||
this._isLockedOnFollowingUser.set(false)
|
// this must happen after the camera is committed
|
||||||
this.updateInstanceState({ followingUserId: null })
|
this._isLockedOnFollowingUser.set(false)
|
||||||
this.emit('stop-following')
|
this.updateInstanceState({ followingUserId: null })
|
||||||
})
|
this.emit('stop-following')
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3681,8 +3779,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// finish off any in-progress interactions
|
// finish off any in-progress interactions
|
||||||
this.complete()
|
this.complete()
|
||||||
|
|
||||||
return this.batch(
|
return this.run(
|
||||||
() => this.store.put([{ ...this.getInstanceState(), currentPageId: pageId }]),
|
() => {
|
||||||
|
this.store.put([{ ...this.getInstanceState(), currentPageId: pageId }])
|
||||||
|
},
|
||||||
{ history: 'record-preserveRedoStack' }
|
{ history: 'record-preserveRedoStack' }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -3705,7 +3805,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
const prev = this.getPage(partial.id)
|
const prev = this.getPage(partial.id)
|
||||||
if (!prev) return this
|
if (!prev) return this
|
||||||
|
|
||||||
return this.batch(() => this.store.update(partial.id, (page) => ({ ...page, ...partial })))
|
return this.run(() => this.store.update(partial.id, (page) => ({ ...page, ...partial })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3722,7 +3822,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
createPage(page: Partial<TLPage>): this {
|
createPage(page: Partial<TLPage>): this {
|
||||||
this.history.batch(() => {
|
this.run(() => {
|
||||||
if (this.getInstanceState().isReadonly) return
|
if (this.getInstanceState().isReadonly) return
|
||||||
if (this.getPages().length >= this.options.maxPages) return
|
if (this.getPages().length >= this.options.maxPages) return
|
||||||
const pages = this.getPages()
|
const pages = this.getPages()
|
||||||
|
@ -3764,7 +3864,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
deletePage(page: TLPageId | TLPage): this {
|
deletePage(page: TLPageId | TLPage): this {
|
||||||
const id = typeof page === 'string' ? page : page.id
|
const id = typeof page === 'string' ? page : page.id
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
if (this.getInstanceState().isReadonly) return
|
if (this.getInstanceState().isReadonly) return
|
||||||
const pages = this.getPages()
|
const pages = this.getPages()
|
||||||
if (pages.length === 1) return
|
if (pages.length === 1) return
|
||||||
|
@ -3799,7 +3899,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
const prevCamera = { ...this.getCamera() }
|
const prevCamera = { ...this.getCamera() }
|
||||||
const content = this.getContentFromCurrentPage(this.getSortedChildIdsForParent(freshPage.id))
|
const content = this.getContentFromCurrentPage(this.getSortedChildIdsForParent(freshPage.id))
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
const pages = this.getPages()
|
const pages = this.getPages()
|
||||||
const index = getIndexBetween(freshPage.index, pages[pages.indexOf(freshPage) + 1]?.index)
|
const index = getIndexBetween(freshPage.index, pages[pages.indexOf(freshPage) + 1]?.index)
|
||||||
|
|
||||||
|
@ -3870,7 +3970,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
createAssets(assets: TLAsset[]): this {
|
createAssets(assets: TLAsset[]): this {
|
||||||
if (this.getInstanceState().isReadonly) return this
|
if (this.getInstanceState().isReadonly) return this
|
||||||
if (assets.length <= 0) return this
|
if (assets.length <= 0) return this
|
||||||
this.history.ignore(() => this.store.put(assets))
|
this.run(() => this.store.put(assets), { history: 'ignore' })
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3889,14 +3989,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
updateAssets(assets: TLAssetPartial[]): this {
|
updateAssets(assets: TLAssetPartial[]): this {
|
||||||
if (this.getInstanceState().isReadonly) return this
|
if (this.getInstanceState().isReadonly) return this
|
||||||
if (assets.length <= 0) return this
|
if (assets.length <= 0) return this
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
this.store.put(
|
() => {
|
||||||
assets.map((partial) => ({
|
this.store.put(
|
||||||
...this.store.get(partial.id)!,
|
assets.map((partial) => ({
|
||||||
...partial,
|
...this.store.get(partial.id)!,
|
||||||
}))
|
...partial,
|
||||||
)
|
}))
|
||||||
})
|
)
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3921,7 +4024,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
: (assets as TLAsset[]).map((a) => a.id)
|
: (assets as TLAsset[]).map((a) => a.id)
|
||||||
if (ids.length <= 0) return this
|
if (ids.length <= 0) return this
|
||||||
|
|
||||||
this.history.ignore(() => this.store.remove(ids))
|
this.run(() => this.store.remove(ids), { history: 'ignore' })
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5019,40 +5122,37 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
const shapesToReparent = compact(ids.map((id) => this.getShape(id)))
|
const shapesToReparent = compact(ids.map((id) => this.getShape(id)))
|
||||||
|
|
||||||
// The user is allowed to re-parent locked shapes. Unintuitive? Yeah! But there are plenty of
|
// Ignore locked shapes so that we can reparent locked shapes, for example
|
||||||
// times when a locked shape's parent is deleted... and we need to put that shape somewhere!
|
// when a locked shape's parent is deleted.
|
||||||
const lockedShapes = shapesToReparent.filter((shape) => shape.isLocked)
|
this.run(
|
||||||
|
() => {
|
||||||
|
for (let i = 0; i < shapesToReparent.length; i++) {
|
||||||
|
const shape = shapesToReparent[i]
|
||||||
|
|
||||||
if (lockedShapes.length) {
|
const pageTransform = this.getShapePageTransform(shape)!
|
||||||
// If we have locked shapes, unlock them before we update them
|
if (!pageTransform) continue
|
||||||
this.updateShapes(lockedShapes.map(({ id, type }) => ({ id, type, isLocked: false })))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < shapesToReparent.length; i++) {
|
const pagePoint = pageTransform.point()
|
||||||
const shape = shapesToReparent[i]
|
if (!pagePoint) continue
|
||||||
|
|
||||||
const pageTransform = this.getShapePageTransform(shape)!
|
const newPoint = invertedParentTransform.applyToPoint(pagePoint)
|
||||||
if (!pageTransform) continue
|
const newRotation = pageTransform.rotation() - parentPageRotation
|
||||||
|
|
||||||
const pagePoint = pageTransform.point()
|
changes.push({
|
||||||
if (!pagePoint) continue
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
parentId: parentId,
|
||||||
|
x: newPoint.x,
|
||||||
|
y: newPoint.y,
|
||||||
|
rotation: newRotation,
|
||||||
|
index: indices[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const newPoint = invertedParentTransform.applyToPoint(pagePoint)
|
this.updateShapes(changes)
|
||||||
const newRotation = pageTransform.rotation() - parentPageRotation
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
changes.push({
|
)
|
||||||
id: shape.id,
|
|
||||||
type: shape.type,
|
|
||||||
parentId: parentId,
|
|
||||||
x: newPoint.x,
|
|
||||||
y: newPoint.y,
|
|
||||||
rotation: newRotation,
|
|
||||||
index: indices[i],
|
|
||||||
isLocked: shape.isLocked, // this will re-lock locked shapes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateShapes(changes)
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -5520,7 +5620,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
||||||
this.history.batch(() => {
|
this.run(() => {
|
||||||
const ids =
|
const ids =
|
||||||
typeof shapes[0] === 'string'
|
typeof shapes[0] === 'string'
|
||||||
? (shapes as TLShapeId[])
|
? (shapes as TLShapeId[])
|
||||||
|
@ -5666,7 +5766,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
const fromPageZ = this.getCamera().z
|
const fromPageZ = this.getCamera().z
|
||||||
|
|
||||||
this.history.batch(() => {
|
this.run(() => {
|
||||||
// Delete the shapes on the current page
|
// Delete the shapes on the current page
|
||||||
this.deleteShapes(ids)
|
this.deleteShapes(ids)
|
||||||
|
|
||||||
|
@ -5723,7 +5823,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
if (allUnlocked) {
|
if (allUnlocked) {
|
||||||
this.updateShapes(
|
this.updateShapes(
|
||||||
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true }))
|
||||||
|
@ -5877,7 +5977,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
|
compact(shapesToFlip.map((id) => this.getShapePageBounds(id)))
|
||||||
).center
|
).center
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
for (const shape of shapesToFlip) {
|
for (const shape of shapesToFlip) {
|
||||||
const bounds = this.getShapeGeometry(shape).bounds
|
const bounds = this.getShapeGeometry(shape).bounds
|
||||||
const initialPageTransform = this.getShapePageTransform(shape.id)
|
const initialPageTransform = this.getShapePageTransform(shape.id)
|
||||||
|
@ -6383,7 +6483,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'vertical': {
|
case 'vertical': {
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
for (const shape of shapesToStretch) {
|
for (const shape of shapesToStretch) {
|
||||||
const pageRotation = this.getShapePageTransform(shape)!.rotation()
|
const pageRotation = this.getShapePageTransform(shape)!.rotation()
|
||||||
if (pageRotation % PI2) continue
|
if (pageRotation % PI2) continue
|
||||||
|
@ -6407,7 +6507,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'horizontal': {
|
case 'horizontal': {
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
for (const shape of shapesToStretch) {
|
for (const shape of shapesToStretch) {
|
||||||
const bounds = shapeBounds[shape.id]
|
const bounds = shapeBounds[shape.id]
|
||||||
const pageBounds = shapePageBounds[shape.id]
|
const pageBounds = shapePageBounds[shape.id]
|
||||||
|
@ -6773,7 +6873,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
const focusedGroupId = this.getFocusedGroupId()
|
const focusedGroupId = this.getFocusedGroupId()
|
||||||
|
|
||||||
return this.batch(() => {
|
this.run(() => {
|
||||||
// 1. Parents
|
// 1. Parents
|
||||||
|
|
||||||
// Make sure that each partial will become the child of either the
|
// Make sure that each partial will become the child of either the
|
||||||
|
@ -6933,6 +7033,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
this.store.put(shapeRecordsToCreate)
|
this.store.put(shapeRecordsToCreate)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
private animatingShapes = new Map<TLShapeId, string>()
|
private animatingShapes = new Map<TLShapeId, string>()
|
||||||
|
@ -7092,7 +7194,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (ids.length <= 1) return this
|
if (ids.length <= 1) return this
|
||||||
|
|
||||||
const shapesToGroup = compact(this._getUnlockedShapeIds(ids).map((id) => this.getShape(id)))
|
const shapesToGroup = compact(
|
||||||
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
||||||
|
this.getShape(id)
|
||||||
|
)
|
||||||
|
)
|
||||||
const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
|
const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id)
|
||||||
const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
|
const pageBounds = Box.Common(compact(shapesToGroup.map((id) => this.getShapePageBounds(id))))
|
||||||
|
|
||||||
|
@ -7115,7 +7221,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index
|
const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
this.createShapes<TLGroupShape>([
|
this.createShapes<TLGroupShape>([
|
||||||
{
|
{
|
||||||
id: groupId,
|
id: groupId,
|
||||||
|
@ -7155,18 +7261,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
ungroupShapes(ids: TLShapeId[], options?: Partial<{ select: boolean }>): this
|
ungroupShapes(ids: TLShapeId[], options?: Partial<{ select: boolean }>): this
|
||||||
ungroupShapes(shapes: TLShape[], options?: Partial<{ select: boolean }>): this
|
ungroupShapes(shapes: TLShape[], options?: Partial<{ select: boolean }>): this
|
||||||
ungroupShapes(shapes: TLShapeId[] | TLShape[], options = {} as Partial<{ select: boolean }>) {
|
ungroupShapes(shapes: TLShapeId[] | TLShape[], options = {} as Partial<{ select: boolean }>) {
|
||||||
|
if (this.getInstanceState().isReadonly) return this
|
||||||
|
|
||||||
const { select = true } = options
|
const { select = true } = options
|
||||||
const ids =
|
const ids =
|
||||||
typeof shapes[0] === 'string'
|
typeof shapes[0] === 'string'
|
||||||
? (shapes as TLShapeId[])
|
? (shapes as TLShapeId[])
|
||||||
: (shapes as TLShape[]).map((s) => s.id)
|
: (shapes as TLShape[]).map((s) => s.id)
|
||||||
if (this.getInstanceState().isReadonly) return this
|
|
||||||
if (ids.length === 0) return this
|
|
||||||
|
|
||||||
// Only ungroup when the select tool is active
|
const shapesToUngroup = compact(
|
||||||
|
(this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) =>
|
||||||
|
this.getShape(id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (shapesToUngroup.length === 0) return this
|
||||||
|
|
||||||
|
// todo: the editor shouldn't know about the select tool, move to group / ungroup actions
|
||||||
if (this.getCurrentToolId() !== 'select') return this
|
if (this.getCurrentToolId() !== 'select') return this
|
||||||
|
|
||||||
// If not already in idle, cancel the current interaction (get back to idle)
|
|
||||||
if (!this.isIn('select.idle')) {
|
if (!this.isIn('select.idle')) {
|
||||||
this.cancel()
|
this.cancel()
|
||||||
}
|
}
|
||||||
|
@ -7179,7 +7291,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
// Get all groups in the selection
|
// Get all groups in the selection
|
||||||
const groups: TLGroupShape[] = []
|
const groups: TLGroupShape[] = []
|
||||||
|
|
||||||
compact(ids.map((id) => this.getShape(id))).forEach((shape) => {
|
shapesToUngroup.forEach((shape) => {
|
||||||
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
if (this.isShapeOfType<TLGroupShape>(shape, 'group')) {
|
||||||
groups.push(shape)
|
groups.push(shape)
|
||||||
} else {
|
} else {
|
||||||
|
@ -7189,7 +7301,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (groups.length === 0) return this
|
if (groups.length === 0) return this
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
let group: TLGroupShape
|
let group: TLGroupShape
|
||||||
|
|
||||||
for (let i = 0, n = groups.length; i < n; i++) {
|
for (let i = 0, n = groups.length; i < n; i++) {
|
||||||
|
@ -7253,8 +7365,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
const shape = this.getShape(partial.id)
|
const shape = this.getShape(partial.id)
|
||||||
if (!shape) continue
|
if (!shape) continue
|
||||||
|
|
||||||
// If the shape is locked and we're not setting isLocked to true, continue
|
// If we're "forcing" the update, then we'll update the shape
|
||||||
if (this.isShapeOrAncestorLocked(shape) && !Object.hasOwn(partial, 'isLocked')) continue
|
// regardless of whether it / its ancestor is locked
|
||||||
|
if (!this._shouldIgnoreShapeLock) {
|
||||||
|
if (shape.isLocked) {
|
||||||
|
// If the shape itself is locked (even if one of its ancestors is
|
||||||
|
// also locked) then only allow an update that unlocks the shape.
|
||||||
|
if (!(Object.hasOwn(partial, 'isLocked') && !partial.isLocked)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if (this.isShapeOrAncestorLocked(shape)) {
|
||||||
|
// If the shape itself is unlocked, and any of the shape's
|
||||||
|
// ancestors are locked then we'll skip the update
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove any animating shapes from the list of partials
|
// Remove any animating shapes from the list of partials
|
||||||
this.animatingShapes.delete(partial.id)
|
this.animatingShapes.delete(partial.id)
|
||||||
|
@ -7270,7 +7395,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _updateShapes = (_partials: (TLShapePartial | null | undefined)[]) => {
|
private _updateShapes = (_partials: (TLShapePartial | null | undefined)[]) => {
|
||||||
if (this.getInstanceState().isReadonly) return
|
if (this.getInstanceState().isReadonly) return
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
const updates = []
|
const updates = []
|
||||||
|
|
||||||
let shape: TLShape | undefined
|
let shape: TLShape | undefined
|
||||||
|
@ -7323,27 +7448,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
deleteShapes(ids: TLShapeId[]): this
|
deleteShapes(ids: TLShapeId[]): this
|
||||||
deleteShapes(shapes: TLShape[]): this
|
deleteShapes(shapes: TLShape[]): this
|
||||||
deleteShapes(_ids: TLShapeId[] | TLShape[]): this {
|
deleteShapes(_ids: TLShapeId[] | TLShape[]): this {
|
||||||
|
if (this.getInstanceState().isReadonly) return this
|
||||||
|
|
||||||
if (!Array.isArray(_ids)) {
|
if (!Array.isArray(_ids)) {
|
||||||
throw Error('Editor.deleteShapes: must provide an array of shapes or shapeIds')
|
throw Error('Editor.deleteShapes: must provide an array of shapes or shapeIds')
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = this._getUnlockedShapeIds(
|
const shapeIds =
|
||||||
typeof _ids[0] === 'string' ? (_ids as TLShapeId[]) : (_ids as TLShape[]).map((s) => s.id)
|
typeof _ids[0] === 'string' ? (_ids as TLShapeId[]) : (_ids as TLShape[]).map((s) => s.id)
|
||||||
)
|
|
||||||
|
|
||||||
if (this.getInstanceState().isReadonly) return this
|
// Normally we don't want to delete locked shapes, but if the force option is set, we'll delete them anyway
|
||||||
if (ids.length === 0) return this
|
const shapeIdsToDelete = this._shouldIgnoreShapeLock
|
||||||
|
? shapeIds
|
||||||
|
: this._getUnlockedShapeIds(shapeIds)
|
||||||
|
|
||||||
const allIds = new Set(ids)
|
if (shapeIdsToDelete.length === 0) return this
|
||||||
|
|
||||||
for (const id of ids) {
|
// We also need to delete these shapes' descendants
|
||||||
|
const allShapeIdsToDelete = new Set<TLShapeId>(shapeIdsToDelete)
|
||||||
|
|
||||||
|
for (const id of shapeIdsToDelete) {
|
||||||
this.visitDescendants(id, (childId) => {
|
this.visitDescendants(id, (childId) => {
|
||||||
allIds.add(childId)
|
allShapeIdsToDelete.add(childId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedIds = [...allIds]
|
return this.run(() => this.store.remove([...allShapeIdsToDelete]))
|
||||||
return this.batch(() => this.store.remove(deletedIds))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8137,7 +8267,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
// Create any assets that need to be created
|
// Create any assets that need to be created
|
||||||
if (assetsToCreate.length > 0) {
|
if (assetsToCreate.length > 0) {
|
||||||
this.createAssets(assetsToCreate)
|
this.createAssets(assetsToCreate)
|
||||||
|
@ -8369,24 +8499,27 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: We only have to do this if there are multiple users in the document
|
// todo: We only have to do this if there are multiple users in the document
|
||||||
this.history.ignore(() => {
|
this.run(
|
||||||
this.store.put([
|
() => {
|
||||||
{
|
this.store.put([
|
||||||
id: TLPOINTER_ID,
|
{
|
||||||
typeName: 'pointer',
|
id: TLPOINTER_ID,
|
||||||
x: currentPagePoint.x,
|
typeName: 'pointer',
|
||||||
y: currentPagePoint.y,
|
x: currentPagePoint.x,
|
||||||
lastActivityTimestamp:
|
y: currentPagePoint.y,
|
||||||
// If our pointer moved only because we're following some other user, then don't
|
lastActivityTimestamp:
|
||||||
// update our last activity timestamp; otherwise, update it to the current timestamp.
|
// If our pointer moved only because we're following some other user, then don't
|
||||||
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
|
// update our last activity timestamp; otherwise, update it to the current timestamp.
|
||||||
? this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
|
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
|
||||||
this._tickManager.now
|
? this.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ??
|
||||||
: this._tickManager.now,
|
this._tickManager.now
|
||||||
meta: {},
|
: this._tickManager.now,
|
||||||
},
|
meta: {},
|
||||||
])
|
},
|
||||||
})
|
])
|
||||||
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8643,7 +8776,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
private _pendingEventsForNextTick: TLEventInfo[] = []
|
private _pendingEventsForNextTick: TLEventInfo[] = []
|
||||||
|
|
||||||
private _flushEventsForTick(elapsed: number) {
|
private _flushEventsForTick(elapsed: number) {
|
||||||
this.batch(() => {
|
this.run(() => {
|
||||||
if (this._pendingEventsForNextTick.length > 0) {
|
if (this._pendingEventsForNextTick.length > 0) {
|
||||||
const events = [...this._pendingEventsForNextTick]
|
const events = [...this._pendingEventsForNextTick]
|
||||||
this._pendingEventsForNextTick.length = 0
|
this._pendingEventsForNextTick.length = 0
|
||||||
|
@ -9194,39 +9327,42 @@ function withIsolatedShapes<T>(
|
||||||
): T {
|
): T {
|
||||||
let result!: Result<T, unknown>
|
let result!: Result<T, unknown>
|
||||||
|
|
||||||
editor.history.ignore(() => {
|
editor.run(
|
||||||
const changes = editor.store.extractingChanges(() => {
|
() => {
|
||||||
const bindingsWithBoth = new Set<TLBindingId>()
|
const changes = editor.store.extractingChanges(() => {
|
||||||
const bindingsToRemove = new Set<TLBindingId>()
|
const bindingsWithBoth = new Set<TLBindingId>()
|
||||||
|
const bindingsToRemove = new Set<TLBindingId>()
|
||||||
|
|
||||||
for (const shapeId of shapeIds) {
|
for (const shapeId of shapeIds) {
|
||||||
const shape = editor.getShape(shapeId)
|
const shape = editor.getShape(shapeId)
|
||||||
if (!shape) continue
|
if (!shape) continue
|
||||||
|
|
||||||
for (const binding of editor.getBindingsInvolvingShape(shapeId)) {
|
for (const binding of editor.getBindingsInvolvingShape(shapeId)) {
|
||||||
const hasFrom = shapeIds.has(binding.fromId)
|
const hasFrom = shapeIds.has(binding.fromId)
|
||||||
const hasTo = shapeIds.has(binding.toId)
|
const hasTo = shapeIds.has(binding.toId)
|
||||||
if (hasFrom && hasTo) {
|
if (hasFrom && hasTo) {
|
||||||
bindingsWithBoth.add(binding.id)
|
bindingsWithBoth.add(binding.id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!hasFrom || !hasTo) {
|
if (!hasFrom || !hasTo) {
|
||||||
bindingsToRemove.add(binding.id)
|
bindingsToRemove.add(binding.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
editor.deleteBindings([...bindingsToRemove], { isolateShapes: true })
|
editor.deleteBindings([...bindingsToRemove], { isolateShapes: true })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = Result.ok(callback(bindingsWithBoth))
|
result = Result.ok(callback(bindingsWithBoth))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
result = Result.err(error)
|
result = Result.err(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.store.applyDiff(reverseRecordsDiff(changes))
|
editor.store.applyDiff(reverseRecordsDiff(changes))
|
||||||
})
|
},
|
||||||
|
{ history: 'ignore' }
|
||||||
|
)
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
return result.value
|
return result.value
|
||||||
|
|
|
@ -56,7 +56,7 @@ function createCounterHistoryManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setName = (name = 'David') => {
|
const setName = (name = 'David') => {
|
||||||
manager.ignore(() => _setName(name))
|
manager.batch(() => _setName(name), { history: 'ignore' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAge = (age = 35) => {
|
const setAge = (age = 35) => {
|
||||||
|
|
|
@ -72,17 +72,17 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumRedos() {
|
getNumRedos() {
|
||||||
return this.stacks.get().redos.length
|
return this.stacks.get().redos.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_isInBatch = false
|
_isInBatch = false
|
||||||
|
|
||||||
batch = (fn: () => void, opts?: TLHistoryBatchOptions) => {
|
batch = (fn: () => void, opts?: TLHistoryBatchOptions) => {
|
||||||
const previousState = this.state
|
const previousState = this.state
|
||||||
|
|
||||||
|
@ -93,16 +93,13 @@ export class HistoryManager<R extends UnknownRecord> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this._isInBatch) {
|
if (this._isInBatch) {
|
||||||
fn()
|
transact(fn)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isInBatch = true
|
this._isInBatch = true
|
||||||
try {
|
try {
|
||||||
transact(() => {
|
transact(fn)
|
||||||
fn()
|
|
||||||
this.onBatchComplete()
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.annotateError(error)
|
this.annotateError(error)
|
||||||
throw error
|
throw error
|
||||||
|
@ -116,10 +113,6 @@ export class HistoryManager<R extends UnknownRecord> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ignore(fn: () => void) {
|
|
||||||
return this.batch(fn, { history: 'ignore' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// History
|
// History
|
||||||
private _undo = ({
|
private _undo = ({
|
||||||
pushToRedoStack,
|
pushToRedoStack,
|
||||||
|
|
|
@ -87,7 +87,7 @@ 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.editor.run(() => {
|
||||||
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
|
||||||
|
|
|
@ -443,7 +443,7 @@ export function registerDefaultExternalContentHandlers(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
if (shouldAlsoCreateAsset) {
|
if (shouldAlsoCreateAsset) {
|
||||||
editor.createAssets([asset])
|
editor.createAssets([asset])
|
||||||
}
|
}
|
||||||
|
@ -526,7 +526,7 @@ export async function createShapesForAssets(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
// 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) {
|
||||||
|
@ -589,7 +589,7 @@ export function createEmptyBookmarkShape(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
editor.createShapes([partial]).select(partial.id)
|
editor.createShapes([partial]).select(partial.id)
|
||||||
centerSelectionAroundPoint(editor, position)
|
centerSelectionAroundPoint(editor, position)
|
||||||
})
|
})
|
||||||
|
|
|
@ -234,7 +234,7 @@ const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TL
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
// Create the new asset
|
// Create the new asset
|
||||||
editor.createAssets([asset])
|
editor.createAssets([asset])
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class DragAndDropManager {
|
||||||
|
|
||||||
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
private setDragTimer(movingShapes: TLShape[], duration: number, cb: () => void) {
|
||||||
this.droppingNodeTimer = this.editor.timers.setTimeout(() => {
|
this.droppingNodeTimer = this.editor.timers.setTimeout(() => {
|
||||||
this.editor.batch(() => {
|
this.editor.run(() => {
|
||||||
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
|
this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb)
|
||||||
})
|
})
|
||||||
this.droppingNodeTimer = null
|
this.droppingNodeTimer = null
|
||||||
|
|
|
@ -19,14 +19,15 @@ export class Crop extends StateNode {
|
||||||
markId = ''
|
markId = ''
|
||||||
override onEnter = () => {
|
override onEnter = () => {
|
||||||
this.didCancel = false
|
this.didCancel = false
|
||||||
this.markId = this.editor.history.mark()
|
this.markId = 'crop'
|
||||||
|
this.editor.mark(this.markId)
|
||||||
}
|
}
|
||||||
didCancel = false
|
didCancel = false
|
||||||
override onExit = () => {
|
override onExit = () => {
|
||||||
if (this.didCancel) {
|
if (this.didCancel) {
|
||||||
this.editor.bailToMark(this.markId)
|
this.editor.bailToMark(this.markId)
|
||||||
} else {
|
} else {
|
||||||
this.editor.history.squashToMark(this.markId)
|
this.editor.squashToMark(this.markId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override onCancel = () => {
|
override onCancel = () => {
|
||||||
|
|
|
@ -142,7 +142,7 @@ export class PointingShape extends StateNode {
|
||||||
textLabel.bounds.containsPoint(pointInShapeSpace, 0) &&
|
textLabel.bounds.containsPoint(pointInShapeSpace, 0) &&
|
||||||
textLabel.hitTestPoint(pointInShapeSpace)
|
textLabel.hitTestPoint(pointInShapeSpace)
|
||||||
) {
|
) {
|
||||||
this.editor.batch(() => {
|
this.editor.run(() => {
|
||||||
this.editor.mark('editing on pointer up')
|
this.editor.mark('editing on pointer up')
|
||||||
this.editor.select(selectingShape.id)
|
this.editor.select(selectingShape.id)
|
||||||
|
|
||||||
|
|
|
@ -298,7 +298,7 @@ function createNShapes(editor: Editor, n: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
|
editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,7 +250,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
||||||
const handleCreatePageClick = useCallback(() => {
|
const handleCreatePageClick = useCallback(() => {
|
||||||
if (isReadonlyMode) return
|
if (isReadonlyMode) return
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
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 })
|
||||||
|
@ -399,7 +399,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
|
||||||
editor.renamePage(page.id, name)
|
editor.renamePage(page.id, name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
setIsEditing(true)
|
setIsEditing(true)
|
||||||
editor.setCurrentPage(page.id)
|
editor.setCurrentPage(page.id)
|
||||||
})
|
})
|
||||||
|
|
|
@ -80,7 +80,7 @@ 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(() => {
|
editor.run(() => {
|
||||||
if (editor.isIn('select')) {
|
if (editor.isIn('select')) {
|
||||||
editor.setStyleForSelectedShapes(style, value)
|
editor.setStyleForSelectedShapes(style, value)
|
||||||
}
|
}
|
||||||
|
@ -360,7 +360,7 @@ 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(() => {
|
editor.run(() => {
|
||||||
if (editor.isIn('select')) {
|
if (editor.isIn('select')) {
|
||||||
editor.setOpacityForSelectedShapes(item)
|
editor.setOpacityForSelectedShapes(item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -394,7 +394,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
if (!canApplySelectionAction()) return
|
if (!canApplySelectionAction()) return
|
||||||
if (mustGoBackToSelectToolFirst()) return
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
trackEvent('convert-to-bookmark', { source })
|
trackEvent('convert-to-bookmark', { source })
|
||||||
const shapes = editor.getSelectedShapes()
|
const shapes = editor.getSelectedShapes()
|
||||||
|
|
||||||
|
@ -439,7 +439,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
|
|
||||||
trackEvent('convert-to-embed', { source })
|
trackEvent('convert-to-embed', { source })
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
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)))
|
||||||
|
|
||||||
|
@ -970,7 +970,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '$a',
|
kbd: '$a',
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
if (mustGoBackToSelectToolFirst()) return
|
if (mustGoBackToSelectToolFirst()) return
|
||||||
|
|
||||||
trackEvent('select-all-shapes', { source })
|
trackEvent('select-all-shapes', { source })
|
||||||
|
@ -1263,7 +1263,7 @@ 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
|
||||||
editor.timers.requestAnimationFrame(() => {
|
editor.timers.requestAnimationFrame(() => {
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
trackEvent('toggle-focus-mode', { source })
|
trackEvent('toggle-focus-mode', { source })
|
||||||
clearDialogs()
|
clearDialogs()
|
||||||
clearToasts()
|
clearToasts()
|
||||||
|
@ -1362,7 +1362,7 @@ 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.run(() => {
|
||||||
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)
|
||||||
|
@ -1376,7 +1376,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?t',
|
kbd: '?t',
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
const style = DefaultColorStyle
|
const style = DefaultColorStyle
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
editor.mark('change-color')
|
editor.mark('change-color')
|
||||||
if (editor.isIn('select')) {
|
if (editor.isIn('select')) {
|
||||||
editor.setStyleForSelectedShapes(style, 'white')
|
editor.setStyleForSelectedShapes(style, 'white')
|
||||||
|
@ -1392,7 +1392,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
kbd: '?f',
|
kbd: '?f',
|
||||||
onSelect(source) {
|
onSelect(source) {
|
||||||
const style = DefaultFillStyle
|
const style = DefaultFillStyle
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
editor.mark('change-fill')
|
editor.mark('change-fill')
|
||||||
if (editor.isIn('select')) {
|
if (editor.isIn('select')) {
|
||||||
editor.setStyleForSelectedShapes(style, 'fill')
|
editor.setStyleForSelectedShapes(style, 'fill')
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
|
||||||
(isOpen: boolean) => {
|
(isOpen: boolean) => {
|
||||||
rIsOpen.current = isOpen
|
rIsOpen.current = isOpen
|
||||||
|
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
editor.complete()
|
editor.complete()
|
||||||
editor.addOpenMenu(id)
|
editor.addOpenMenu(id)
|
||||||
|
|
|
@ -101,7 +101,7 @@ 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.run(() => {
|
||||||
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,7 +17,7 @@ export function removeFrame(editor: Editor, ids: TLShapeId[]) {
|
||||||
if (!frames.length) return
|
if (!frames.length) return
|
||||||
|
|
||||||
const allChildren: TLShapeId[] = []
|
const allChildren: TLShapeId[] = []
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
frames.map((frame) => {
|
frames.map((frame) => {
|
||||||
const children = editor.getSortedChildIdsForParent(frame.id)
|
const children = editor.getSortedChildIdsForParent(frame.id)
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
|
@ -66,7 +66,7 @@ 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(() => {
|
editor.run(() => {
|
||||||
const changes: TLShapePartial[] = childIds.map((child) => {
|
const changes: TLShapePartial[] = childIds.map((child) => {
|
||||||
const shape = editor.getShape(child)!
|
const shape = editor.getShape(child)!
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -34,7 +34,7 @@ const TLDRAW_V1_VERSION = 15.5
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function buildFromV1Document(editor: Editor, _document: unknown) {
|
export function buildFromV1Document(editor: Editor, _document: unknown) {
|
||||||
let document = _document as TLV1Document
|
let document = _document as TLV1Document
|
||||||
editor.batch(() => {
|
editor.run(() => {
|
||||||
document = migrate(document, TLDRAW_V1_VERSION)
|
document = migrate(document, TLDRAW_V1_VERSION)
|
||||||
// Cancel any interactions / states
|
// Cancel any interactions / states
|
||||||
editor.cancel().cancel().cancel().cancel()
|
editor.cancel().cancel().cancel().cancel()
|
||||||
|
@ -594,7 +594,7 @@ export function buildFromV1Document(editor: Editor, _document: unknown) {
|
||||||
// Set the current page to the first page again
|
// Set the current page to the first page again
|
||||||
editor.setCurrentPage(firstPageId)
|
editor.setCurrentPage(firstPageId)
|
||||||
|
|
||||||
editor.history.clear()
|
editor.clearHistory()
|
||||||
editor.selectNone()
|
editor.selectNone()
|
||||||
|
|
||||||
const bounds = editor.getCurrentPageBounds()
|
const bounds = editor.getCurrentPageBounds()
|
||||||
|
|
|
@ -306,7 +306,7 @@ export async function parseAndLoadDocument(
|
||||||
editor.store.put(nonShapes, 'initialize')
|
editor.store.put(nonShapes, 'initialize')
|
||||||
editor.store.ensureStoreIsUsable()
|
editor.store.ensureStoreIsUsable()
|
||||||
editor.store.put(shapes, 'initialize')
|
editor.store.put(shapes, 'initialize')
|
||||||
editor.history.clear()
|
editor.clearHistory()
|
||||||
// Put the old bounds back in place
|
// Put the old bounds back in place
|
||||||
editor.updateViewportScreenBounds(initialBounds)
|
editor.updateViewportScreenBounds(initialBounds)
|
||||||
|
|
||||||
|
|
|
@ -131,6 +131,10 @@ export class TestEditor extends Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHistory() {
|
||||||
|
return this.history
|
||||||
|
}
|
||||||
|
|
||||||
private _lastCreatedShapes: TLShape[] = []
|
private _lastCreatedShapes: TLShape[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -172,3 +172,137 @@ describe('Unlocking', () => {
|
||||||
expect(getLockedStatus()).toStrictEqual([false, false])
|
expect(getLockedStatus()).toStrictEqual([false, false])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When forced', () => {
|
||||||
|
it('Can be deleted', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
const numberOfShapesBefore = editor.getCurrentPageShapes().length
|
||||||
|
editor.deleteShapes([ids.lockedShapeA])
|
||||||
|
expect(editor.getCurrentPageShapes().length).toBe(numberOfShapesBefore - 1)
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can be changed', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
editor.updateShapes([{ id: ids.lockedShapeA, type: 'geo', x: 100 }])
|
||||||
|
expect(editor.getShape(ids.lockedShapeA)!.x).toBe(100)
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can be grouped / ungrouped', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
const shapeCount = editor.getCurrentPageShapes().length
|
||||||
|
editor.groupShapes([ids.lockedShapeA, ids.unlockedShapeA, ids.unlockedShapeB])
|
||||||
|
expect(editor.getCurrentPageShapes().length).toBe(shapeCount + 1)
|
||||||
|
expect(editor.getShape(ids.lockedShapeA)!.parentId).not.toBe(editor.getCurrentPageId())
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be moved', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
const shape = editor.getShape(ids.lockedShapeA)
|
||||||
|
editor.pointerDown(150, 150, { target: 'shape', shape })
|
||||||
|
editor.expectToBeIn('select.pointing_canvas')
|
||||||
|
|
||||||
|
editor.pointerMove(10, 10)
|
||||||
|
editor.expectToBeIn('select.brushing')
|
||||||
|
|
||||||
|
editor.pointerUp()
|
||||||
|
editor.expectToBeIn('select.idle')
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can be selected with select all', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
editor.selectAll()
|
||||||
|
expect(editor.getSelectedShapeIds()).toEqual([ids.unlockedShapeA, ids.unlockedShapeB])
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be selected by clicking', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
const shape = editor.getShape(ids.lockedShapeA)!
|
||||||
|
|
||||||
|
editor
|
||||||
|
.pointerDown(10, 10, { target: 'shape', shape })
|
||||||
|
.expectToBeIn('select.pointing_canvas')
|
||||||
|
.pointerUp()
|
||||||
|
.expectToBeIn('select.idle')
|
||||||
|
expect(editor.getSelectedShapeIds()).not.toContain(shape.id)
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be edited', () => {
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
const shape = editor.getShape(ids.lockedShapeA)!
|
||||||
|
const shapeCount = editor.getCurrentPageShapes().length
|
||||||
|
|
||||||
|
// We create a new shape and we edit that one
|
||||||
|
editor.doubleClick(10, 10, { target: 'shape', shape }).expectToBeIn('select.editing_shape')
|
||||||
|
expect(editor.getCurrentPageShapes().length).toBe(shapeCount + 1)
|
||||||
|
expect(editor.getSelectedShapeIds()).not.toContain(shape.id)
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not update a locked shape, even if spreading in a full shape', () => {
|
||||||
|
const myShapeId = createShapeId()
|
||||||
|
editor.createShape({ id: myShapeId, type: 'geo', isLocked: true })
|
||||||
|
const myLockedShape = editor.getShape(myShapeId)!
|
||||||
|
// include the `isLocked` property, but don't change it
|
||||||
|
editor.updateShape({ ...myLockedShape, x: 100 })
|
||||||
|
expect(editor.getShape(myShapeId)).toMatchObject(myLockedShape)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works when forced', () => {
|
||||||
|
const myShapeId = createShapeId()
|
||||||
|
editor.createShape({ id: myShapeId, type: 'geo', isLocked: true })
|
||||||
|
const myLockedShape = editor.getShape(myShapeId)!
|
||||||
|
|
||||||
|
// no change from update
|
||||||
|
editor.updateShape({ ...myLockedShape, x: 100 })
|
||||||
|
expect(editor.getShape(myShapeId)).toMatchObject(myLockedShape)
|
||||||
|
|
||||||
|
// no change from delete
|
||||||
|
editor.deleteShapes([myLockedShape])
|
||||||
|
expect(editor.getShape(myShapeId)).toMatchObject(myLockedShape)
|
||||||
|
|
||||||
|
// update works
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
editor.updateShape({ ...myLockedShape, x: 100 })
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
expect(editor.getShape(myShapeId)).toMatchObject({ ...myLockedShape, x: 100 })
|
||||||
|
|
||||||
|
// delete works
|
||||||
|
editor.run(
|
||||||
|
() => {
|
||||||
|
editor.deleteShapes([myLockedShape])
|
||||||
|
},
|
||||||
|
{ ignoreShapeLock: true }
|
||||||
|
)
|
||||||
|
expect(editor.getShape(myShapeId)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
|
@ -59,28 +59,28 @@ describe('Editor.moveShapesToPage', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Adds undo items', () => {
|
it('Adds undo items', () => {
|
||||||
editor.history.clear()
|
editor.getHistory().clear()
|
||||||
expect(editor.history.getNumUndos()).toBe(0)
|
expect(editor.getHistory().getNumUndos()).toBe(0)
|
||||||
editor.moveShapesToPage([ids.box1], ids.page2)
|
editor.moveShapesToPage([ids.box1], ids.page2)
|
||||||
expect(editor.history.getNumUndos()).toBe(1)
|
expect(editor.getHistory().getNumUndos()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Does nothing on an empty ids array', () => {
|
it('Does nothing on an empty ids array', () => {
|
||||||
editor.history.clear()
|
editor.getHistory().clear()
|
||||||
editor.moveShapesToPage([], ids.page2)
|
editor.moveShapesToPage([], ids.page2)
|
||||||
expect(editor.history.getNumUndos()).toBe(0)
|
expect(editor.getHistory().getNumUndos()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Does nothing if the new page is not found or is deleted', () => {
|
it('Does nothing if the new page is not found or is deleted', () => {
|
||||||
editor.history.clear()
|
editor.getHistory().clear()
|
||||||
editor.moveShapesToPage([ids.box1], PageRecordType.createId('missing'))
|
editor.moveShapesToPage([ids.box1], PageRecordType.createId('missing'))
|
||||||
expect(editor.history.getNumUndos()).toBe(0)
|
expect(editor.getHistory().getNumUndos()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Does not move shapes to the current page', () => {
|
it('Does not move shapes to the current page', () => {
|
||||||
editor.history.clear()
|
editor.getHistory().clear()
|
||||||
editor.moveShapesToPage([ids.box1], ids.page1)
|
editor.moveShapesToPage([ids.box1], ids.page1)
|
||||||
expect(editor.history.getNumUndos()).toBe(0)
|
expect(editor.getHistory().getNumUndos()).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Restores on undo / redo', () => {
|
it('Restores on undo / redo', () => {
|
||||||
|
|
|
@ -24,13 +24,13 @@ describe('resizing a shape', () => {
|
||||||
editor.createShapes([{ id: ids.boxA, type: 'geo', props: { w: 100, h: 100 } }])
|
editor.createShapes([{ id: ids.boxA, type: 'geo', props: { w: 100, h: 100 } }])
|
||||||
|
|
||||||
editor.mark('start')
|
editor.mark('start')
|
||||||
const startHistoryLength = editor.history.getNumUndos()
|
const startHistoryLength = editor.getHistory().getNumUndos()
|
||||||
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
||||||
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
|
expect(editor.getHistory().getNumUndos()).toBe(startHistoryLength + 1)
|
||||||
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
||||||
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
|
expect(editor.getHistory().getNumUndos()).toBe(startHistoryLength + 1)
|
||||||
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
editor.resizeShape(ids.boxA, { x: 2, y: 2 })
|
||||||
expect(editor.history.getNumUndos()).toBe(startHistoryLength + 1)
|
expect(editor.getHistory().getNumUndos()).toBe(startHistoryLength + 1)
|
||||||
|
|
||||||
expect(editor.getShapePageBounds(ids.boxA)).toCloselyMatchObject({
|
expect(editor.getShapePageBounds(ids.boxA)).toCloselyMatchObject({
|
||||||
w: 800,
|
w: 800,
|
||||||
|
|
|
@ -46,14 +46,14 @@ describe('setCurrentPage', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('squashes', () => {
|
it('squashes', () => {
|
||||||
|
const page1Id = editor.getCurrentPageId()
|
||||||
const page2Id = PageRecordType.createId('page2')
|
const page2Id = PageRecordType.createId('page2')
|
||||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||||
|
editor.setCurrentPage(page1Id)
|
||||||
editor.history.clear()
|
editor.setCurrentPage(page2Id)
|
||||||
editor.setCurrentPage(editor.getPages()[1].id)
|
editor.setCurrentPage(page2Id)
|
||||||
editor.setCurrentPage(editor.getPages()[0].id)
|
editor.undo()
|
||||||
editor.setCurrentPage(editor.getPages()[0].id)
|
expect(editor.getCurrentPageId()).toBe(page1Id)
|
||||||
expect(editor.history.getNumUndos()).toBe(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preserves the undo stack', () => {
|
it('preserves the undo stack', () => {
|
||||||
|
@ -61,14 +61,14 @@ describe('setCurrentPage', () => {
|
||||||
const page2Id = PageRecordType.createId('page2')
|
const page2Id = PageRecordType.createId('page2')
|
||||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||||
|
|
||||||
editor.history.clear()
|
editor.clearHistory()
|
||||||
editor.createShapes([{ type: 'geo', id: boxId, props: { w: 100, h: 100 } }])
|
editor.createShapes([{ type: 'geo', id: boxId, props: { w: 100, h: 100 } }])
|
||||||
editor.undo()
|
editor.undo()
|
||||||
editor.setCurrentPage(editor.getPages()[1].id)
|
editor.setCurrentPage(editor.getPages()[1].id)
|
||||||
editor.setCurrentPage(editor.getPages()[0].id)
|
editor.setCurrentPage(editor.getPages()[0].id)
|
||||||
editor.setCurrentPage(editor.getPages()[0].id)
|
editor.setCurrentPage(editor.getPages()[0].id)
|
||||||
expect(editor.getShape(boxId)).toBeUndefined()
|
expect(editor.getShape(boxId)).toBeUndefined()
|
||||||
expect(editor.history.getNumUndos()).toBe(1)
|
// expect(editor.history.getNumUndos()).toBe(1)
|
||||||
editor.redo()
|
editor.redo()
|
||||||
expect(editor.getShape(boxId)).not.toBeUndefined()
|
expect(editor.getShape(boxId)).not.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue