history options / markId / createPage (#1796)
This PR: - adds history options to several commands in order to allow them to support squashing and ephemeral data (previously, these commands had boolean values for squashing / ephemeral) It also: - changes `markId` to return the editor instance rather than the mark id passed into the command - removes `focus` and `blur` commands - changes `createPage` parameters - unifies `animateShape` / `animateShapes` options ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests
This commit is contained in:
parent
ae56d975e0
commit
8991468446
72 changed files with 977 additions and 505 deletions
|
@ -22,8 +22,6 @@ export default function APIExample() {
|
|||
// Create a shape id
|
||||
const id = createShapeId('hello')
|
||||
|
||||
editor.focus()
|
||||
|
||||
// Create a shape
|
||||
editor.createShapes<TLGeoShape>([
|
||||
{
|
||||
|
@ -72,7 +70,7 @@ export default function APIExample() {
|
|||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw persistenceKey="api-example" onMount={handleMount} autoFocus={false}>
|
||||
<Tldraw persistenceKey="api-example" onMount={handleMount}>
|
||||
<InsideOfEditorContext />
|
||||
</Tldraw>
|
||||
</div>
|
||||
|
|
|
@ -40,6 +40,7 @@ import { TLAssetPartial } from '@tldraw/tlschema';
|
|||
import { TLBaseShape } from '@tldraw/tlschema';
|
||||
import { TLBookmarkAsset } from '@tldraw/tlschema';
|
||||
import { TLCamera } from '@tldraw/tlschema';
|
||||
import { TLCursor } from '@tldraw/tlschema';
|
||||
import { TLCursorType } from '@tldraw/tlschema';
|
||||
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
|
||||
import { TLDocument } from '@tldraw/tlschema';
|
||||
|
@ -530,14 +531,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
alignShapes(shapes: TLShape[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
// (undocumented)
|
||||
alignShapes(ids: TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
|
||||
animateShape(partial: null | TLShapePartial | undefined, options?: Partial<{
|
||||
animateShape(partial: null | TLShapePartial | undefined, animationOptions?: TLAnimationOptions): this;
|
||||
animateShapes(partials: (null | TLShapePartial | undefined)[], animationOptions?: Partial<{
|
||||
duration: number;
|
||||
ease: (t: number) => number;
|
||||
easing: (t: number) => number;
|
||||
}>): this;
|
||||
animateShapes(partials: (null | TLShapePartial | undefined)[], options?: {
|
||||
duration?: number;
|
||||
ease?: (t: number) => number;
|
||||
}): this;
|
||||
animateToShape(shapeId: TLShapeId, opts?: TLAnimationOptions): this;
|
||||
animateToUser(userId: string): this;
|
||||
// @internal (undocumented)
|
||||
|
@ -551,8 +549,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
bail(): this;
|
||||
bailToMark(id: string): this;
|
||||
batch(fn: () => void): this;
|
||||
// (undocumented)
|
||||
blur: () => void;
|
||||
bringForward(shapes: TLShape[]): this;
|
||||
// (undocumented)
|
||||
bringForward(ids: TLShapeId[]): this;
|
||||
|
@ -589,9 +585,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
inputs?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
createPage(title: string, id?: TLPageId, belowPageIndex?: string): this;
|
||||
createShape<T extends TLUnknownShape>(partial: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
||||
createShapes<T extends TLUnknownShape>(partials: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
||||
createPage(page: Partial<TLPage>): this;
|
||||
createShape<T extends TLUnknownShape>(shape: OptionalKeys<TLShapePartial<T>, 'id'>): this;
|
||||
createShapes<T extends TLUnknownShape>(shapes: OptionalKeys<TLShapePartial<T>, 'id'>[]): this;
|
||||
get croppingShapeId(): null | TLShapeId;
|
||||
get currentPage(): TLPage;
|
||||
get currentPageBounds(): Box2d | undefined;
|
||||
|
@ -660,14 +656,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
flipShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical'): this;
|
||||
// (undocumented)
|
||||
flipShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
||||
// (undocumented)
|
||||
focus: () => void;
|
||||
get focusedGroupId(): TLPageId | TLShapeId;
|
||||
getAncestorPageId(shape?: TLShape): TLPageId | undefined;
|
||||
// (undocumented)
|
||||
getAncestorPageId(shapeId?: TLShapeId): TLPageId | undefined;
|
||||
getArrowInfo(shape: TLArrowShape): TLArrowInfo | undefined;
|
||||
// (undocumented)
|
||||
getArrowInfo(shape: TLArrowShape): ArrowInfo | undefined;
|
||||
getArrowInfo(id: TLShapeId): TLArrowInfo | undefined;
|
||||
getArrowsBoundTo(shapeId: TLShapeId): {
|
||||
arrowId: TLShapeId;
|
||||
handleId: "end" | "start";
|
||||
|
@ -677,9 +672,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getAsset(id: TLAssetId): TLAsset | undefined;
|
||||
getAssetForExternalContent(info: TLExternalAssetContent_2): Promise<TLAsset | undefined>;
|
||||
getContainer: () => HTMLElement;
|
||||
getContent(ids: TLShapeId[]): TLContent | undefined;
|
||||
getContentFromCurrentPage(ids: TLShapeId[]): TLContent | undefined;
|
||||
// (undocumented)
|
||||
getContent(shapes: TLShape[]): TLContent | undefined;
|
||||
getContentFromCurrentPage(shapes: TLShape[]): TLContent | undefined;
|
||||
getCurrentPageShapeIds(pageId: TLPageId): Set<TLShapeId>;
|
||||
// (undocumented)
|
||||
getCurrentPageShapeIds(page: TLPage): Set<TLShapeId>;
|
||||
|
@ -772,9 +767,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
darkMode?: boolean | undefined;
|
||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
|
||||
}>): Promise<SVGSVGElement | undefined>;
|
||||
groupShapes(ids: TLShapeId[], groupId?: TLShapeId): this;
|
||||
// (undocumented)
|
||||
groupShapes(shapes: TLShape[], groupId?: TLShapeId): this;
|
||||
// (undocumented)
|
||||
groupShapes(ids: TLShapeId[], groupId?: TLShapeId): this;
|
||||
hasAncestor(shape: TLShape | undefined, ancestorId: TLShapeId): boolean;
|
||||
// (undocumented)
|
||||
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
|
||||
|
@ -828,13 +823,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
isShapeOrAncestorLocked(shape?: TLShape): boolean;
|
||||
// (undocumented)
|
||||
isShapeOrAncestorLocked(id?: TLShapeId): boolean;
|
||||
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): string;
|
||||
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this;
|
||||
moveShapesToPage(shapes: TLShape[], pageId: TLPageId): this;
|
||||
// (undocumented)
|
||||
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
|
||||
nudgeShapes(shapes: TLShape[], offset: VecLike, ephemeral?: boolean): this;
|
||||
nudgeShapes(shapes: TLShape[], offset: VecLike, historyOptions?: CommandHistoryOptions): this;
|
||||
// (undocumented)
|
||||
nudgeShapes(ids: TLShapeId[], offset: VecLike, ephemeral?: boolean): this;
|
||||
nudgeShapes(ids: TLShapeId[], offset: VecLike, historyOptions?: CommandHistoryOptions): this;
|
||||
get onlySelectedShape(): null | TLShape;
|
||||
get openMenus(): string[];
|
||||
packShapes(shapes: TLShape[], gap: number): this;
|
||||
|
@ -850,7 +845,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
pan(offset: VecLike, animation?: TLAnimationOptions): this;
|
||||
panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this;
|
||||
popFocusLayer(): this;
|
||||
putContent(content: TLContent, options?: {
|
||||
putContentOntoCurrentPage(content: TLContent, options?: {
|
||||
point?: VecLike;
|
||||
select?: boolean;
|
||||
preservePosition?: boolean;
|
||||
|
@ -864,9 +859,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
registerExternalContentHandler<T extends TLExternalContent_2['type']>(type: T, handler: ((info: T extends TLExternalContent_2['type'] ? TLExternalContent_2 & {
|
||||
type: T;
|
||||
} : TLExternalContent_2) => void) | null): this;
|
||||
renamePage(page: TLPage, name: string, squashing?: boolean): this;
|
||||
renamePage(page: TLPage, name: string, historyOptions?: CommandHistoryOptions): this;
|
||||
// (undocumented)
|
||||
renamePage(id: TLPageId, name: string, squashing?: boolean): this;
|
||||
renamePage(id: TLPageId, name: string, historyOptions?: CommandHistoryOptions): this;
|
||||
get renderingBounds(): Box2d;
|
||||
get renderingBoundsExpanded(): Box2d;
|
||||
renderingBoundsMargin: number;
|
||||
|
@ -885,15 +880,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// (undocumented)
|
||||
reparentShapes(ids: TLShapeId[], parentId: TLParentId, insertIndex?: string): this;
|
||||
resetZoom(point?: Vec2d, animation?: TLAnimationOptions): this;
|
||||
resizeShape(id: TLShapeId, scale: VecLike, options?: {
|
||||
initialBounds?: Box2d;
|
||||
scaleOrigin?: VecLike;
|
||||
scaleAxisRotation?: number;
|
||||
initialShape?: TLShape;
|
||||
initialPageTransform?: MatLike;
|
||||
dragHandle?: TLResizeHandle;
|
||||
mode?: TLResizeMode;
|
||||
}): this;
|
||||
resizeShape(shape: TLShape, scale: VecLike, options?: TLResizeShapeOptions): this;
|
||||
// (undocumented)
|
||||
resizeShape(id: TLShapeId, scale: VecLike, options?: TLResizeShapeOptions): this;
|
||||
readonly root: RootState;
|
||||
rotateShapesBy(shapes: TLShape[], delta: number): this;
|
||||
// (undocumented)
|
||||
|
@ -921,18 +910,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
sendToBack(ids: TLShapeId[]): this;
|
||||
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
|
||||
setCroppingShapeId(id: null | TLShapeId): this;
|
||||
setCurrentPage(page: TLPage, opts?: TLViewportOptions): this;
|
||||
setCurrentPage(page: TLPage, historyOptions?: CommandHistoryOptions): this;
|
||||
// (undocumented)
|
||||
setCurrentPage(pageId: TLPageId, opts?: TLViewportOptions): this;
|
||||
setCurrentPage(pageId: TLPageId, historyOptions?: CommandHistoryOptions): this;
|
||||
setCurrentTool(id: string, info?: {}): this;
|
||||
setCursor: (cursor: Partial<TLCursor>) => this;
|
||||
setEditingShapeId(id: null | TLShapeId): this;
|
||||
setErasingShapeIds(ids: TLShapeId[]): this;
|
||||
setFocusedGroupId(next: null | TLShapeId): this;
|
||||
setHintingIds(ids: TLShapeId[]): this;
|
||||
setHoveredShapeId(id: null | TLShapeId): this;
|
||||
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
|
||||
setSelectedShapeIds(ids: TLShapeId[], squashing?: boolean): this;
|
||||
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
|
||||
setOpacity(opacity: number, historyOptions?: CommandHistoryOptions): this;
|
||||
setSelectedShapeIds(ids: TLShapeId[], historyOptions?: CommandHistoryOptions): this;
|
||||
setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: CommandHistoryOptions): this;
|
||||
shapeUtils: {
|
||||
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
||||
};
|
||||
|
@ -964,19 +954,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
toggleLock(shapes: TLShape[]): this;
|
||||
// (undocumented)
|
||||
toggleLock(ids: TLShapeId[]): this;
|
||||
undo(): HistoryManager<this>;
|
||||
undo(): this;
|
||||
ungroupShapes(ids: TLShapeId[]): this;
|
||||
// (undocumented)
|
||||
ungroupShapes(ids: TLShape[]): this;
|
||||
updateAssets(assets: TLAssetPartial[]): this;
|
||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, ephemeral?: boolean): this;
|
||||
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingShapeId' | 'focusedGroupId' | 'pageId' | 'selectedShapeIds'>>, historyOptions?: CommandHistoryOptions): this;
|
||||
updateDocumentSettings(settings: Partial<TLDocument>): this;
|
||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, ephemeral?: boolean, squashing?: boolean): this;
|
||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
|
||||
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, historyOptions?: CommandHistoryOptions): this;
|
||||
updatePage(partial: RequiredKeys<TLPage, 'id'>, historyOptions?: CommandHistoryOptions): this;
|
||||
// @internal
|
||||
updateRenderingBounds(): this;
|
||||
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, squashing?: boolean): this;
|
||||
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], squashing?: boolean): this;
|
||||
updateShape<T extends TLUnknownShape>(partial: null | TLShapePartial<T> | undefined, historyOptions?: CommandHistoryOptions): this;
|
||||
updateShapes<T extends TLUnknownShape>(partials: (null | TLShapePartial<T> | undefined)[], historyOptions?: CommandHistoryOptions): this;
|
||||
updateViewportScreenBounds(center?: boolean): this;
|
||||
readonly user: UserPreferencesManager;
|
||||
get viewportPageBounds(): Box2d;
|
||||
|
@ -1128,7 +1118,7 @@ export abstract class Geometry2d {
|
|||
export function getArcLength(C: VecLike, r: number, A: VecLike, B: VecLike): number;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getArrowheadPathForType(info: ArrowInfo, side: 'end' | 'start', strokeWidth: number): string | undefined;
|
||||
export function getArrowheadPathForType(info: TLArrowInfo, side: 'end' | 'start', strokeWidth: number): string | undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape): {
|
||||
|
@ -1140,7 +1130,7 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap
|
|||
export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string;
|
||||
|
||||
// @public
|
||||
export function getCurvedArrowHandlePath(info: ArrowInfo & {
|
||||
export function getCurvedArrowHandlePath(info: TLArrowInfo & {
|
||||
isStraight: false;
|
||||
}): string;
|
||||
|
||||
|
@ -1198,12 +1188,12 @@ export function getRotationSnapshot({ editor }: {
|
|||
}): null | TLRotationSnapshot;
|
||||
|
||||
// @public
|
||||
export function getSolidCurvedArrowPath(info: ArrowInfo & {
|
||||
export function getSolidCurvedArrowPath(info: TLArrowInfo & {
|
||||
isStraight: false;
|
||||
}): string;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getSolidStraightArrowPath(info: ArrowInfo & {
|
||||
export function getSolidStraightArrowPath(info: TLArrowInfo & {
|
||||
isStraight: true;
|
||||
}): string;
|
||||
|
||||
|
@ -1211,7 +1201,7 @@ export function getSolidStraightArrowPath(info: ArrowInfo & {
|
|||
export const getStarBounds: (sides: number, w: number, h: number) => Box2d;
|
||||
|
||||
// @public (undocumented)
|
||||
export function getStraightArrowHandlePath(info: ArrowInfo & {
|
||||
export function getStraightArrowHandlePath(info: TLArrowInfo & {
|
||||
isStraight: true;
|
||||
}): string;
|
||||
|
||||
|
@ -1988,7 +1978,7 @@ export const TAU: number;
|
|||
// @public (undocumented)
|
||||
export type TLAnimationOptions = Partial<{
|
||||
duration: number;
|
||||
easing: typeof EASINGS.easeInOutCubic;
|
||||
easing: (t: number) => number;
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -2144,7 +2134,6 @@ export type TLEditorComponents = {
|
|||
// @public (undocumented)
|
||||
export interface TLEditorOptions {
|
||||
getContainer: () => HTMLElement;
|
||||
// (undocumented)
|
||||
initialState?: string;
|
||||
shapeUtils: readonly TLShapeUtilConstructor<TLUnknownShape>[];
|
||||
store: TLStore;
|
||||
|
@ -2473,6 +2462,17 @@ export type TLResizeInfo<T extends TLShape> = {
|
|||
// @public
|
||||
export type TLResizeMode = 'resize_bounds' | 'scale_shape';
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLResizeShapeOptions = Partial<{
|
||||
initialBounds: Box2d;
|
||||
scaleOrigin: VecLike;
|
||||
scaleAxisRotation: number;
|
||||
initialShape: TLShape;
|
||||
initialPageTransform: MatLike;
|
||||
dragHandle: TLResizeHandle;
|
||||
mode: TLResizeMode;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type TLRotationSnapshot = {
|
||||
selectionPageCenter: Vec2d;
|
||||
|
|
|
@ -136,7 +136,12 @@ export {
|
|||
SVG_PADDING,
|
||||
ZOOMS,
|
||||
} from './lib/constants'
|
||||
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
|
||||
export {
|
||||
Editor,
|
||||
type TLAnimationOptions,
|
||||
type TLEditorOptions,
|
||||
type TLResizeShapeOptions,
|
||||
} from './lib/editor/Editor'
|
||||
export {
|
||||
SnapManager,
|
||||
type GapsSnapLine,
|
||||
|
|
|
@ -274,7 +274,7 @@ function TldrawEditorWithReadyStore({
|
|||
|
||||
React.useLayoutEffect(() => {
|
||||
if (editor && autoFocus) {
|
||||
editor.focus()
|
||||
editor.getContainer().focus()
|
||||
}
|
||||
}, [editor, autoFocus])
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
|||
import { HistoryManager } from './HistoryManager'
|
||||
import { CommandHistoryOptions, HistoryManager } from './HistoryManager'
|
||||
import { stack } from './Stack'
|
||||
|
||||
function createCounterHistoryManager() {
|
||||
|
@ -286,3 +286,153 @@ describe(HistoryManager, () => {
|
|||
expect(editor.getCount()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('history options', () => {
|
||||
let manager: HistoryManager<any>
|
||||
let state: { a: number; b: number }
|
||||
|
||||
let setA: (n: number, historyOptions?: CommandHistoryOptions) => any
|
||||
let setB: (n: number, historyOptions?: CommandHistoryOptions) => any
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new HistoryManager({ emit: () => void null }, () => {
|
||||
return
|
||||
})
|
||||
|
||||
state = {
|
||||
a: 0,
|
||||
b: 0,
|
||||
}
|
||||
|
||||
setA = manager.createCommand(
|
||||
'setA',
|
||||
(n: number, historyOptions?: CommandHistoryOptions) => ({
|
||||
data: { next: n, prev: state.a },
|
||||
...historyOptions,
|
||||
}),
|
||||
{
|
||||
do: ({ next }) => {
|
||||
state = { ...state, a: next }
|
||||
},
|
||||
undo: ({ prev }) => {
|
||||
state = { ...state, a: prev }
|
||||
},
|
||||
squash: ({ prev }, { next }) => ({ prev, next }),
|
||||
}
|
||||
)
|
||||
|
||||
setB = manager.createCommand(
|
||||
'setB',
|
||||
(n: number, historyOptions?: CommandHistoryOptions) => ({
|
||||
data: { next: n, prev: state.b },
|
||||
...historyOptions,
|
||||
}),
|
||||
{
|
||||
do: ({ next }) => {
|
||||
state = { ...state, b: next }
|
||||
},
|
||||
undo: ({ prev }) => {
|
||||
state = { ...state, b: prev }
|
||||
},
|
||||
squash: ({ prev }, { next }) => ({ prev, next }),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('sets, undoes, redoes', () => {
|
||||
manager.mark()
|
||||
setA(1)
|
||||
manager.mark()
|
||||
setB(1)
|
||||
manager.mark()
|
||||
setB(2)
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 2 })
|
||||
|
||||
manager.undo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 1 })
|
||||
|
||||
manager.redo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 2 })
|
||||
})
|
||||
|
||||
it('sets, undoes, redoes', () => {
|
||||
manager.mark()
|
||||
setA(1)
|
||||
manager.mark()
|
||||
setB(1)
|
||||
manager.mark()
|
||||
setB(2)
|
||||
setB(3)
|
||||
setB(4)
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 4 })
|
||||
|
||||
manager.undo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 1 })
|
||||
|
||||
manager.redo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 4 })
|
||||
})
|
||||
|
||||
it('sets ephemeral, undoes, redos', () => {
|
||||
manager.mark()
|
||||
setA(1)
|
||||
manager.mark()
|
||||
setB(1) // B 0->1
|
||||
manager.mark()
|
||||
setB(2, { ephemeral: true }) // B 0->2, but ephemeral
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 2 })
|
||||
|
||||
manager.undo() // undoes B 2->0
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 0 })
|
||||
|
||||
manager.redo() // redoes B 0->1, but not B 1-> 2
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 1 }) // no change, b 1->2 was ephemeral
|
||||
})
|
||||
|
||||
it('sets squashing, undoes, redos', () => {
|
||||
manager.mark()
|
||||
setA(1)
|
||||
manager.mark()
|
||||
setB(1)
|
||||
setB(2, { squashing: true }) // squashes with the previous command
|
||||
setB(3, { squashing: true }) // squashes with the previous command
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 3 })
|
||||
|
||||
manager.undo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 0 })
|
||||
|
||||
manager.redo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 3 })
|
||||
})
|
||||
|
||||
it('sets squashing and ephemeral, undoes, redos', () => {
|
||||
manager.mark()
|
||||
setA(1)
|
||||
manager.mark()
|
||||
setB(1)
|
||||
setB(2, { squashing: true }) // squashes with the previous command
|
||||
setB(3, { squashing: true, ephemeral: true }) // squashes with the previous command
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 3 })
|
||||
|
||||
manager.undo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 0 })
|
||||
|
||||
manager.redo()
|
||||
|
||||
expect(state).toMatchObject({ a: 1, b: 2 }) // B2->3 was ephemeral
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,13 +4,26 @@ import { uniqueId } from '../../utils/uniqueId'
|
|||
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
||||
import { Stack, stack } from './Stack'
|
||||
|
||||
/** @public */
|
||||
export type CommandHistoryOptions = Partial<{
|
||||
/**
|
||||
* When true, this command will be squashed with the previous command in the undo / redo stack.
|
||||
*/
|
||||
squashing: boolean
|
||||
/**
|
||||
* When true, this command will not add anything to the undo / redo stack. Its change will never be undone or redone.
|
||||
*/
|
||||
ephemeral: boolean
|
||||
/**
|
||||
* When true, adding this this command will not clear out the redo stack.
|
||||
*/
|
||||
preservesRedoStack: boolean
|
||||
}>
|
||||
|
||||
type CommandFn<Data> = (...args: any[]) =>
|
||||
| {
|
||||
| ({
|
||||
data: Data
|
||||
squashing?: boolean
|
||||
ephemeral?: boolean
|
||||
preservesRedoStack?: boolean
|
||||
}
|
||||
} & CommandHistoryOptions)
|
||||
| null
|
||||
| undefined
|
||||
| void
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
|
||||
import { VecLike } from '../../../../primitives/Vec2d'
|
||||
|
||||
export type ArrowPoint = {
|
||||
/** @public */
|
||||
export type TLArrowPoint = {
|
||||
handle: VecLike
|
||||
point: VecLike
|
||||
arrowhead: TLArrowShapeArrowheadStyle
|
||||
}
|
||||
|
||||
export interface ArcInfo {
|
||||
/** @public */
|
||||
export interface TLArcInfo {
|
||||
center: VecLike
|
||||
radius: number
|
||||
size: number
|
||||
|
@ -16,20 +18,21 @@ export interface ArcInfo {
|
|||
sweepFlag: number
|
||||
}
|
||||
|
||||
export type ArrowInfo =
|
||||
/** @public */
|
||||
export type TLArrowInfo =
|
||||
| {
|
||||
isStraight: false
|
||||
start: ArrowPoint
|
||||
end: ArrowPoint
|
||||
start: TLArrowPoint
|
||||
end: TLArrowPoint
|
||||
middle: VecLike
|
||||
handleArc: ArcInfo
|
||||
bodyArc: ArcInfo
|
||||
handleArc: TLArcInfo
|
||||
bodyArc: TLArcInfo
|
||||
isValid: boolean
|
||||
}
|
||||
| {
|
||||
isStraight: true
|
||||
start: ArrowPoint
|
||||
end: ArrowPoint
|
||||
start: TLArrowPoint
|
||||
end: TLArrowPoint
|
||||
middle: VecLike
|
||||
isValid: boolean
|
||||
length: number
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Vec2d, VecLike } from '../../../../primitives/Vec2d'
|
||||
import { intersectCircleCircle } from '../../../../primitives/intersect'
|
||||
import { PI, TAU } from '../../../../primitives/utils'
|
||||
import { ArrowInfo } from './arrow-types'
|
||||
import { TLArrowInfo } from './arrow-types'
|
||||
|
||||
type TLArrowPointsInfo = {
|
||||
point: VecLike
|
||||
|
@ -9,7 +9,7 @@ type TLArrowPointsInfo = {
|
|||
}
|
||||
|
||||
function getArrowPoints(
|
||||
info: ArrowInfo,
|
||||
info: TLArrowInfo,
|
||||
side: 'start' | 'end',
|
||||
strokeWidth: number
|
||||
): TLArrowPointsInfo {
|
||||
|
@ -110,7 +110,7 @@ export function getPipeHead() {
|
|||
|
||||
/** @public */
|
||||
export function getArrowheadPathForType(
|
||||
info: ArrowInfo,
|
||||
info: TLArrowInfo,
|
||||
side: 'start' | 'end',
|
||||
strokeWidth: number
|
||||
): string | undefined {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
shortAngleDist,
|
||||
} from '../../../../primitives/utils'
|
||||
import type { Editor } from '../../../Editor'
|
||||
import { ArcInfo, ArrowInfo } from './arrow-types'
|
||||
import { TLArcInfo, TLArrowInfo } from './arrow-types'
|
||||
import {
|
||||
BOUND_ARROW_OFFSET,
|
||||
MIN_ARROW_LENGTH,
|
||||
|
@ -24,7 +24,11 @@ import {
|
|||
} from './shared'
|
||||
import { getStraightArrowInfo } from './straight-arrow'
|
||||
|
||||
export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBend = 0): ArrowInfo {
|
||||
export function getCurvedArrowInfo(
|
||||
editor: Editor,
|
||||
shape: TLArrowShape,
|
||||
extraBend = 0
|
||||
): TLArrowInfo {
|
||||
const { arrowheadEnd, arrowheadStart } = shape.props
|
||||
const bend = shape.props.bend + extraBend
|
||||
|
||||
|
@ -291,7 +295,7 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
|
|||
* @param info - The arrow info.
|
||||
* @public
|
||||
*/
|
||||
export function getCurvedArrowHandlePath(info: ArrowInfo & { isStraight: false }) {
|
||||
export function getCurvedArrowHandlePath(info: TLArrowInfo & { isStraight: false }) {
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
|
@ -306,7 +310,7 @@ export function getCurvedArrowHandlePath(info: ArrowInfo & { isStraight: false }
|
|||
* @param info - The arrow info.
|
||||
* @public
|
||||
*/
|
||||
export function getSolidCurvedArrowPath(info: ArrowInfo & { isStraight: false }) {
|
||||
export function getSolidCurvedArrowPath(info: TLArrowInfo & { isStraight: false }) {
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
|
@ -373,7 +377,7 @@ export function getArcBoundingBox(center: VecLike, radius: number, start: VecLik
|
|||
* @param b - The end of the arc
|
||||
* @param c - A point on the arc
|
||||
*/
|
||||
export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): ArcInfo {
|
||||
export function getArcInfo(a: VecLike, b: VecLike, c: VecLike): TLArcInfo {
|
||||
// find a circle from the three points
|
||||
const u = -2 * (a.x * (b.y - c.y) - a.y * (b.x - c.x) + b.x * c.y - c.x * b.y)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
intersectLineSegmentPolyline,
|
||||
} from '../../../../primitives/intersect'
|
||||
import { Editor } from '../../../Editor'
|
||||
import { ArrowInfo } from './arrow-types'
|
||||
import { TLArrowInfo } from './arrow-types'
|
||||
import {
|
||||
BOUND_ARROW_OFFSET,
|
||||
BoundShapeInfo,
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
getBoundShapeInfoForTerminal,
|
||||
} from './shared'
|
||||
|
||||
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): ArrowInfo {
|
||||
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): TLArrowInfo {
|
||||
const { start, end, arrowheadStart, arrowheadEnd } = shape.props
|
||||
|
||||
const terminalsInArrowSpace = getArrowTerminalsInArrowSpace(editor, shape)
|
||||
|
@ -204,12 +204,12 @@ function updateArrowheadPointWithBoundShape(
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export function getStraightArrowHandlePath(info: ArrowInfo & { isStraight: true }) {
|
||||
export function getStraightArrowHandlePath(info: TLArrowInfo & { isStraight: true }) {
|
||||
return getArrowPath(info.start.handle, info.end.handle)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function getSolidStraightArrowPath(info: ArrowInfo & { isStraight: true }) {
|
||||
export function getSolidStraightArrowPath(info: TLArrowInfo & { isStraight: true }) {
|
||||
return getArrowPath(info.start.point, info.end.point)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
|
|
|
@ -8,7 +8,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
|
|
|
@ -80,7 +80,8 @@ export class Pointing extends StateNode {
|
|||
|
||||
const id = createShapeId()
|
||||
|
||||
this.markId = this.editor.mark(`creating:${id}`)
|
||||
this.markId = `creating:${id}`
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.createShapes<TLArrowShape>([
|
||||
{
|
||||
|
@ -109,7 +110,7 @@ export class Pointing extends StateNode {
|
|||
if (startTerminal?.type === 'binding') {
|
||||
this.editor.setHintingIds([startTerminal.boundShapeId])
|
||||
}
|
||||
this.editor.updateShapes([change], true)
|
||||
this.editor.updateShapes([change], { squashing: true })
|
||||
}
|
||||
|
||||
// Cache the current shape after those changes
|
||||
|
@ -139,7 +140,7 @@ export class Pointing extends StateNode {
|
|||
if (endTerminal?.type === 'binding') {
|
||||
this.editor.setHintingIds([endTerminal.boundShapeId])
|
||||
}
|
||||
this.editor.updateShapes([change], true)
|
||||
this.editor.updateShapes([change], { squashing: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,7 +154,7 @@ export class Pointing extends StateNode {
|
|||
})
|
||||
|
||||
if (change) {
|
||||
this.editor.updateShapes([change], true)
|
||||
this.editor.updateShapes([change], { squashing: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -364,7 +364,9 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes<TLDrawShape | TLHighlightShape>([shapePartial], true)
|
||||
this.editor.updateShapes<TLDrawShape | TLHighlightShape>([shapePartial], {
|
||||
squashing: true,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -424,7 +426,7 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial], true)
|
||||
this.editor.updateShapes([shapePartial], { squashing: true })
|
||||
}
|
||||
|
||||
break
|
||||
|
@ -566,7 +568,7 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial], true)
|
||||
this.editor.updateShapes([shapePartial], { squashing: true })
|
||||
|
||||
break
|
||||
}
|
||||
|
@ -611,7 +613,7 @@ export class Drawing extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
this.editor.updateShapes([shapePartial], true)
|
||||
this.editor.updateShapes([shapePartial], { squashing: true })
|
||||
|
||||
// Set a maximum length for the lines array; after 200 points, complete the line.
|
||||
if (newPoints.length > 500) {
|
||||
|
|
|
@ -8,7 +8,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ export const FrameLabelInput = forwardRef<
|
|||
props: { name: value },
|
||||
},
|
||||
],
|
||||
true
|
||||
{ squashing: true }
|
||||
)
|
||||
},
|
||||
[id, editor]
|
||||
|
@ -61,7 +61,7 @@ export const FrameLabelInput = forwardRef<
|
|||
props: { name: value },
|
||||
},
|
||||
],
|
||||
true
|
||||
{ squashing: true }
|
||||
)
|
||||
},
|
||||
[id, editor]
|
||||
|
|
|
@ -8,7 +8,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
|
||||
|
|
|
@ -157,7 +157,7 @@ describe('Misc', () => {
|
|||
y: 150,
|
||||
})
|
||||
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: 10 }, true)
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: 10 })
|
||||
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
|
|
|
@ -7,7 +7,7 @@ export class Idle extends StateNode {
|
|||
|
||||
override onEnter = (info: { shapeId: TLShapeId }) => {
|
||||
this.shapeId = info.shapeId
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = () => {
|
||||
|
|
|
@ -30,7 +30,8 @@ export class Pointing extends StateNode {
|
|||
const shape = info.shapeId && this.editor.getShape<TLLineShape>(info.shapeId)
|
||||
|
||||
if (shape) {
|
||||
this.markId = this.editor.mark(`creating:${shape.id}`)
|
||||
this.markId = `creating:${shape.id}`
|
||||
this.editor.mark(this.markId)
|
||||
this.shape = shape
|
||||
|
||||
if (inputs.shiftKey) {
|
||||
|
@ -85,7 +86,8 @@ export class Pointing extends StateNode {
|
|||
} else {
|
||||
const id = createShapeId()
|
||||
|
||||
this.markId = this.editor.mark(`creating:${id}`)
|
||||
this.markId = `creating:${id}`
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.createShapes<TLLineShape>([
|
||||
{
|
||||
|
|
|
@ -8,7 +8,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onCancel = () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
|
||||
|
|
|
@ -19,7 +19,8 @@ export class Pointing extends StateNode {
|
|||
|
||||
const id = createShapeId()
|
||||
|
||||
this.markId = this.editor.mark(`creating:${id}`)
|
||||
this.markId = `creating:${id}`
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.createShapes<TLTextShape>([
|
||||
{
|
||||
|
|
|
@ -10,6 +10,6 @@ export class EraserTool extends StateNode {
|
|||
static override children = () => [Idle, Pointing, Erasing]
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ export class Erasing extends StateNode {
|
|||
private excludedShapeIds = new Set<TLShapeId>()
|
||||
|
||||
override onEnter = (info: TLPointerEventInfo) => {
|
||||
this.markId = this.editor.mark('erase scribble begin')
|
||||
this.markId = 'erase scribble begin'
|
||||
this.editor.mark(this.markId)
|
||||
this.info = info
|
||||
|
||||
const { originPagePoint } = this.editor.inputs
|
||||
|
|
|
@ -4,7 +4,7 @@ export class Idle extends StateNode {
|
|||
static override id = 'idle'
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'grab', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'grab', rotation: 0 })
|
||||
}
|
||||
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
|
|
|
@ -5,7 +5,10 @@ export class Pointing extends StateNode {
|
|||
|
||||
override onEnter = () => {
|
||||
this.editor.stopCameraAnimation()
|
||||
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'grabbing', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
|
|
|
@ -9,6 +9,6 @@ export class LaserTool extends StateNode {
|
|||
static override children = () => [Idle, Lasering]
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
|
||||
override onCancel?: TLCancelEvent | undefined = (info) => {
|
||||
this.editor.setSelectedShapeIds(this.initialSelectedShapeIds, true)
|
||||
this.editor.setSelectedShapeIds(this.initialSelectedShapeIds, { squashing: true })
|
||||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
||||
|
@ -168,7 +168,7 @@ export class Brushing extends StateNode {
|
|||
}
|
||||
|
||||
this.editor.updateInstanceState({ brush: { ...this.brush.toJson() } })
|
||||
this.editor.setSelectedShapeIds(Array.from(results), true)
|
||||
this.editor.setSelectedShapeIds(Array.from(results), { squashing: true })
|
||||
}
|
||||
|
||||
override onInterrupt: TLInterruptEvent = () => {
|
||||
|
|
|
@ -5,7 +5,10 @@ export class Idle extends StateNode {
|
|||
static override id = 'idle'
|
||||
|
||||
override onEnter = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
|
||||
const { onlySelectedShape } = this.editor
|
||||
|
||||
|
@ -22,7 +25,10 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
override onExit: TLExitEventHandler = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
|
||||
this.editor.off('change-history', this.cleanupCroppingState)
|
||||
}
|
||||
|
|
|
@ -27,12 +27,15 @@ export class TranslatingCrop extends StateNode {
|
|||
this.snapshot = this.createSnapshot()
|
||||
|
||||
this.editor.mark(this.markId)
|
||||
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'move', rotation: 0 })
|
||||
this.updateShapes()
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerMove = () => {
|
||||
|
@ -99,7 +102,7 @@ export class TranslatingCrop extends StateNode {
|
|||
const partial = getTranslateCroppedImageChange(this.editor, shape, delta)
|
||||
|
||||
if (partial) {
|
||||
this.editor.updateShapes([partial], true)
|
||||
this.editor.updateShapes([partial], { squashing: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,8 @@ export class Cropping extends StateNode {
|
|||
}
|
||||
) => {
|
||||
this.info = info
|
||||
this.markId = this.editor.mark('cropping')
|
||||
this.markId = 'cropping'
|
||||
this.editor.mark(this.markId)
|
||||
this.snapshot = this.createSnapshot()
|
||||
this.updateShapes()
|
||||
}
|
||||
|
@ -199,7 +200,7 @@ export class Cropping extends StateNode {
|
|||
},
|
||||
}
|
||||
|
||||
this.editor.updateShapes([partial], true)
|
||||
this.editor.updateShapes([partial], { squashing: true })
|
||||
this.updateCursor()
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,8 @@ export class DraggingHandle extends StateNode {
|
|||
this.info = info
|
||||
this.parent.currentToolIdMask = info.onInteractionEnd
|
||||
this.shapeId = shape.id
|
||||
this.markId = isCreating ? `creating:${shape.id}` : this.editor.mark('dragging handle')
|
||||
this.markId = isCreating ? `creating:${shape.id}` : 'dragging handle'
|
||||
if (!isCreating) this.editor.mark(this.markId)
|
||||
this.initialHandle = deepCopy(handle)
|
||||
this.initialPageTransform = this.editor.getShapePageTransform(shape)!
|
||||
this.initialPageRotation = this.initialPageTransform.rotation()
|
||||
|
@ -60,7 +61,7 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
|
||||
true
|
||||
{ ephemeral: true }
|
||||
)
|
||||
|
||||
// <!-- Only relevant to arrows
|
||||
|
@ -168,7 +169,10 @@ export class DraggingHandle extends StateNode {
|
|||
this.parent.currentToolIdMask = undefined
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.snaps.clear()
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
private complete() {
|
||||
|
@ -298,7 +302,7 @@ export class DraggingHandle extends StateNode {
|
|||
}
|
||||
|
||||
if (changes) {
|
||||
editor.updateShapes([next], true)
|
||||
editor.updateShapes([next], { squashing: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,10 @@ export class Idle extends StateNode {
|
|||
|
||||
override onEnter = () => {
|
||||
this.parent.currentToolIdMask = undefined
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
|
|
|
@ -33,7 +33,10 @@ export class PointingCropHandle extends StateNode {
|
|||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
this.parent.currentToolIdMask = undefined
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,18 @@ export class PointingHandle extends StateNode {
|
|||
this.editor.setHintingIds([initialTerminal.boundShapeId])
|
||||
}
|
||||
|
||||
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'grabbing', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.setHintingIds([])
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
|
|
|
@ -28,7 +28,10 @@ export class PointingRotateHandle extends StateNode {
|
|||
|
||||
override onExit = () => {
|
||||
this.parent.currentToolIdMask = undefined
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
override onPointerMove = () => {
|
||||
|
|
|
@ -56,13 +56,16 @@ export class Resizing extends StateNode {
|
|||
this.creationCursorOffset = creationCursorOffset
|
||||
|
||||
if (info.isCreating) {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'cross', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
|
||||
this.snapshot = this._createSnapshot()
|
||||
this.markId = isCreating
|
||||
? `creating:${this.editor.onlySelectedShape!.id}`
|
||||
: this.editor.mark('starting resizing')
|
||||
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'starting resizing'
|
||||
|
||||
if (!isCreating) this.editor.mark(this.markId)
|
||||
|
||||
this.handleResizeStart()
|
||||
this.updateShapes()
|
||||
|
@ -349,12 +352,15 @@ export class Resizing extends StateNode {
|
|||
|
||||
nextCursor.rotation = rotation
|
||||
|
||||
this.editor.updateInstanceState({ cursor: nextCursor })
|
||||
this.editor.setCursor(nextCursor)
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.parent.currentToolIdMask = undefined
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
this.editor.snaps.clear()
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ export class Rotating extends StateNode {
|
|||
this.info = info
|
||||
this.parent.currentToolIdMask = info.onInteractionEnd
|
||||
|
||||
this.markId = this.editor.mark('rotate start')
|
||||
this.markId = 'rotate start'
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
const snapshot = getRotationSnapshot({ editor: this.editor })
|
||||
if (!snapshot) return this.parent.transition('idle', this.info)
|
||||
|
@ -40,7 +41,7 @@ export class Rotating extends StateNode {
|
|||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'none', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'default', rotation: 0 })
|
||||
this.parent.currentToolIdMask = undefined
|
||||
|
||||
this.snapshot = {} as TLRotationSnapshot
|
||||
|
|
|
@ -170,7 +170,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
: [...newlySelectedShapeIds]
|
||||
),
|
||||
],
|
||||
true
|
||||
{ squashing: true }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
}
|
||||
|
||||
private cancel() {
|
||||
this.editor.setSelectedShapeIds([...this.initialSelectedShapeIds], true)
|
||||
this.editor.setSelectedShapeIds([...this.initialSelectedShapeIds], { squashing: true })
|
||||
this.parent.transition('idle', {})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,9 +53,8 @@ export class Translating extends StateNode {
|
|||
this.isCreating = isCreating
|
||||
this.editAfterComplete = editAfterComplete
|
||||
|
||||
this.markId = isCreating
|
||||
? this.editor.mark(`creating:${this.editor.onlySelectedShape!.id}`)
|
||||
: this.editor.mark('translating')
|
||||
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'translating'
|
||||
this.editor.mark(this.markId)
|
||||
this.handleEnter(info)
|
||||
this.editor.on('tick', this.updateParent)
|
||||
}
|
||||
|
@ -66,7 +65,10 @@ export class Translating extends StateNode {
|
|||
this.selectionSnapshot = {} as any
|
||||
this.snapshot = {} as any
|
||||
this.editor.snaps.clear()
|
||||
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'default', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
this.dragAndDropManager.clear()
|
||||
}
|
||||
|
||||
|
@ -111,7 +113,8 @@ export class Translating extends StateNode {
|
|||
|
||||
this.isCloning = true
|
||||
this.reset()
|
||||
this.markId = this.editor.mark('translating')
|
||||
this.markId = 'translating'
|
||||
this.editor.mark(this.markId)
|
||||
|
||||
this.editor.duplicateShapes(Array.from(this.editor.selectedShapeIds))
|
||||
|
||||
|
@ -124,7 +127,8 @@ export class Translating extends StateNode {
|
|||
this.isCloning = false
|
||||
this.snapshot = this.selectionSnapshot
|
||||
this.reset()
|
||||
this.markId = this.editor.mark('translating')
|
||||
this.markId = 'translating'
|
||||
this.editor.mark(this.markId)
|
||||
this.updateShapes()
|
||||
}
|
||||
|
||||
|
@ -171,7 +175,7 @@ export class Translating extends StateNode {
|
|||
this.isCloning = false
|
||||
this.info = info
|
||||
|
||||
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
|
||||
this.editor.setCursor({ type: 'move', rotation: 0 })
|
||||
this.selectionSnapshot = getTranslatingSnapshot(this.editor)
|
||||
|
||||
// Don't clone on create; otherwise clone on altKey
|
||||
|
@ -406,6 +410,6 @@ export function moveShapesToPoint({
|
|||
}
|
||||
})
|
||||
),
|
||||
true
|
||||
{ squashing: true }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class ZoomTool extends StateNode {
|
|||
this.currentToolIdMask = undefined
|
||||
this.editor.updateInstanceState(
|
||||
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
|
||||
true
|
||||
{ ephemeral: true }
|
||||
)
|
||||
this.currentToolIdMask = undefined
|
||||
}
|
||||
|
@ -53,9 +53,15 @@ export class ZoomTool extends StateNode {
|
|||
|
||||
private updateCursor() {
|
||||
if (this.editor.inputs.altKey) {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'zoom-out', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'zoom-out', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
} else {
|
||||
this.editor.updateInstanceState({ cursor: { type: 'zoom-in', rotation: 0 } }, true)
|
||||
this.editor.updateInstanceState(
|
||||
{ cursor: { type: 'zoom-in', rotation: 0 } },
|
||||
{ ephemeral: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,13 +77,11 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
|
|||
<_ContextMenu.Item
|
||||
key="new-page"
|
||||
onSelect={() => {
|
||||
editor.mark('move_shapes_to_page')
|
||||
const newPageId = PageRecordType.createId()
|
||||
const ids = editor.selectedShapeIds
|
||||
const oldPageId = editor.currentPageId
|
||||
editor.batch(() => {
|
||||
editor.createPage('Page 1', newPageId)
|
||||
editor.setCurrentPage(oldPageId)
|
||||
editor.mark('move_shapes_to_page')
|
||||
editor.createPage({ name: 'Page', id: newPageId })
|
||||
editor.moveShapesToPage(ids, newPageId)
|
||||
})
|
||||
}}
|
||||
|
|
|
@ -17,7 +17,7 @@ export const PageItemInput = function PageItemInput({
|
|||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
editor.renamePage(id, value ? value : 'New Page', true)
|
||||
editor.renamePage(id, value ? value : 'New Page', { ephemeral: true })
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ export const PageItemInput = function PageItemInput({
|
|||
const handleComplete = useCallback(
|
||||
(value: string) => {
|
||||
editor.mark('rename page')
|
||||
editor.renamePage(id, value || 'New Page', false)
|
||||
editor.renamePage(id, value || 'New Page', { ephemeral: false })
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
|
|
@ -240,10 +240,13 @@ export const PageMenu = function PageMenu() {
|
|||
const handleCreatePageClick = useCallback(() => {
|
||||
if (isReadonlyMode) return
|
||||
|
||||
editor.batch(() => {
|
||||
editor.mark('creating page')
|
||||
const newPageId = PageRecordType.createId()
|
||||
editor.createPage(msg('page-menu.new-page-initial-name'), newPageId)
|
||||
editor.createPage({ name: msg('page-menu.new-page-initial-name'), id: newPageId })
|
||||
editor.setCurrentPage(newPageId)
|
||||
setIsEditing(true)
|
||||
})
|
||||
}, [editor, msg, isReadonlyMode])
|
||||
|
||||
return (
|
||||
|
@ -383,8 +386,10 @@ export const PageMenu = function PageMenu() {
|
|||
editor.renamePage(page.id, name)
|
||||
}
|
||||
} else {
|
||||
editor.batch(() => {
|
||||
setIsEditing(true)
|
||||
editor.setCurrentPage(page.id)
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -95,7 +95,7 @@ function useStyleChangeCallback() {
|
|||
|
||||
return React.useMemo(() => {
|
||||
return function <T>(style: StyleProp<T>, value: T, squashing: boolean) {
|
||||
editor.setStyle(style, value, squashing)
|
||||
editor.setStyle(style, value, { squashing })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
}
|
||||
}, [editor])
|
||||
|
@ -118,7 +118,7 @@ function CommonStylePickerSet({
|
|||
const handleOpacityValueChange = React.useCallback(
|
||||
(value: number, ephemeral: boolean) => {
|
||||
const item = tldrawSupportedOpacities[value]
|
||||
editor.setOpacity(item, ephemeral)
|
||||
editor.setOpacity(item, { ephemeral })
|
||||
editor.updateInstanceState({ isChangingStyle: true })
|
||||
},
|
||||
[editor]
|
||||
|
|
|
@ -323,7 +323,7 @@ export async function pasteExcalidrawContent(editor: Editor, clipboard: any, poi
|
|||
|
||||
editor.mark('paste')
|
||||
|
||||
editor.putContent(tldrawContent, {
|
||||
editor.putContentOntoCurrentPage(tldrawContent, {
|
||||
point: p,
|
||||
select: false,
|
||||
preserveIds: true,
|
||||
|
|
|
@ -12,7 +12,7 @@ export function pasteTldrawContent(editor: Editor, clipboard: TLContent, point?:
|
|||
const p = point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : undefined)
|
||||
|
||||
editor.mark('paste')
|
||||
editor.putContent(clipboard, {
|
||||
editor.putContentOntoCurrentPage(clipboard, {
|
||||
point: p,
|
||||
select: true,
|
||||
})
|
||||
|
|
|
@ -210,7 +210,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
readonlyOk: false,
|
||||
onSelect(source) {
|
||||
trackEvent('toggle-auto-size', { source })
|
||||
editor.mark()
|
||||
editor.mark('toggling auto size')
|
||||
editor.updateShapes(
|
||||
editor.selectedShapes
|
||||
.filter(
|
||||
|
@ -842,7 +842,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
{
|
||||
exportBackground: !editor.instanceState.exportBackground,
|
||||
},
|
||||
true
|
||||
{ ephemeral: true }
|
||||
)
|
||||
},
|
||||
checkbox: true,
|
||||
|
@ -902,7 +902,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
{
|
||||
isDebugMode: !editor.instanceState.isDebugMode,
|
||||
},
|
||||
true
|
||||
{ ephemeral: true }
|
||||
)
|
||||
},
|
||||
checkbox: true,
|
||||
|
|
|
@ -514,7 +514,7 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
|
|||
* @public
|
||||
*/
|
||||
const handleNativeOrMenuCopy = (editor: Editor) => {
|
||||
const content = editor.getContent(editor.selectedShapeIds)
|
||||
const content = editor.getContentFromCurrentPage(editor.selectedShapeIds)
|
||||
if (!content) {
|
||||
if (navigator && navigator.clipboard) {
|
||||
navigator.clipboard.writeText('')
|
||||
|
|
|
@ -93,7 +93,7 @@ export function useCopyAs() {
|
|||
}
|
||||
|
||||
case 'json': {
|
||||
const data = editor.getContent(ids)
|
||||
const data = editor.getContentFromCurrentPage(ids)
|
||||
|
||||
if (window.navigator.clipboard) {
|
||||
const jsonStr = JSON.stringify(data)
|
||||
|
|
|
@ -79,7 +79,7 @@ export function useExportAs() {
|
|||
}
|
||||
|
||||
case 'json': {
|
||||
const data = editor.getContent(ids)
|
||||
const data = editor.getContentFromCurrentPage(ids)
|
||||
const dataURL = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(data, null, 4)], { type: 'application/json' })
|
||||
)
|
||||
|
|
|
@ -111,7 +111,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
|
|||
[GeoShapeGeoStyle.id]: id,
|
||||
},
|
||||
},
|
||||
true
|
||||
{ ephemeral: true }
|
||||
)
|
||||
editor.setCurrentTool('geo')
|
||||
trackEvent('select-tool', { source, id: `geo-${id}` })
|
||||
|
|
|
@ -113,7 +113,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
|
|||
} else {
|
||||
const pageId = PageRecordType.createId()
|
||||
v1PageIdsToV2PageIds.set(v1Page.id, pageId)
|
||||
editor.createPage(v1Page.name ?? 'Page', pageId)
|
||||
editor.createPage({ name: v1Page.name ?? 'Page', id: pageId })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ beforeEach(() => {
|
|||
])
|
||||
|
||||
const page1 = editor.currentPageId
|
||||
editor.createPage('page 2', ids.page2)
|
||||
editor.createPage({ name: 'page 2', id: ids.page2 })
|
||||
editor.setCurrentPage(page1)
|
||||
})
|
||||
|
||||
|
@ -429,12 +429,12 @@ describe('isFocused', () => {
|
|||
expect(focusMock).not.toHaveBeenCalled()
|
||||
expect(blurMock).not.toHaveBeenCalled()
|
||||
|
||||
editor.focus()
|
||||
editor.getContainer().focus()
|
||||
|
||||
expect(focusMock).toHaveBeenCalled()
|
||||
expect(blurMock).not.toHaveBeenCalled()
|
||||
|
||||
editor.blur()
|
||||
editor.getContainer().blur()
|
||||
|
||||
expect(blurMock).toHaveBeenCalled()
|
||||
})
|
||||
|
@ -471,7 +471,7 @@ describe('getShapeUtil', () => {
|
|||
{ id: ids.box1, type: 'blorg', x: 100, y: 100, props: { w: 100, h: 100 } },
|
||||
])
|
||||
const page1 = editor.currentPageId
|
||||
editor.createPage('page 2', ids.page2)
|
||||
editor.createPage({ name: 'page 2', id: ids.page2 })
|
||||
editor.setCurrentPage(page1)
|
||||
})
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('createSessionStateSnapshotSignal', () => {
|
|||
expect(isGridMode).toBe(true)
|
||||
expect(numPages).toBe(1)
|
||||
|
||||
editor.createPage('new page')
|
||||
editor.createPage({ name: 'new page' })
|
||||
|
||||
expect(isGridMode).toBe(true)
|
||||
expect(editor.pages.length).toBe(2)
|
||||
|
|
|
@ -136,7 +136,7 @@ export class TestEditor extends Editor {
|
|||
|
||||
copy = (ids = this.selectedShapeIds) => {
|
||||
if (ids.length > 0) {
|
||||
const content = this.getContent(ids)
|
||||
const content = this.getContentFromCurrentPage(ids)
|
||||
if (content) {
|
||||
this.clipboard = content
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export class TestEditor extends Editor {
|
|||
|
||||
cut = (ids = this.selectedShapeIds) => {
|
||||
if (ids.length > 0) {
|
||||
const content = this.getContent(ids)
|
||||
const content = this.getContentFromCurrentPage(ids)
|
||||
if (content) {
|
||||
this.clipboard = content
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ export class TestEditor extends Editor {
|
|||
const p = this.inputs.shiftKey ? this.inputs.currentPagePoint : point
|
||||
|
||||
this.mark('pasting')
|
||||
this.putContent(this.clipboard, {
|
||||
this.putContentOntoCurrentPage(this.clipboard, {
|
||||
point: p,
|
||||
select: true,
|
||||
})
|
||||
|
|
|
@ -219,7 +219,10 @@ describe('<TldrawEditor />', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
await act(async () => {
|
||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
|
||||
editor.updateInstanceState(
|
||||
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
})
|
||||
|
||||
const id = createShapeId()
|
||||
|
@ -340,7 +343,10 @@ describe('Custom shapes', () => {
|
|||
|
||||
expect(editor).toBeTruthy()
|
||||
await act(async () => {
|
||||
editor.updateInstanceState({ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } }, true, true)
|
||||
editor.updateInstanceState(
|
||||
{ screenBounds: { x: 0, y: 0, w: 1080, h: 720 } },
|
||||
{ ephemeral: true, squashing: true }
|
||||
)
|
||||
})
|
||||
|
||||
expect(editor.shapeUtils.card).toBeTruthy()
|
||||
|
|
|
@ -248,7 +248,7 @@ describe('arrowBindingsIndex', () => {
|
|||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
||||
editor.nudgeShapes([ids.box2], { x: 0, y: -1 }, true)
|
||||
editor.nudgeShapes([ids.box2], { x: 0, y: -1 })
|
||||
|
||||
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
|
||||
expect(editor.getArrowsBoundTo(ids.box1)).toHaveLength(3)
|
||||
|
|
129
packages/tldraw/src/test/cleanup.test.ts
Normal file
129
packages/tldraw/src/test/cleanup.test.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import { TLArrowShape, createShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from './TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
const ids = {
|
||||
box1: createShapeId('box1'),
|
||||
box2: createShapeId('box2'),
|
||||
box3: createShapeId('box3'),
|
||||
box4: createShapeId('box4'),
|
||||
box5: createShapeId('box5'),
|
||||
frame1: createShapeId('frame1'),
|
||||
group1: createShapeId('group1'),
|
||||
group2: createShapeId('group2'),
|
||||
group3: createShapeId('group3'),
|
||||
arrow1: createShapeId('arrow1'),
|
||||
arrow2: createShapeId('arrow2'),
|
||||
arrow3: createShapeId('arrow3'),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
function arrow() {
|
||||
return editor.currentPageShapes.find((s) => s.type === 'arrow') as TLArrowShape
|
||||
}
|
||||
|
||||
describe('restoring bound arrows', () => {
|
||||
beforeEach(() => {
|
||||
editor.createShapes([
|
||||
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
|
||||
{ id: ids.box2, type: 'geo', x: 200, y: 0 },
|
||||
])
|
||||
// create arrow from box1 to box2
|
||||
editor
|
||||
.setCurrentTool('arrow')
|
||||
.pointerMove(50, 50)
|
||||
.pointerDown()
|
||||
.pointerMove(250, 50)
|
||||
.pointerUp()
|
||||
})
|
||||
|
||||
it('removes bound arrows on delete, restores them on undo but only when change was done by user', () => {
|
||||
editor.mark('deleting')
|
||||
editor.deleteShapes([ids.box2])
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
editor.undo()
|
||||
expect(arrow().props.end.type).toBe('binding')
|
||||
editor.redo()
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
})
|
||||
|
||||
it('removes / restores multiple bindings', () => {
|
||||
editor.mark('deleting')
|
||||
expect(arrow().props.start.type).toBe('binding')
|
||||
expect(arrow().props.end.type).toBe('binding')
|
||||
|
||||
editor.deleteShapes([ids.box1, ids.box2])
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
|
||||
editor.undo()
|
||||
expect(arrow().props.start.type).toBe('binding')
|
||||
expect(arrow().props.end.type).toBe('binding')
|
||||
|
||||
editor.redo()
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoring bound arrows multiplayer', () => {
|
||||
it('restores bound arrows after the shape was deleted by a different client', () => {
|
||||
editor.mark('before creating box shape')
|
||||
editor.createShapes([{ id: ids.box2, type: 'geo', x: 100, y: 0 }])
|
||||
|
||||
editor.setCurrentTool('arrow').pointerMove(0, 50).pointerDown().pointerMove(150, 50).pointerUp()
|
||||
|
||||
// console.log(JSON.stringify(editor.history._undos.value.toArray(), null, 2))
|
||||
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('binding')
|
||||
|
||||
// Merge a change from a remote source that deletes box 2
|
||||
editor.store.mergeRemoteChanges(() => {
|
||||
editor.store.remove([ids.box2])
|
||||
})
|
||||
|
||||
// box is gone
|
||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||
// arrow is still there, but without its binding
|
||||
expect(arrow()).not.toBeUndefined()
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
|
||||
editor.undo() // undo creating the arrow
|
||||
|
||||
// arrow is gone too now
|
||||
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||
|
||||
editor.redo() // redo creating the arrow
|
||||
|
||||
expect(editor.getShape(ids.box2)).toBeUndefined()
|
||||
expect(arrow()).not.toBeUndefined()
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('point')
|
||||
|
||||
editor.undo() // undo creating arrow
|
||||
|
||||
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||
|
||||
editor.undo() // undo creating box
|
||||
|
||||
expect(editor.currentPageShapeIds.size).toBe(0)
|
||||
|
||||
editor.redo() // redo creating box
|
||||
|
||||
// box is back! arrow is gone
|
||||
expect(editor.getShape(ids.box2)).not.toBeUndefined()
|
||||
expect(arrow()).toBeUndefined()
|
||||
|
||||
editor.redo() // redo creating arrow
|
||||
|
||||
// box is back! arrow should be bound
|
||||
expect(arrow().props.start.type).toBe('point')
|
||||
expect(arrow().props.end.type).toBe('binding')
|
||||
})
|
||||
})
|
|
@ -10,13 +10,21 @@ beforeEach(() => {
|
|||
it('Creates a page', () => {
|
||||
const oldPageId = editor.currentPageId
|
||||
const n = editor.pages.length
|
||||
editor.createPage('Page 1')
|
||||
editor.mark('creating new page')
|
||||
editor.createPage({ name: 'Page 1' })
|
||||
expect(editor.pages.length).toBe(n + 1)
|
||||
const newPageId = editor.pages[n].id
|
||||
// does not move to the new page right away
|
||||
expect(editor.currentPageId).toBe(oldPageId)
|
||||
|
||||
// needs to be done manually
|
||||
editor.setCurrentPage(newPageId)
|
||||
expect(editor.currentPageId).toBe(newPageId)
|
||||
|
||||
editor.undo()
|
||||
expect(editor.pages.length).toBe(n)
|
||||
expect(editor.currentPageId).toBe(oldPageId)
|
||||
|
||||
editor.redo()
|
||||
expect(editor.pages.length).toBe(n + 1)
|
||||
expect(editor.currentPageId).toBe(newPageId)
|
||||
|
@ -24,7 +32,7 @@ it('Creates a page', () => {
|
|||
|
||||
it("Doesn't create a page if max pages is reached", () => {
|
||||
for (let i = 0; i < MAX_PAGES + 1; i++) {
|
||||
editor.createPage(`Test Page ${i}`)
|
||||
editor.createPage({ name: `Test Page ${i}` })
|
||||
}
|
||||
expect(editor.pages.length).toBe(MAX_PAGES)
|
||||
})
|
||||
|
@ -52,6 +60,6 @@ it('[regression] does not die if every page has the same index', () => {
|
|||
|
||||
expect(editor.pages.every((p) => p.index === page.index)).toBe(true)
|
||||
|
||||
editor.createPage('My Special Test Page')
|
||||
editor.createPage({ name: 'My Special Test Page' })
|
||||
expect(editor.pages.some((p) => p.name === 'My Special Test Page')).toBe(true)
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ beforeEach(() => {
|
|||
describe('deletePage', () => {
|
||||
it('deletes the page', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
const pages = editor.pages
|
||||
expect(pages.length).toBe(2)
|
||||
|
@ -20,13 +20,13 @@ describe('deletePage', () => {
|
|||
})
|
||||
it('is undoable and redoable', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.mark()
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.mark('before creating page')
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
const pages = editor.pages
|
||||
expect(pages.length).toBe(2)
|
||||
|
||||
editor.mark()
|
||||
editor.mark('before deleting page')
|
||||
editor.deletePage(pages[0].id)
|
||||
expect(editor.pages.length).toBe(1)
|
||||
|
||||
|
@ -39,8 +39,8 @@ describe('deletePage', () => {
|
|||
})
|
||||
it('does not allow deleting all pages', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.mark()
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.mark('before creating page')
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
const pages = editor.pages
|
||||
editor.deletePage(pages[1].id)
|
||||
|
@ -53,8 +53,8 @@ describe('deletePage', () => {
|
|||
})
|
||||
it('switches the page if you are deleting the current page', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.mark()
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.mark('before creating page')
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
const currentPageId = editor.currentPageId
|
||||
editor.deletePage(currentPageId)
|
||||
|
@ -65,8 +65,8 @@ describe('deletePage', () => {
|
|||
it('switches the page if another user or tab deletes the current page', () => {
|
||||
const currentPageId = editor.currentPageId
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.mark()
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.mark('before creating')
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
editor.store.mergeRemoteChanges(() => {
|
||||
editor.store.remove([currentPageId])
|
||||
|
|
|
@ -49,7 +49,7 @@ beforeEach(() => {
|
|||
describe('Editor.deleteShapes', () => {
|
||||
it('Deletes a shape', () => {
|
||||
editor.select(ids.box3, ids.box4)
|
||||
editor.mark()
|
||||
editor.mark('before deleting')
|
||||
editor.deleteShapes(editor.selectedShapeIds) // delete the selected shapes
|
||||
expect(editor.getShape(ids.box3)).toBeUndefined()
|
||||
expect(editor.getShape(ids.box4)).toBeUndefined()
|
||||
|
@ -74,7 +74,7 @@ describe('Editor.deleteShapes', () => {
|
|||
it('Deletes descendants', () => {
|
||||
editor.reparentShapes([ids.box4], ids.box3)
|
||||
editor.select(ids.box3)
|
||||
editor.mark()
|
||||
editor.mark('before deleting')
|
||||
editor.deleteShapes(editor.selectedShapeIds) // should be a noop, nothing to delete
|
||||
expect(editor.getShape(ids.box3)).toBeUndefined()
|
||||
expect(editor.getShape(ids.box4)).toBeUndefined()
|
||||
|
@ -90,7 +90,7 @@ describe('Editor.deleteShapes', () => {
|
|||
describe('When deleting arrows', () => {
|
||||
it('Restores any bindings on undo', () => {
|
||||
editor.select(ids.arrow1)
|
||||
editor.mark()
|
||||
editor.mark('before deleting')
|
||||
// @ts-expect-error
|
||||
expect(editor._arrowBindingsIndex.value[ids.box1]).not.toBeUndefined()
|
||||
// @ts-expect-error
|
||||
|
|
|
@ -14,14 +14,18 @@ const ids = {
|
|||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
editor.createPage(ids.page1, ids.page1)
|
||||
const page0Id = editor.currentPageId
|
||||
editor.createPage({ name: ids.page1, id: ids.page1 })
|
||||
expect(editor.currentPageId).toBe(page0Id)
|
||||
editor.setCurrentPage(ids.page1)
|
||||
expect(editor.currentPageId).toBe(ids.page1)
|
||||
editor.createShapes([
|
||||
{ id: ids.ellipse1, type: 'geo', x: 0, y: 0, props: { geo: 'ellipse' } },
|
||||
{ id: ids.box1, type: 'geo', x: 0, y: 0 },
|
||||
{ id: ids.box2, parentId: ids.box1, type: 'geo', x: 150, y: 150 },
|
||||
])
|
||||
editor.createPage(ids.page2, ids.page2)
|
||||
editor.setCurrentPage(ids.page1)
|
||||
editor.createPage({ name: ids.page2, id: ids.page2 })
|
||||
expect(editor.currentPageId).toBe(ids.page1)
|
||||
|
||||
expect(editor.getShape(ids.box1)!.parentId).toEqual(ids.page1)
|
||||
expect(editor.getShape(ids.box2)!.parentId).toEqual(ids.box1)
|
||||
|
@ -103,7 +107,8 @@ describe('Editor.moveShapesToPage', () => {
|
|||
editor = new TestEditor()
|
||||
const page2Id = PageRecordType.createId('newPage2')
|
||||
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
editor.setCurrentPage(page2Id)
|
||||
expect(editor.currentPageId).toBe(page2Id)
|
||||
editor.createShapes([{ id: ids.box1, type: 'geo', x: 0, y: 0, props: { geo: 'ellipse' } }])
|
||||
editor.expectShapeToMatch({
|
||||
|
@ -113,7 +118,9 @@ describe('Editor.moveShapesToPage', () => {
|
|||
})
|
||||
|
||||
const page3Id = PageRecordType.createId('newPage3')
|
||||
editor.createPage('New Page 3', page3Id)
|
||||
|
||||
editor.createPage({ name: 'New Page 3', id: page3Id })
|
||||
editor.setCurrentPage(page3Id)
|
||||
expect(editor.currentPageId).toBe(page3Id)
|
||||
editor.createShapes([{ id: ids.box2, type: 'geo', x: 0, y: 0, props: { geo: 'ellipse' } }])
|
||||
editor.expectShapeToMatch({
|
||||
|
|
|
@ -39,22 +39,22 @@ function nudgeAndGet(ids: TLShapeId[], key: string, shiftKey: boolean) {
|
|||
switch (key) {
|
||||
case 'ArrowLeft': {
|
||||
editor.mark('nudge')
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: -step, y: 0 }, shiftKey)
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: -step, y: 0 })
|
||||
break
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
editor.mark('nudge')
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: step, y: 0 }, shiftKey)
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: step, y: 0 })
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
editor.mark('nudge')
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: -step }, shiftKey)
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: -step })
|
||||
break
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
editor.mark('nudge')
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: step }, shiftKey)
|
||||
editor.nudgeShapes(editor.selectedShapeIds, { x: 0, y: step })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,17 @@ describe('Migrations', () => {
|
|||
const withoutSchema = structuredClone(clipboardContent)
|
||||
// @ts-expect-error
|
||||
delete withoutSchema.schema
|
||||
expect(() => editor.putContent(withoutSchema)).toThrowError()
|
||||
expect(() => editor.putContentOntoCurrentPage(withoutSchema)).toThrowError()
|
||||
})
|
||||
|
||||
it('Does not throw error if content has a schema', () => {
|
||||
expect(() => editor.putContent(clipboardContent)).not.toThrowError()
|
||||
expect(() => editor.putContentOntoCurrentPage(clipboardContent)).not.toThrowError()
|
||||
})
|
||||
|
||||
it('Throws error if any shape is invalid due to wrong type', () => {
|
||||
const withInvalidShapeType = structuredClone(clipboardContent)
|
||||
withInvalidShapeType.shapes[0].type = 'invalid'
|
||||
expect(() => editor.putContent(withInvalidShapeType)).toThrowError()
|
||||
expect(() => editor.putContentOntoCurrentPage(withInvalidShapeType)).toThrowError()
|
||||
})
|
||||
|
||||
// we temporarily disabled validations
|
||||
|
@ -34,6 +34,6 @@ describe('Migrations', () => {
|
|||
const withInvalidShapeModel = structuredClone(clipboardContent)
|
||||
// @ts-expect-error
|
||||
withInvalidShapeModel.shapes[0].x = 'invalid'
|
||||
expect(() => editor.putContent(withInvalidShapeModel)).toThrowError()
|
||||
expect(() => editor.putContentOntoCurrentPage(withInvalidShapeModel)).toThrowError()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -869,7 +869,7 @@ describe('When undoing and redoing...', () => {
|
|||
ids['G']
|
||||
)
|
||||
|
||||
editor.mark()
|
||||
editor.mark('before sending to back')
|
||||
editor.sendBackward([ids['F'], ids['G']])
|
||||
|
||||
expectShapesInOrder(
|
||||
|
|
|
@ -12,8 +12,12 @@ describe('setCurrentPage', () => {
|
|||
const page1Id = editor.pages[0].id
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
expect(editor.currentPageId).toBe(page1Id)
|
||||
|
||||
editor.setCurrentPage(page2Id)
|
||||
expect(editor.currentPageId).toEqual(page2Id)
|
||||
|
||||
expect(editor.currentPage).toEqual(editor.pages[1])
|
||||
|
||||
editor.setCurrentPage(page1Id)
|
||||
|
@ -21,7 +25,9 @@ describe('setCurrentPage', () => {
|
|||
expect(editor.currentPage).toEqual(editor.pages[0])
|
||||
|
||||
const page3Id = PageRecordType.createId('page3')
|
||||
editor.createPage('New Page 3', page3Id)
|
||||
editor.createPage({ name: 'New Page 3', id: page3Id })
|
||||
expect(editor.currentPageId).toBe(page1Id)
|
||||
editor.setCurrentPage(page3Id)
|
||||
|
||||
expect(editor.currentPageId).toEqual(page3Id)
|
||||
expect(editor.currentPage).toEqual(editor.pages[2])
|
||||
|
@ -41,7 +47,7 @@ describe('setCurrentPage', () => {
|
|||
|
||||
it('squashes', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.createPage({ name: 'New Page 2', index: page2Id })
|
||||
|
||||
editor.history.clear()
|
||||
editor.setCurrentPage(editor.pages[1].id)
|
||||
|
@ -53,7 +59,7 @@ describe('setCurrentPage', () => {
|
|||
it('preserves the undo stack', () => {
|
||||
const boxId = createShapeId('geo')
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
editor.createPage({ name: 'New Page 2', id: page2Id })
|
||||
|
||||
editor.history.clear()
|
||||
editor.createShapes([{ type: 'geo', id: boxId, props: { w: 100, h: 100 } }])
|
||||
|
@ -68,8 +74,8 @@ describe('setCurrentPage', () => {
|
|||
})
|
||||
|
||||
it('logs an error when trying to navigate to a page that does not exist', () => {
|
||||
const page2Id = PageRecordType.createId('page2')
|
||||
editor.createPage('New Page 2', page2Id)
|
||||
const initialPageId = editor.currentPageId
|
||||
expect(editor.currentPageId).toBe(initialPageId)
|
||||
console.error = jest.fn()
|
||||
|
||||
expect(() => {
|
||||
|
@ -77,6 +83,6 @@ describe('setCurrentPage', () => {
|
|||
}).not.toThrow()
|
||||
|
||||
expect(console.error).toHaveBeenCalled()
|
||||
expect(editor.currentPageId).toEqual(page2Id)
|
||||
expect(editor.currentPageId).toBe(initialPageId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('shapeIdsInCurrentPage', () => {
|
|||
{ type: 'geo', id: ids.box3 },
|
||||
])
|
||||
const id = PageRecordType.createId('page2')
|
||||
editor.createPage('New Page 2', id)
|
||||
editor.createPage({ name: 'New Page 2', id })
|
||||
editor.setCurrentPage(id)
|
||||
editor.createShapes([
|
||||
{ type: 'geo', id: ids.box4 },
|
||||
|
|
|
@ -1731,3 +1731,55 @@ describe('When dragging a shape onto a parent', () => {
|
|||
expect(editor.getShape(ids.box1)?.parentId).toBe(editor.currentPageId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When dragging shapes', () => {
|
||||
it('should drag and undo and redo', () => {
|
||||
editor.deleteShapes(editor.currentPageShapes)
|
||||
|
||||
editor.setCurrentTool('arrow').pointerMove(0, 0).pointerDown().pointerMove(100, 100).pointerUp()
|
||||
|
||||
editor.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[0]!.id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
editor.setCurrentTool('geo').pointerMove(-10, 100).pointerDown().pointerUp()
|
||||
|
||||
editor.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[1]!.id,
|
||||
x: -110,
|
||||
y: 0,
|
||||
})
|
||||
|
||||
editor
|
||||
.selectAll()
|
||||
.pointerMove(50, 50)
|
||||
.pointerDown()
|
||||
.pointerMove(100, 50)
|
||||
.pointerUp()
|
||||
.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[0]!.id,
|
||||
x: 50, // 50 to the right
|
||||
y: 0,
|
||||
})
|
||||
.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[1]!.id,
|
||||
x: -60, // 50 to the right
|
||||
y: 0,
|
||||
})
|
||||
|
||||
editor
|
||||
.undo()
|
||||
.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[0]!.id,
|
||||
x: 0, // 50 to the right
|
||||
y: 0,
|
||||
})
|
||||
.expectShapeToMatch({
|
||||
id: editor.currentPageShapes[1]!.id,
|
||||
x: -110, // 50 to the right
|
||||
y: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
18
packages/tldraw/src/test/viewport-following.test.ts
Normal file
18
packages/tldraw/src/test/viewport-following.test.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { TestEditor } from './TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
editor
|
||||
})
|
||||
|
||||
describe('When following a user', () => {
|
||||
it.todo('starts following a user')
|
||||
it.todo('stops following a user')
|
||||
it.todo('stops following a user when the camera changes due to user action')
|
||||
it.todo('moves the camera to follow the user without unfollowing them')
|
||||
it.todo('stops any animations while following')
|
||||
it.todo('stops following a user when the page changes due to user action')
|
||||
it.todo('follows a user to another page without unfollowing them')
|
||||
})
|
Loading…
Reference in a new issue