Cropping undo/redo UX (#3891)
This PR aims to improve the UX around undo/redo and cropping. Before the PR if you do some cropping, then stop cropping, then hit `undo`, you will end up back in the cropping state and it will undo each of your resize/translate cropping operations individually. This is weird 🙅🏼 It should just undo the whole sequence of changes that happened during cropping. To achieve that, this PR introduces a new history method called `squashToMark`, which strips out all the marks between the current head of the undo stack and the mark id you pass in. This PR also makes the default history record mode of `updateCurrentPageState` to `ignore` like it already was for `updateInstanceState`. The fact that it was recording changes to the `croppingShapeId` was the reason that hitting undo would put you back into the cropping state. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here.
This commit is contained in:
parent
23f8b3fd60
commit
25dcc29803
14 changed files with 201 additions and 116 deletions
|
@ -1535,7 +1535,9 @@ export class HistoryManager<R extends UnknownRecord> {
|
|||
// (undocumented)
|
||||
onBatchComplete: () => void;
|
||||
// (undocumented)
|
||||
redo: () => this | undefined;
|
||||
redo: () => this;
|
||||
// (undocumented)
|
||||
squashToMark: (id: string) => this;
|
||||
// (undocumented)
|
||||
undo: () => this;
|
||||
}
|
||||
|
|
|
@ -892,6 +892,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
undo(): this {
|
||||
this._flushEventsForTick(0)
|
||||
this.complete()
|
||||
this.history.undo()
|
||||
return this
|
||||
}
|
||||
|
@ -917,6 +918,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*/
|
||||
redo(): this {
|
||||
this._flushEventsForTick(0)
|
||||
this.complete()
|
||||
this.history.redo()
|
||||
return this
|
||||
}
|
||||
|
@ -1440,12 +1442,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
partial: Partial<Omit<TLInstancePageState, 'selectedShapeIds'>>,
|
||||
historyOptions?: TLHistoryBatchOptions
|
||||
) => {
|
||||
this.batch(() => {
|
||||
this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({
|
||||
...state,
|
||||
...partial,
|
||||
}))
|
||||
}, historyOptions)
|
||||
this.batch(
|
||||
() => {
|
||||
this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({
|
||||
...state,
|
||||
...partial,
|
||||
}))
|
||||
},
|
||||
{
|
||||
history: 'ignore',
|
||||
...historyOptions,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -430,4 +430,45 @@ describe('history options', () => {
|
|||
manager.redo()
|
||||
expect(getState()).toMatchObject({ a: 3, b: 2 })
|
||||
})
|
||||
|
||||
it('squashToMark works', () => {
|
||||
const a = manager.mark()
|
||||
setA(1)
|
||||
const b = manager.mark()
|
||||
setB(1)
|
||||
setB(2)
|
||||
setB(3)
|
||||
manager.mark()
|
||||
setA(2)
|
||||
setB(4)
|
||||
manager.mark()
|
||||
setB(5)
|
||||
setB(6)
|
||||
|
||||
expect(getState()).toMatchObject({ a: 2, b: 6 })
|
||||
|
||||
manager.squashToMark(b)
|
||||
|
||||
// does not affect state
|
||||
expect(getState()).toMatchObject({ a: 2, b: 6 })
|
||||
|
||||
// but now undoing should take us back to a
|
||||
manager.undo()
|
||||
expect(getState()).toMatchObject({ a: 1, b: 0 })
|
||||
|
||||
// and redoing should take us back to the end
|
||||
manager.redo()
|
||||
expect(getState()).toMatchObject({ a: 2, b: 6 })
|
||||
|
||||
// and we can get back to the start with two undos
|
||||
manager.undo().undo()
|
||||
expect(getState()).toMatchObject({ a: 0, b: 0 })
|
||||
|
||||
manager.redo().redo()
|
||||
manager.squashToMark(a)
|
||||
|
||||
expect(getState()).toMatchObject({ a: 2, b: 6 })
|
||||
manager.undo()
|
||||
expect(getState()).toMatchObject({ a: 0, b: 0 })
|
||||
})
|
||||
})
|
||||
|
|
|
@ -206,7 +206,7 @@ export class HistoryManager<R extends UnknownRecord> {
|
|||
|
||||
let { undos, redos } = this.stacks.get()
|
||||
if (redos.length === 0) {
|
||||
return
|
||||
return this
|
||||
}
|
||||
|
||||
// ignore any intermediate marks - this should take us to the first `diff` entry
|
||||
|
@ -252,6 +252,41 @@ export class HistoryManager<R extends UnknownRecord> {
|
|||
return this
|
||||
}
|
||||
|
||||
squashToMark = (id: string) => {
|
||||
// remove marks between head and the mark
|
||||
|
||||
let top = this.stacks.get().undos
|
||||
const popped: Array<RecordsDiff<R>> = []
|
||||
|
||||
while (top.head && !(top.head.type === 'stop' && top.head.id === id)) {
|
||||
if (top.head.type === 'diff') {
|
||||
popped.push(top.head.diff)
|
||||
}
|
||||
top = top.tail
|
||||
}
|
||||
|
||||
if (!top.head || top.head?.id !== id) {
|
||||
console.error('Could not find mark to squash to: ', id)
|
||||
return this
|
||||
}
|
||||
if (popped.length === 0) {
|
||||
return this
|
||||
}
|
||||
|
||||
const diff = createEmptyRecordsDiff<R>()
|
||||
squashRecordDiffsMutable(diff, popped.reverse())
|
||||
|
||||
this.stacks.update(({ redos }) => ({
|
||||
undos: top.push({
|
||||
type: 'diff',
|
||||
diff,
|
||||
}),
|
||||
redos,
|
||||
}))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
mark = (id = uniqueId()) => {
|
||||
transact(() => {
|
||||
this.flushPendingDiff()
|
||||
|
|
|
@ -90,8 +90,8 @@ export const TldrawSelectionForeground = track(function TldrawSelectionForegroun
|
|||
'select.pointing_shape',
|
||||
'select.crop.idle',
|
||||
'select.crop.pointing_crop',
|
||||
'select.pointing_resize_handle',
|
||||
'select.pointing_crop_handle'
|
||||
'select.crop.pointing_crop_handle',
|
||||
'select.pointing_resize_handle'
|
||||
)) ||
|
||||
(showSelectionBounds &&
|
||||
editor.isIn('select.resizing') &&
|
||||
|
@ -106,9 +106,9 @@ export const TldrawSelectionForeground = track(function TldrawSelectionForegroun
|
|||
|
||||
const showCropHandles =
|
||||
editor.isInAny(
|
||||
'select.pointing_crop_handle',
|
||||
'select.crop.idle',
|
||||
'select.crop.pointing_crop'
|
||||
'select.crop.pointing_crop',
|
||||
'select.crop.pointing_crop_handle'
|
||||
) &&
|
||||
!isChangingStyle &&
|
||||
!isReadonlyMode
|
||||
|
|
|
@ -4,11 +4,7 @@ export function registerDefaultSideEffects(editor: Editor) {
|
|||
return [
|
||||
editor.sideEffects.registerAfterChangeHandler('instance_page_state', (prev, next) => {
|
||||
if (prev.croppingShapeId !== next.croppingShapeId) {
|
||||
const isInCroppingState = editor.isInAny(
|
||||
'select.crop',
|
||||
'select.pointing_crop_handle',
|
||||
'select.cropping'
|
||||
)
|
||||
const isInCroppingState = editor.isIn('select.crop')
|
||||
if (!prev.croppingShapeId && next.croppingShapeId) {
|
||||
if (!isInCroppingState) {
|
||||
editor.setCurrentTool('select.crop.idle')
|
||||
|
|
|
@ -98,10 +98,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
throw Error("Bookmark assets can't be rendered as images")
|
||||
}
|
||||
|
||||
const showCropPreview =
|
||||
isSelected &&
|
||||
isCropping &&
|
||||
this.editor.isInAny('select.crop', 'select.cropping', 'select.pointing_crop_handle')
|
||||
const showCropPreview = isSelected && isCropping && this.editor.isIn('select.crop')
|
||||
|
||||
// We only want to reduce motion for mimeTypes that have motion
|
||||
const reduceMotion =
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { StateNode, TLStateNodeConstructor, react } from '@tldraw/editor'
|
||||
import { Brushing } from './childStates/Brushing'
|
||||
import { Crop } from './childStates/Crop/Crop'
|
||||
import { Cropping } from './childStates/Cropping'
|
||||
import { Cropping } from './childStates/Crop/children/Cropping'
|
||||
import { PointingCropHandle } from './childStates/Crop/children/PointingCropHandle'
|
||||
import { DraggingHandle } from './childStates/DraggingHandle'
|
||||
import { EditingShape } from './childStates/EditingShape'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { PointingArrowLabel } from './childStates/PointingArrowLabel'
|
||||
import { PointingCanvas } from './childStates/PointingCanvas'
|
||||
import { PointingCropHandle } from './childStates/PointingCropHandle'
|
||||
import { PointingHandle } from './childStates/PointingHandle'
|
||||
import { PointingResizeHandle } from './childStates/PointingResizeHandle'
|
||||
import { PointingRotateHandle } from './childStates/PointingRotateHandle'
|
||||
|
|
|
@ -1,10 +1,35 @@
|
|||
import { StateNode } from '@tldraw/editor'
|
||||
import { Cropping } from './children/Cropping'
|
||||
import { Idle } from './children/Idle'
|
||||
import { PointingCrop } from './children/PointingCrop'
|
||||
import { PointingCropHandle } from './children/PointingCropHandle'
|
||||
import { TranslatingCrop } from './children/TranslatingCrop'
|
||||
|
||||
export class Crop extends StateNode {
|
||||
static override id = 'crop'
|
||||
static override initial = 'idle'
|
||||
static override children = () => [Idle, TranslatingCrop, PointingCrop]
|
||||
static override children = () => [
|
||||
Idle,
|
||||
TranslatingCrop,
|
||||
PointingCrop,
|
||||
PointingCropHandle,
|
||||
Cropping,
|
||||
]
|
||||
|
||||
markId = ''
|
||||
override onEnter = () => {
|
||||
this.didCancel = false
|
||||
this.markId = this.editor.history.mark()
|
||||
}
|
||||
didCancel = false
|
||||
override onExit = () => {
|
||||
if (this.didCancel) {
|
||||
this.editor.bailToMark(this.markId)
|
||||
} else {
|
||||
this.editor.history.squashToMark(this.markId)
|
||||
}
|
||||
}
|
||||
override onCancel = () => {
|
||||
this.didCancel = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,9 @@ import {
|
|||
Vec,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
import { kickoutOccludedShapes } from '../../../selectHelpers'
|
||||
import { CursorTypeMap } from '../../PointingResizeHandle'
|
||||
import { MIN_CROP_SIZE } from '../crop-constants'
|
||||
|
||||
type Snapshot = ReturnType<Cropping['createSnapshot']>
|
||||
|
||||
|
@ -207,7 +207,7 @@ export class Cropping extends StateNode {
|
|||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingShape(null)
|
||||
this.parent.transition('idle')
|
||||
this.editor.setCurrentTool('select.idle')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,7 +217,7 @@ export class Cropping extends StateNode {
|
|||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingShape(null)
|
||||
this.parent.transition('idle')
|
||||
this.editor.setCurrentTool('select.idle')
|
||||
}
|
||||
}
|
||||
|
|
@ -10,21 +10,13 @@ export class Idle extends StateNode {
|
|||
|
||||
const onlySelectedShape = this.editor.getOnlySelectedShape()
|
||||
|
||||
// well this fucking sucks. what the fuck.
|
||||
// it's possible for a user to enter cropping, then undo
|
||||
// (which clears the cropping id) but still remain in this state.
|
||||
this.editor.on('tick', this.cleanupCroppingState)
|
||||
|
||||
if (onlySelectedShape) {
|
||||
this.editor.mark('crop')
|
||||
this.editor.setCroppingShape(onlySelectedShape.id)
|
||||
}
|
||||
}
|
||||
|
||||
override onExit: TLExitEventHandler = () => {
|
||||
this.editor.setCursor({ type: 'default', rotation: 0 })
|
||||
|
||||
this.editor.off('tick', this.cleanupCroppingState)
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
|
@ -85,27 +77,21 @@ export class Idle extends StateNode {
|
|||
case 'bottom_right_rotate': {
|
||||
this.editor.setCurrentTool('select.pointing_rotate_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
onInteractionEnd: 'select.crop.idle',
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'top':
|
||||
case 'right':
|
||||
case 'bottom':
|
||||
case 'left': {
|
||||
this.editor.setCurrentTool('select.pointing_crop_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'left':
|
||||
case 'top_left':
|
||||
case 'top_right':
|
||||
case 'bottom_left':
|
||||
case 'bottom_right': {
|
||||
this.editor.setCurrentTool('select.pointing_crop_handle', {
|
||||
this.editor.setCurrentTool('select.crop.pointing_crop_handle', {
|
||||
...info,
|
||||
onInteractionEnd: 'select.crop',
|
||||
onInteractionEnd: 'select.crop.idle',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
@ -165,12 +151,6 @@ export class Idle extends StateNode {
|
|||
this.editor.setCurrentTool('select.idle', {})
|
||||
}
|
||||
|
||||
private cleanupCroppingState = () => {
|
||||
if (!this.editor.getCroppingShapeId()) {
|
||||
this.editor.setCurrentTool('select.idle', {})
|
||||
}
|
||||
}
|
||||
|
||||
private nudgeCroppingImage(ephemeral = false) {
|
||||
const {
|
||||
editor: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { StateNode, TLEventHandlers, TLPointerEventInfo } from '@tldraw/editor'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
import { CursorTypeMap } from '../../PointingResizeHandle'
|
||||
|
||||
type TLPointingCropHandleInfo = TLPointerEventInfo & {
|
||||
target: 'selection'
|
||||
|
@ -51,7 +51,7 @@ export class PointingCropHandle extends StateNode {
|
|||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingShape(null)
|
||||
this.parent.transition('idle')
|
||||
this.editor.setCurrentTool('select.idle')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ export class PointingCropHandle extends StateNode {
|
|||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
this.editor.setCroppingShape(null)
|
||||
this.parent.transition('idle')
|
||||
this.editor.setCurrentTool('select.idle')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -137,20 +137,13 @@ export class Idle extends StateNode {
|
|||
case 'top':
|
||||
case 'right':
|
||||
case 'bottom':
|
||||
case 'left': {
|
||||
if (shouldEnterCropMode) {
|
||||
this.parent.transition('pointing_crop_handle', info)
|
||||
} else {
|
||||
this.parent.transition('pointing_resize_handle', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'left':
|
||||
case 'top_left':
|
||||
case 'top_right':
|
||||
case 'bottom_left':
|
||||
case 'bottom_right': {
|
||||
if (shouldEnterCropMode) {
|
||||
this.parent.transition('pointing_crop_handle', info)
|
||||
this.parent.transition('crop.pointing_crop_handle', info)
|
||||
} else {
|
||||
this.parent.transition('pointing_resize_handle', info)
|
||||
}
|
||||
|
|
|
@ -33,11 +33,7 @@ beforeEach(() => {
|
|||
// this side effect is normally added via a hook
|
||||
editor.sideEffects.registerAfterChangeHandler('instance_page_state', (prev, next) => {
|
||||
if (prev.croppingShapeId !== next.croppingShapeId) {
|
||||
const isInCroppingState = editor.isInAny(
|
||||
'select.crop',
|
||||
'select.pointing_crop_handle',
|
||||
'select.cropping'
|
||||
)
|
||||
const isInCroppingState = editor.isIn('select.crop')
|
||||
if (!prev.croppingShapeId && next.croppingShapeId) {
|
||||
if (!isInCroppingState) {
|
||||
editor.setCurrentTool('select.crop.idle')
|
||||
|
@ -100,12 +96,20 @@ describe('When in the select.idle state', () => {
|
|||
expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB])
|
||||
expect(editor.getCroppingShapeId()).toBe(ids.imageB)
|
||||
|
||||
editor.updateShape({
|
||||
id: ids.imageB,
|
||||
type: 'image',
|
||||
props: {
|
||||
crop: { topLeft: { x: 0.1, y: 0.1 }, bottomRight: { x: 0.9, y: 0.9 } },
|
||||
},
|
||||
})
|
||||
|
||||
editor.undo()
|
||||
|
||||
// first selection should have been a mark
|
||||
editor.expectToBeIn('select.idle')
|
||||
// back to the start of the crop (undo the crop)
|
||||
editor.expectToBeIn('select.crop.idle')
|
||||
expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB])
|
||||
expect(editor.getCroppingShapeId()).toBe(null)
|
||||
expect(editor.getCroppingShapeId()).toBe(ids.imageB)
|
||||
|
||||
editor.undo()
|
||||
|
||||
|
@ -118,9 +122,13 @@ describe('When in the select.idle state', () => {
|
|||
.redo() // select again
|
||||
.redo() // crop again
|
||||
|
||||
editor.expectToBeIn('select.crop.idle')
|
||||
// does not start copping again, but will redo the crop operation
|
||||
editor.expectToBeIn('select.idle')
|
||||
expect(editor.getSelectedShapeIds()).toMatchObject([ids.imageB])
|
||||
expect(editor.getCroppingShapeId()).toBe(ids.imageB)
|
||||
expect(editor.getCroppingShapeId()).toBe(null)
|
||||
expect(editor.getOnlySelectedShape()!.props).toMatchObject({
|
||||
crop: { topLeft: { x: 0.1, y: 0.1 }, bottomRight: { x: 0.9, y: 0.9 } },
|
||||
})
|
||||
})
|
||||
|
||||
it('when ONLY ONE image is selected double clicking a selection handle should transition to select.crop', () => {
|
||||
|
@ -195,7 +203,7 @@ describe('When in the select.idle state', () => {
|
|||
expect(editor.getCroppingShapeId()).toBe(ids.imageB)
|
||||
})
|
||||
|
||||
it('when only an image is selected control-pointing a selection handle should transition to select.pointing_crop_handle', () => {
|
||||
it('when only an image is selected control-pointing a selection handle should transition to select.crop.pointing_crop_handle', () => {
|
||||
// two shapes / edge
|
||||
editor
|
||||
.cancel()
|
||||
|
@ -218,7 +226,7 @@ describe('When in the select.idle state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 550, { target: 'selection', handle: 'bottom', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
|
||||
// one shape / corner
|
||||
editor
|
||||
|
@ -226,7 +234,7 @@ describe('When in the select.idle state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -266,14 +274,14 @@ describe('When in the crop.idle state', () => {
|
|||
.expectToBeIn('select.pointing_shape')
|
||||
})
|
||||
|
||||
it('pointing a selection handle should enter the select.pointing_crop_handle state', () => {
|
||||
it('pointing a selection handle should enter the select.crop.pointing_crop_handle state', () => {
|
||||
// corner
|
||||
editor
|
||||
.expectToBeIn('select.idle')
|
||||
.doubleClick(550, 550, ids.imageB)
|
||||
.expectToBeIn('select.crop.idle')
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: false })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
|
||||
//reset
|
||||
editor.cancel().cancel()
|
||||
|
@ -284,7 +292,7 @@ describe('When in the crop.idle state', () => {
|
|||
.doubleClick(550, 550, ids.imageB)
|
||||
.expectToBeIn('select.crop.idle')
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
})
|
||||
|
||||
it('pointing the cropping image should enter the select.crop.translating_crop state', () => {
|
||||
|
@ -522,16 +530,16 @@ describe('When in the select.crop.translating_crop state', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When in the select.pointing_crop_handle state', () => {
|
||||
it('moving the pointer should transition to select.cropping', () => {
|
||||
describe('When in the select.crop.pointing_crop_handle state', () => {
|
||||
it('moving the pointer should transition to select.crop.cropping', () => {
|
||||
editor
|
||||
.cancel()
|
||||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
})
|
||||
|
||||
it('when entered from select.idle, pressing escape / cancel should return to idle and clear cropping idle', () => {
|
||||
|
@ -541,7 +549,7 @@ describe('When in the select.pointing_crop_handle state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.cancel()
|
||||
.expectToBeIn('select.idle')
|
||||
|
||||
|
@ -556,7 +564,7 @@ describe('When in the select.pointing_crop_handle state', () => {
|
|||
.doubleClick(550, 550, ids.imageB)
|
||||
.expectToBeIn('select.crop.idle')
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: false })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.cancel()
|
||||
.expectToBeIn('select.crop.idle')
|
||||
|
||||
|
@ -564,7 +572,7 @@ describe('When in the select.pointing_crop_handle state', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('When in the select.cropping state', () => {
|
||||
describe('When in the select.crop.cropping state', () => {
|
||||
it('moving the pointer should adjust the crop', () => {
|
||||
const before = editor.getShape<TLImageShape>(ids.imageB)!.props.crop!
|
||||
|
||||
|
@ -573,9 +581,9 @@ describe('When in the select.cropping state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
|
||||
expect(editor.getShape<TLImageShape>(ids.imageB)!.props.crop!).not.toMatchObject(before)
|
||||
})
|
||||
|
@ -588,9 +596,9 @@ describe('When in the select.cropping state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.cancel()
|
||||
.expectToBeIn('select.idle')
|
||||
|
||||
|
@ -606,9 +614,9 @@ describe('When in the select.cropping state', () => {
|
|||
.doubleClick(550, 550, ids.imageB)
|
||||
.expectToBeIn('select.crop.idle')
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.cancel()
|
||||
.expectToBeIn('select.crop.idle')
|
||||
|
||||
|
@ -624,9 +632,9 @@ describe('When in the select.cropping state', () => {
|
|||
.doubleClick(550, 550, ids.imageB)
|
||||
.expectToBeIn('select.crop.idle')
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom', ctrlKey: false })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.pointerUp()
|
||||
.expectToBeIn('select.crop.idle')
|
||||
|
||||
|
@ -645,9 +653,9 @@ describe('When in the select.cropping state', () => {
|
|||
.expectToBeIn('select.idle')
|
||||
.select(ids.imageB)
|
||||
.pointerDown(500, 600, { target: 'selection', handle: 'bottom_left', ctrlKey: true })
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.pointerMove(510, 590)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.pointerUp()
|
||||
.expectToBeIn('select.idle')
|
||||
|
||||
|
@ -680,10 +688,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + moveX, imageY + moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX + (imageWidth - MIN_CROP_SIZE),
|
||||
|
@ -752,10 +760,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + moveX, imageY + moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX + moveX,
|
||||
|
@ -790,10 +798,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + imageWidth - moveX, imageY + moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX,
|
||||
|
@ -828,10 +836,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + moveX, imageY + imageHeight - moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX + moveX,
|
||||
|
@ -866,10 +874,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + imageWidth - moveX, imageY + imageHeight - moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX,
|
||||
|
@ -902,10 +910,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + moveX, imageY + imageHeight / 2)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX + moveX,
|
||||
|
@ -938,10 +946,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + imageWidth / 2, imageY + moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX,
|
||||
|
@ -974,10 +982,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(150, 150)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.pointerMove(imageX + imageWidth - moveX, imageY + imageHeight / 2)
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
|
@ -1011,10 +1019,10 @@ describe('When cropping...', () => {
|
|||
},
|
||||
{ ctrlKey: true }
|
||||
)
|
||||
.expectToBeIn('select.pointing_crop_handle')
|
||||
.expectToBeIn('select.crop.pointing_crop_handle')
|
||||
.expectShapeToMatch({ id: ids.imageA, x: imageX, y: imageY, props: imageProps })
|
||||
.pointerMove(imageX + imageWidth / 2, imageY + imageHeight - moveY)
|
||||
.expectToBeIn('select.cropping')
|
||||
.expectToBeIn('select.crop.cropping')
|
||||
.expectShapeToMatch({
|
||||
id: ids.imageA,
|
||||
x: imageX,
|
||||
|
|
Loading…
Reference in a new issue