refac: cleaning round for image cropping/resizing

This commit is contained in:
fsylvain committed 2026-09-28 07:56:13 +02:00
1 parent e15f444603
commit 0fc00ceeff
3 files changed
+211 -38

No files matched your search

+40 -38
View File
@@ -357,16 +357,27 @@ function pointFromEvent(event: MouseEvent | TouchEvent): { x: number; y: number
return { x: mouseEvent.clientX, y: mouseEvent.clientY }
}
function bindPointerDrag(onMove: (event: MouseEvent | TouchEvent) => void, onEnd: () => void) {
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onEnd)
window.addEventListener('touchmove', onMove)
window.addEventListener('touchend', onEnd)
}
function unbindPointerDrag(onMove: (event: MouseEvent | TouchEvent) => void, onEnd: () => void) {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onEnd)
window.removeEventListener('touchmove', onMove)
window.removeEventListener('touchend', onEnd)
}
let moveOrigin: { x: number; y: number; rect: CropRect } | null = null
function onFrameDragStart(event: MouseEvent | TouchEvent) {
event.preventDefault()
const point = pointFromEvent(event)
moveOrigin = { x: point.x, y: point.y, rect: { ...rect.value } }
window.addEventListener('mousemove', onFrameDragMove)
window.addEventListener('mouseup', onFrameDragEnd)
window.addEventListener('touchmove', onFrameDragMove)
window.addEventListener('touchend', onFrameDragEnd)
bindPointerDrag(onFrameDragMove, onFrameDragEnd)
}
function onFrameDragMove(event: MouseEvent | TouchEvent) {
@@ -386,10 +397,7 @@ function onFrameDragMove(event: MouseEvent | TouchEvent) {
function onFrameDragEnd() {
moveOrigin = null
window.removeEventListener('mousemove', onFrameDragMove)
window.removeEventListener('mouseup', onFrameDragEnd)
window.removeEventListener('touchmove', onFrameDragMove)
window.removeEventListener('touchend', onFrameDragEnd)
unbindPointerDrag(onFrameDragMove, onFrameDragEnd)
}
let resizeOrigin: {
@@ -411,10 +419,7 @@ function onHandleDragStart(event: MouseEvent | TouchEvent, corner: Corner) {
corner,
ratio: lockedRatioValue.value,
}
window.addEventListener('mousemove', onHandleDragMove)
window.addEventListener('mouseup', onHandleDragEnd)
window.addEventListener('touchmove', onHandleDragMove)
window.addEventListener('touchend', onHandleDragEnd)
bindPointerDrag(onHandleDragMove, onHandleDragEnd)
}
function onHandleDragMove(event: MouseEvent | TouchEvent) {
@@ -450,36 +455,33 @@ function onHandleDragMove(event: MouseEvent | TouchEvent) {
function onHandleDragEnd() {
resizeOrigin = null
window.removeEventListener('mousemove', onHandleDragMove)
window.removeEventListener('mouseup', onHandleDragEnd)
window.removeEventListener('touchmove', onHandleDragMove)
window.removeEventListener('touchend', onHandleDragEnd)
unbindPointerDrag(onHandleDragMove, onHandleDragEnd)
}
const ARROW_KEY_DELTAS: Record<string, [number, number]> = {
ArrowRight: [1, 0],
ArrowLeft: [-1, 0],
ArrowUp: [0, -1],
ArrowDown: [0, 1],
}
function onFrameKeydown(event: KeyboardEvent) {
const step = 1
const lockedRatio = lockedRatioValue.value
if (event.key === 'ArrowRight') {
event.preventDefault()
rect.value = event.shiftKey
? resizeRect(rect.value, step, 0, boundsWidth.value, boundsHeight.value, lockedRatio)
: moveRect(rect.value, step, 0, boundsWidth.value, boundsHeight.value)
} else if (event.key === 'ArrowLeft') {
event.preventDefault()
rect.value = event.shiftKey
? resizeRect(rect.value, -step, 0, boundsWidth.value, boundsHeight.value, lockedRatio)
: moveRect(rect.value, -step, 0, boundsWidth.value, boundsHeight.value)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
rect.value = event.shiftKey
? resizeRect(rect.value, 0, -step, boundsWidth.value, boundsHeight.value, lockedRatio)
: moveRect(rect.value, 0, -step, boundsWidth.value, boundsHeight.value)
} else if (event.key === 'ArrowDown') {
event.preventDefault()
rect.value = event.shiftKey
? resizeRect(rect.value, 0, step, boundsWidth.value, boundsHeight.value, lockedRatio)
: moveRect(rect.value, 0, step, boundsWidth.value, boundsHeight.value)
const delta = ARROW_KEY_DELTAS[event.key]
if (!delta) {
return
}
event.preventDefault()
const [dx, dy] = delta
rect.value = event.shiftKey
? resizeRect(
rect.value,
dx,
dy,
boundsWidth.value,
boundsHeight.value,
lockedRatioValue.value,
)
: moveRect(rect.value, dx, dy, boundsWidth.value, boundsHeight.value)
}
async function confirmCropUpload() {
@@ -340,6 +340,56 @@ describe('Arrow keys move the crop frame and Shift+arrow keys resize it', () =>
})
})
describe('Dragging the crop frame moves it, and dragging a corner handle resizes it', () => {
it('clamps the dragged rect inside the image bounds', async () => {
const wrapper = mountUploader()
await openModal(wrapper)
await chooseFile(north_wall())
await selectRatioPill('1:1')
const uploader = wrapper.findComponent(ImageUploader).vm as unknown as {
cropRect: CropRect
}
const before = { ...uploader.cropRect }
cropFrame().dispatchEvent(
new MouseEvent('mousedown', { clientX: 0, clientY: 0, bubbles: true }),
)
window.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 40 }))
window.dispatchEvent(new MouseEvent('mouseup'))
await nextTick()
expect(uploader.cropRect.x).toBe(before.x + 100)
expect(uploader.cropRect.y).toBe(before.y)
expect(uploader.cropRect.width).toBe(before.width)
expect(uploader.cropRect.height).toBe(before.height)
window.dispatchEvent(new MouseEvent('mousemove', { clientX: 999, clientY: 0 }))
await nextTick()
expect(uploader.cropRect.x).toBe(before.x + 100)
const handle = document.body.querySelectorAll<HTMLElement>(
'[data-testid="crop-handle"]',
)[3]!
handle.dispatchEvent(new MouseEvent('mousedown', { clientX: 0, clientY: 0, bubbles: true }))
const afterMove = { ...uploader.cropRect }
window.dispatchEvent(new MouseEvent('mousemove', { clientX: -50, clientY: -50 }))
window.dispatchEvent(new MouseEvent('mouseup'))
await nextTick()
expect(uploader.cropRect.width).toBe(afterMove.width - 50)
expect(uploader.cropRect.height).toBe(afterMove.height - 50)
expect(uploader.cropRect.x).toBe(afterMove.x)
expect(uploader.cropRect.y).toBe(afterMove.y)
window.dispatchEvent(new MouseEvent('mousemove', { clientX: -9999, clientY: 0 }))
await nextTick()
expect(uploader.cropRect.width).toBe(afterMove.width - 50)
wrapper.unmount()
})
})
describe('Rotate cycles the image in 90° steps and Reset returns to the opened defaults', () => {
it('rotates then resets', async () => {
const wrapper = mountUploader()