diff --git a/core/blockly.ts b/core/blockly.ts index dfcc6a1cb1f..09db3c12cad 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -23,7 +23,6 @@ import {BlocklyOptions} from './blockly_options.js'; import {Blocks} from './blocks.js'; import * as browserEvents from './browser_events.js'; import * as bubbles from './bubbles.js'; -import {BubbleDragger} from './bubble_dragger.js'; import * as bumpObjects from './bump_objects.js'; import * as clipboard from './clipboard.js'; import * as common from './common.js'; @@ -456,7 +455,6 @@ export {BlockDragger}; export {BlockSvg}; export {Blocks}; export {bubbles}; -export {BubbleDragger}; export {CollapsibleToolboxCategory}; export {ComponentManager}; export {Connection}; diff --git a/core/bubble_dragger.ts b/core/bubble_dragger.ts deleted file mode 100644 index d3820e4c5cc..00000000000 --- a/core/bubble_dragger.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Methods for dragging a bubble visually. - * - * @class - */ -// Former goog.module ID: Blockly.BubbleDragger - -import {ComponentManager} from './component_manager.js'; -import type {CommentMove} from './events/events_comment_move.js'; -import * as eventUtils from './events/utils.js'; -import type {IBubble} from './interfaces/i_bubble.js'; -import type {IDeleteArea} from './interfaces/i_delete_area.js'; -import type {IDragTarget} from './interfaces/i_drag_target.js'; -import {Coordinate} from './utils/coordinate.js'; -import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; -import * as layers from './layers.js'; - -/** - * Class for a bubble dragger. It moves things on the bubble canvas around the - * workspace when they are being dragged by a mouse or touch. These can be - * block comments, mutators, warnings, or workspace comments. - */ -export class BubbleDragger { - /** Which drag target the mouse pointer is over, if any. */ - private dragTarget_: IDragTarget | null = null; - - /** Whether the bubble would be deleted if dropped immediately. */ - private wouldDeleteBubble_ = false; - private readonly startXY_: Coordinate; - - /** - * @param bubble The item on the bubble canvas to drag. - * @param workspace The workspace to drag on. - */ - constructor( - private bubble: IBubble, - private workspace: WorkspaceSvg, - ) { - /** - * The location of the top left corner of the dragging bubble's body at the - * beginning of the drag, in workspace coordinates. - */ - this.startXY_ = this.bubble.getRelativeToSurfaceXY(); - } - - /** - * Start dragging a bubble. - * - * @internal - */ - startBubbleDrag() { - if (!eventUtils.getGroup()) { - eventUtils.setGroup(true); - } - - this.workspace.setResizesEnabled(false); - if ((this.bubble as AnyDuringMigration).setAutoLayout) { - (this.bubble as AnyDuringMigration).setAutoLayout(false); - } - - this.workspace.getLayerManager()?.moveToDragLayer(this.bubble); - - this.bubble.setDragging && this.bubble.setDragging(true); - } - - /** - * Execute a step of bubble dragging, based on the given event. Update the - * display accordingly. - * - * @param e The most recent move event. - * @param currentDragDeltaXY How far the pointer has moved from the position - * at the start of the drag, in pixel units. - * @internal - */ - dragBubble(e: PointerEvent, currentDragDeltaXY: Coordinate) { - const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); - const newLoc = Coordinate.sum(this.startXY_, delta); - this.bubble.moveDuringDrag(newLoc); - - const oldDragTarget = this.dragTarget_; - this.dragTarget_ = this.workspace.getDragTarget(e); - - const oldWouldDeleteBubble = this.wouldDeleteBubble_; - this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_); - if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) { - // Prevent unnecessary add/remove class calls. - this.updateCursorDuringBubbleDrag_(); - } - // Call drag enter/exit/over after wouldDeleteBlock is called in - // shouldDelete_ - if (this.dragTarget_ !== oldDragTarget) { - oldDragTarget && oldDragTarget.onDragExit(this.bubble); - this.dragTarget_ && this.dragTarget_.onDragEnter(this.bubble); - } - this.dragTarget_ && this.dragTarget_.onDragOver(this.bubble); - } - - /** - * Whether ending the drag would delete the bubble. - * - * @param dragTarget The drag target that the bubblee is currently over. - * @returns Whether dropping the bubble immediately would delete the block. - */ - private shouldDelete_(dragTarget: IDragTarget | null): boolean { - if (dragTarget) { - const componentManager = this.workspace.getComponentManager(); - const isDeleteArea = componentManager.hasCapability( - dragTarget.id, - ComponentManager.Capability.DELETE_AREA, - ); - if (isDeleteArea) { - return (dragTarget as IDeleteArea).wouldDelete(this.bubble); - } - } - return false; - } - - /** - * Update the cursor (and possibly the trash can lid) to reflect whether the - * dragging bubble would be deleted if released immediately. - */ - private updateCursorDuringBubbleDrag_() { - this.bubble.setDeleteStyle(this.wouldDeleteBubble_); - } - - /** - * Finish a bubble drag and put the bubble back on the workspace. - * - * @param e The pointerup event. - * @param currentDragDeltaXY How far the pointer has moved from the position - * at the start of the drag, in pixel units. - * @internal - */ - endBubbleDrag(e: PointerEvent, currentDragDeltaXY: Coordinate) { - // Make sure internal state is fresh. - this.dragBubble(e, currentDragDeltaXY); - - const preventMove = - this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble); - let newLoc; - if (preventMove) { - newLoc = this.startXY_; - } else { - const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); - newLoc = Coordinate.sum(this.startXY_, delta); - } - // Move the bubble to its final location. - this.bubble.moveTo(newLoc.x, newLoc.y); - - if (this.dragTarget_) { - this.dragTarget_.onDrop(this.bubble); - } - - if (this.wouldDeleteBubble_) { - // Fire a move event, so we know where to go back to for an undo. - this.fireMoveEvent_(); - this.bubble.dispose(); - } else { - // Put everything back onto the bubble canvas. - if (this.bubble.setDragging) { - this.bubble.setDragging(false); - this.workspace - .getLayerManager() - ?.moveOffDragLayer(this.bubble, layers.BUBBLE); - } - this.fireMoveEvent_(); - } - this.workspace.setResizesEnabled(true); - - eventUtils.setGroup(false); - } - - /** Fire a move event at the end of a bubble drag. */ - private fireMoveEvent_() { - if (this.bubble instanceof WorkspaceCommentSvg) { - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( - this.bubble, - ) as CommentMove; - event.setOldCoordinate(this.startXY_); - event.recordNew(); - eventUtils.fire(event); - } - // TODO (fenichel): move events for comments. - return; - } - - /** - * Convert a coordinate object from pixels to workspace units, including a - * correction for mutator workspaces. - * This function does not consider differing origins. It simply scales the - * input's x and y values. - * - * @param pixelCoord A coordinate with x and y values in CSS pixel units. - * @returns The input coordinate divided by the workspace scale. - */ - private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate { - const result = new Coordinate( - pixelCoord.x / this.workspace.scale, - pixelCoord.y / this.workspace.scale, - ); - if (this.workspace.isMutator) { - // If we're in a mutator, its scale is always 1, purely because of some - // oddities in our rendering optimizations. The actual scale is the same - // as the scale on the parent workspace. Fix that for dragging. - const mainScale = this.workspace.options.parentWorkspace!.scale; - result.scale(1 / mainScale); - } - return result; - } -} diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts index 51b71d4ec6b..28f12c4fa21 100644 --- a/core/bubbles/bubble.ts +++ b/core/bubbles/bubble.ts @@ -5,6 +5,7 @@ */ import * as browserEvents from '../browser_events.js'; +import {BubbleDragStrategy} from '../dragging/bubble_drag_strategy.js'; import {IBubble} from '../interfaces/i_bubble.js'; import {ContainerRegion} from '../metrics_manager.js'; import {Scrollbar} from '../scrollbar.js'; @@ -78,6 +79,8 @@ export abstract class Bubble implements IBubble { /** The position of the left of the bubble realtive to its anchor. */ private relativeLeft = 0; + private dragStrategy = new BubbleDragStrategy(this, this.workspace); + /** * @param workspace The workspace this bubble belongs to. * @param anchor The anchor location of the thing this bubble is attached to. @@ -612,4 +615,29 @@ export abstract class Bubble implements IBubble { showContextMenu(_e: Event) { // NOOP in base class. } + + /** Returns whether this bubble is movable or not. */ + isMovable(): boolean { + return true; + } + + /** Starts a drag on the bubble. */ + startDrag(): void { + this.dragStrategy.startDrag(); + } + + /** Drags the bubble to the given location. */ + drag(newLoc: Coordinate): void { + this.dragStrategy.drag(newLoc); + } + + /** Ends the drag on the bubble. */ + endDrag(): void { + this.dragStrategy.endDrag(); + } + + /** Moves the bubble back to where it was at the start of a drag. */ + revertDrag(): void { + this.dragStrategy.revertDrag(); + } } diff --git a/core/dragging/bubble_drag_strategy.ts b/core/dragging/bubble_drag_strategy.ts new file mode 100644 index 00000000000..97bcfdfebeb --- /dev/null +++ b/core/dragging/bubble_drag_strategy.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IDragStrategy} from '../interfaces/i_draggable.js'; +import {Coordinate} from '../utils.js'; +import * as eventUtils from '../events/utils.js'; +import {IBubble, WorkspaceSvg} from '../blockly.js'; +import * as layers from '../layers.js'; + +export class BubbleDragStrategy implements IDragStrategy { + private startLoc: Coordinate | null = null; + + constructor( + private bubble: IBubble, + private workspace: WorkspaceSvg, + ) {} + + isMovable(): boolean { + return true; + } + + startDrag(): void { + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } + this.startLoc = this.bubble.getRelativeToSurfaceXY(); + this.workspace.setResizesEnabled(false); + this.workspace.getLayerManager()?.moveToDragLayer(this.bubble); + this.bubble.setDragging && this.bubble.setDragging(true); + } + + drag(newLoc: Coordinate): void { + this.bubble.moveDuringDrag(newLoc); + } + + endDrag(): void { + this.workspace.setResizesEnabled(true); + eventUtils.setGroup(false); + + this.workspace + .getLayerManager() + ?.moveOffDragLayer(this.bubble, layers.BLOCK); + this.bubble.setDragging(false); + } + + revertDrag(): void { + if (this.startLoc) this.bubble.moveDuringDrag(this.startLoc); + } +} diff --git a/core/gesture.ts b/core/gesture.ts index 3ac499b3254..406e8d5f160 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -18,7 +18,6 @@ import './events/events_click.js'; import * as blockAnimations from './block_animations.js'; import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; -import {BubbleDragger} from './bubble_dragger.js'; import * as common from './common.js'; import {config} from './config.js'; import * as dropDownDiv from './dropdowndiv.js'; @@ -37,6 +36,7 @@ import type {WorkspaceSvg} from './workspace_svg.js'; import type {IIcon} from './interfaces/i_icon.js'; import {IDragger} from './interfaces/i_dragger.js'; import * as registry from './registry.js'; +import {IDraggable} from './interfaces/i_draggable.js'; /** * Note: In this file "start" refers to pointerdown @@ -114,9 +114,6 @@ export class Gesture { */ private boundEvents: browserEvents.Data[] = []; - /** The object tracking a bubble drag, or null if none is in progress. */ - private bubbleDragger: BubbleDragger | null = null; - private dragger: IDragger | null = null; /** @@ -309,12 +306,12 @@ export class Gesture { * * @returns True if a bubble is being dragged. */ - private updateIsDraggingBubble(): boolean { + private updateIsDraggingBubble(e: PointerEvent): boolean { if (!this.startBubble) { return false; } - this.startDraggingBubble(); + this.startDraggingBubble(e); return true; } @@ -390,7 +387,7 @@ export class Gesture { // First check if it was a bubble drag. Bubbles always sit on top of // blocks. - if (this.updateIsDraggingBubble()) { + if (this.updateIsDraggingBubble(e)) { return; } // Then check if it was a block drag. @@ -404,20 +401,13 @@ export class Gesture { /** Create a block dragger and start dragging the selected block. */ private startDraggingBlock(e: PointerEvent) { this.dragging = true; - - const DraggerClass = registry.getClassFromOptions( - registry.Type.DRAGGER, - this.creatorWorkspace.options, - true, - ); - - this.dragger = new DraggerClass!(this.targetBlock!, this.startWorkspace_!); + this.dragger = this.createDragger(this.targetBlock!, this.startWorkspace_!); this.dragger.onDragStart(e); this.dragger.onDrag(e, this.currentDragDeltaXY); } /** Create a bubble dragger and start dragging the selected bubble. */ - private startDraggingBubble() { + private startDraggingBubble(e: PointerEvent) { if (!this.startBubble) { throw new Error( 'Cannot update dragging the bubble because the start ' + @@ -432,15 +422,21 @@ export class Gesture { } this.dragging = true; - this.bubbleDragger = new BubbleDragger( - this.startBubble, - this.startWorkspace_, - ); - this.bubbleDragger.startBubbleDrag(); - this.bubbleDragger.dragBubble( - this.mostRecentEvent, - this.currentDragDeltaXY, + this.dragger = this.createDragger(this.startBubble, this.startWorkspace_); + this.dragger.onDragStart(e); + this.dragger.onDrag(e, this.currentDragDeltaXY); + } + + private createDragger( + draggable: IDraggable, + workspace: WorkspaceSvg, + ): IDragger { + const DraggerClass = registry.getClassFromOptions( + registry.Type.DRAGGER, + this.creatorWorkspace.options, + true, ); + return new DraggerClass!(draggable, workspace); } /** @@ -584,11 +580,6 @@ export class Gesture { this.workspaceDragger.drag(this.currentDragDeltaXY); } else if (this.dragger) { this.dragger.onDrag(this.mostRecentEvent, this.currentDragDeltaXY); - } else if (this.bubbleDragger) { - this.bubbleDragger.dragBubble( - this.mostRecentEvent, - this.currentDragDeltaXY, - ); } e.preventDefault(); e.stopPropagation(); @@ -624,9 +615,7 @@ export class Gesture { // than clicks. Fields and icons have higher priority than blocks; blocks // have higher priority than workspaces. The ordering within drags does // not matter, because the three types of dragging are exclusive. - if (this.bubbleDragger) { - this.bubbleDragger.endBubbleDrag(e, this.currentDragDeltaXY); - } else if (this.dragger) { + if (this.dragger) { this.dragger.onDragEnd(e, this.currentDragDeltaXY); } else if (this.workspaceDragger) { this.workspaceDragger.endDrag(this.currentDragDeltaXY); @@ -787,12 +776,7 @@ export class Gesture { return; } Touch.longStop(); - if (this.bubbleDragger) { - this.bubbleDragger.endBubbleDrag( - this.mostRecentEvent, - this.currentDragDeltaXY, - ); - } else if (this.dragger) { + if (this.dragger) { this.dragger.onDragEnd(this.mostRecentEvent, this.currentDragDeltaXY); } else if (this.workspaceDragger) { this.workspaceDragger.endDrag(this.currentDragDeltaXY); @@ -1229,10 +1213,10 @@ export class Gesture { * @returns The dragger that is currently in use or null if no drag is in * progress. */ - getCurrentDragger(): WorkspaceDragger | BubbleDragger | IBlockDragger | null { + getCurrentDragger(): WorkspaceDragger | IBlockDragger | null { // TODO: Change this to return the `dragger`, when we get rid of the last // other dragger. - return this.workspaceDragger ?? this.bubbleDragger; + return this.workspaceDragger; } /** diff --git a/core/interfaces/i_bubble.ts b/core/interfaces/i_bubble.ts index 93e3f0d4728..e27fccafd48 100644 --- a/core/interfaces/i_bubble.ts +++ b/core/interfaces/i_bubble.ts @@ -8,7 +8,7 @@ import type {Coordinate} from '../utils/coordinate.js'; // Former goog.module ID: Blockly.IBubble import type {IContextMenu} from './i_contextmenu.js'; -import type {IDraggable} from './i_draggable.old.js'; +import type {IDraggable} from './i_draggable.js'; /** * A bubble interface. diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts index 0aa4292c684..4c15ec7d704 100644 --- a/core/workspace_comment_svg.ts +++ b/core/workspace_comment_svg.ts @@ -21,7 +21,6 @@ import * as Css from './css.js'; import type {CommentMove} from './events/events_comment_move.js'; import * as eventUtils from './events/utils.js'; import type {IBoundedElement} from './interfaces/i_bounded_element.js'; -import type {IBubble} from './interfaces/i_bubble.js'; import type {ICopyable} from './interfaces/i_copyable.js'; import * as Touch from './touch.js'; import {Coordinate} from './utils/coordinate.js'; @@ -50,7 +49,7 @@ const TEXTAREA_OFFSET = 2; */ export class WorkspaceCommentSvg extends WorkspaceComment - implements IBoundedElement, IBubble, ICopyable + implements IBoundedElement, ICopyable { /** * The width and height to use to size a workspace comment when it is first @@ -198,7 +197,7 @@ export class WorkspaceCommentSvg private pathMouseDown(e: PointerEvent) { const gesture = this.workspace.getGesture(e); if (gesture) { - gesture.handleBubbleStart(e, this); + gesture.handleBubbleStart(e, this as AnyDuringMigration); } }