-
Notifications
You must be signed in to change notification settings - Fork 3.9k
fix: detect delete areas using pointer coordinates #10078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1480,17 +1480,13 @@ export class WorkspaceSvg | |
| /** | ||
| * Returns the drag target the pointer event is over. | ||
| * | ||
| * @param e Pointer move event or a workspace coordinate. | ||
| * @param e Pointer move event. | ||
| * @returns Null if not over a drag target, or the drag target the event is | ||
| * over. | ||
| */ | ||
| getDragTarget(e: PointerEvent | Coordinate): IDragTarget | null { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here especially, and kinda throughout, I think this really should have always taken a coordinate; I'd suggest leaving it as-is. Also this is technically breaking. In general I think only the actual event listener should handle events, and any methods called by it or its callees should take specific/abstracted arguments, rather than handing events around all over the place and picking one or two properties out of them. |
||
| const coordinate = | ||
| e instanceof Coordinate | ||
| ? svgMath.wsToScreenCoordinates(this, e) | ||
| : new Coordinate(e.clientX, e.clientY); | ||
| getDragTarget(e: PointerEvent): IDragTarget | null { | ||
| for (let i = 0, targetArea; (targetArea = this.dragTargetAreas[i]); i++) { | ||
| if (targetArea.clientRect.contains(coordinate.x, coordinate.y)) { | ||
| if (targetArea.clientRect.contains(e.clientX, e.clientY)) { | ||
| return targetArea.component; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Raspberry Pi Foundation | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import {assert} from '../../node_modules/chai/index.js'; | ||
| import { | ||
| defineBasicBlockWithField, | ||
| defineStackBlock, | ||
| } from './test_helpers/block_definitions.js'; | ||
| import { | ||
| sharedTestSetup, | ||
| sharedTestTeardown, | ||
| } from './test_helpers/setup_teardown.js'; | ||
|
|
||
| suite('Dragger', function () { | ||
| /** | ||
| * @param {!Blockly.BlockSvg} block The block to measure. | ||
| * @returns {{x: number, y: number}} Viewport coordinates at the block center. | ||
| */ | ||
| function blockCenterClient(block) { | ||
| const bounds = block.getSvgRoot().getBoundingClientRect(); | ||
| return { | ||
| x: (bounds.left + bounds.right) / 2, | ||
| y: (bounds.top + bounds.bottom) / 2, | ||
|
Comment on lines
+25
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only maths if it's at (0,0) right? and same for |
||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @param {!Blockly.BlockSvg} block The block to measure. | ||
| * @returns {{x: number, y: number}} Viewport coordinates at the block origin. | ||
| */ | ||
| function blockOriginClient(block) { | ||
| const screen = Blockly.utils.svgMath.wsToScreenCoordinates( | ||
| block.workspace, | ||
| block.getRelativeToSurfaceXY(), | ||
| ); | ||
| return {x: screen.x, y: screen.y}; | ||
| } | ||
|
|
||
| /** | ||
| * @param {!Blockly.utils.Rect} rect The rectangle to measure. | ||
| * @returns {{x: number, y: number}} Viewport coordinates at the rect center. | ||
| */ | ||
| function rectCenterClient(rect) { | ||
| return { | ||
| x: (rect.left + rect.right) / 2, | ||
| y: (rect.top + rect.bottom) / 2, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @param {number} clientX The viewport x coordinate. | ||
| * @param {number} clientY The viewport y coordinate. | ||
| * @param {string=} type The pointer event type. | ||
| * @returns {!PointerEvent} A synthetic pointer event at the given location. | ||
| */ | ||
| function pointerAt(clientX, clientY, type = 'pointermove') { | ||
| return new PointerEvent(type, {clientX, clientY}); | ||
| } | ||
|
|
||
| function hasDeleteStyle(block) { | ||
| return block.getSvgRoot().classList.contains('blocklyDraggingDelete'); | ||
| } | ||
|
|
||
| /** | ||
| * Simulates pressing on the block center and dragging to a viewport point. | ||
| * | ||
| * @param {!Blockly.BlockSvg} block The block to drag. | ||
| * @param {{x: number, y: number}} pointerEnd The viewport point to drag to. | ||
| * @returns {{dragger: !Blockly.dragging.Dragger, dragEvent: !PointerEvent}} | ||
| * The dragger and final pointer event from the simulated drag. | ||
| */ | ||
| function dragBlock(block, pointerEnd) { | ||
| const start = blockCenterClient(block); | ||
| const totalDelta = new Blockly.utils.Coordinate( | ||
| pointerEnd.x - start.x, | ||
| pointerEnd.y - start.y, | ||
| ); | ||
|
|
||
| const dragger = new Blockly.dragging.Dragger(block); | ||
| const dragStartEvent = pointerAt(start.x, start.y, 'pointerdown'); | ||
| const dragEvent = pointerAt(pointerEnd.x, pointerEnd.y); | ||
|
|
||
| dragger.onDragStart(dragStartEvent); | ||
| dragger.onDrag(dragEvent, totalDelta); | ||
|
|
||
| return {dragger, dragEvent}; | ||
| } | ||
|
|
||
| setup(function () { | ||
| sharedTestSetup.call(this); | ||
| defineBasicBlockWithField(); | ||
| defineStackBlock(); | ||
| const toolbox = document.getElementById('toolbox-categories'); | ||
| this.workspace = Blockly.inject('blocklyDiv', {toolbox, trashcan: true}); | ||
| this.workspace.recordDragTargets(); | ||
| this.trashRect = this.workspace.trashcan.getClientRect(); | ||
| this.toolboxRect = this.workspace.toolbox.getClientRect(); | ||
| assert.isNotNull(this.trashRect); | ||
| assert.isNotNull(this.toolboxRect); | ||
|
|
||
| this.block = this.workspace.newBlock('stack_block'); | ||
| this.block.initSvg(); | ||
| this.block.render(); | ||
| }); | ||
|
|
||
| teardown(function () { | ||
| sharedTestTeardown.call(this); | ||
| }); | ||
|
|
||
| [ | ||
| {name: 'trashcan', rectKey: 'trashRect'}, | ||
| {name: 'toolbox', rectKey: 'toolboxRect'}, | ||
| ].forEach(({name, rectKey}) => { | ||
| test(`applies delete styling and deletes when dragged to ${name}`, function () { | ||
| const deleteRect = this[rectKey]; | ||
| const {dragger, dragEvent} = dragBlock( | ||
| this.block, | ||
| rectCenterClient(deleteRect), | ||
| ); | ||
|
|
||
| assert.isTrue( | ||
| deleteRect.contains(dragEvent.clientX, dragEvent.clientY), | ||
| `Expected cursor to be inside ${name} delete area`, | ||
| ); | ||
| assert.isTrue(hasDeleteStyle(this.block)); | ||
|
|
||
| dragger.onDragEnd(dragEvent); | ||
| assert.isTrue(this.block.isDeadOrDying()); | ||
| }); | ||
| }); | ||
|
|
||
| test('does not apply delete styling when only block origin overlaps delete area', function () { | ||
| const start = blockCenterClient(this.block); | ||
| const originBefore = blockOriginClient(this.block); | ||
| const deleteAreaRect = this.toolboxRect; | ||
| const desiredOrigin = { | ||
| x: deleteAreaRect.right - 5, | ||
| y: originBefore.y, | ||
| }; | ||
| const {dragger, dragEvent} = dragBlock(this.block, { | ||
| x: start.x + desiredOrigin.x - originBefore.x, | ||
| y: start.y + desiredOrigin.y - originBefore.y, | ||
| }); | ||
|
|
||
| const originAfter = blockOriginClient(this.block); | ||
| assert.isTrue( | ||
| deleteAreaRect.contains(originAfter.x, originAfter.y), | ||
| 'Expected block origin to overlap delete area', | ||
| ); | ||
| assert.isFalse( | ||
| deleteAreaRect.contains(dragEvent.clientX, dragEvent.clientY), | ||
| 'Expected cursor to be outside delete area', | ||
| ); | ||
| assert.isFalse(hasDeleteStyle(this.block)); | ||
|
|
||
| dragger.onDragEnd(dragEvent); | ||
| assert.isFalse(this.block.isDeadOrDying()); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this moved back here and made a
let? It seems like it's still only assigned in one place and read right after that down below?