Skip to content
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

fix: draggable interface and concrete dragger #7967

Merged
merged 4 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 95 additions & 29 deletions core/dragging/dragger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,132 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {IDragTarget} from '../interfaces/i_drag_target.js';
import {IDeletable, isDeletable} from '../interfaces/i_deletable.js';
import {IDragger} from '../interfaces/i_dragger.js';
import {IDraggable} from '../interfaces/i_draggable.js';
import {Coordinate} from '../utils/coordinate.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import {ComponentManager} from '../component_manager.js';
import {IDeleteArea} from '../interfaces/i_delete_area.js';

export class Dragger implements IDragger {
/** Starting location of the draggable, in workspace coordinates. */
private startLoc: Coordinate;

private dragTarget: IDragTarget | null = null;

constructor(
private draggable: IDraggable,
private workspace: WorkspaceSvg,
) {
this.startLoc = draggable.getLocation();
this.startLoc = draggable.getRelativeToSurfaceXY();
}

/**
* Handles any drag startup.
*
* @param e PointerEvent that started the drag.
*/
/** Handles any drag startup. */
onDragStart(e: PointerEvent) {
this.draggable.startDrag(e);
}

/**
* Handles dragging, including calculating where the element should
* actually be moved to.
* Handles calculating where the element should actually be moved to.
*
* @param e PointerEvent that continued the drag.
* @param totalDelta The total distance, in pixels, that the mouse
* has moved since the start of the drag.
* @param totalDelta The total amount in pixel coordinates the mouse has moved
* since the start of the drag.
*/
onDrag(e: PointerEvent, totalDelta: Coordinate) {
this.draggable.drag(this.newLoc(totalDelta), e);
this.moveDraggable(e, totalDelta);

// Must check `wouldDelete` before calling other hooks on drag targets
// since we have documented that we would do so.
if (isDeletable(this.draggable)) {
(this.draggable as AnyDuringMigration).setDeleteStyle(
rachel-fenichel marked this conversation as resolved.
Show resolved Hide resolved
this.wouldDeleteDraggable(e, this.draggable),
);
}
this.updateDragTarget(e);
}

/** Updates the drag target under the pointer (if there is one). */
protected updateDragTarget(e: PointerEvent) {
const newDragTarget = this.workspace.getDragTarget(e);
if (this.dragTarget !== newDragTarget) {
this.dragTarget?.onDragExit(this.draggable as AnyDuringMigration);
newDragTarget?.onDragEnter(this.draggable as AnyDuringMigration);
}
newDragTarget?.onDragOver(this.draggable as AnyDuringMigration);
this.dragTarget = newDragTarget;
}

/**
* Handles any drag cleanup.
*
* @param e PointerEvent that finished the drag.
* @param totalDelta The total distance, in pixels, that the mouse
* has moved since the start of the drag.
* Calculates the correct workspace coordinate for the movable and tells
* the draggable to go to that location.
*/
onDragEnd(e: PointerEvent, totalDelta: Coordinate) {
this.draggable.endDrag(this.newLoc(totalDelta), e);
private moveDraggable(e: PointerEvent, totalDelta: Coordinate) {
const delta = this.pixelsToWorkspaceUnits(totalDelta);
const newLoc = Coordinate.sum(this.startLoc, delta);
this.draggable.drag(newLoc, e);
}

/**
* Calculates where the IDraggable should actually be moved to.
*
* @param totalDelta The total distance, in pixels, that the mouse
* has moved since the start of the drag.
* @returns The new location, in workspace coordinates.
* Returns true if we would delete the draggable if it was dropped
* at the current location.
*/
protected wouldDeleteDraggable(
e: PointerEvent,
draggable: IDraggable & IDeletable,
) {
const dragTarget = this.workspace.getDragTarget(e);
if (!dragTarget) return false;

const componentManager = this.workspace.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id,
ComponentManager.Capability.DELETE_AREA,
);
if (!isDeleteArea) return false;

return (dragTarget as IDeleteArea).wouldDelete(
draggable,
false,
// !!this.getConnectionCandidate(draggable, delta),
rachel-fenichel marked this conversation as resolved.
Show resolved Hide resolved
);
}

/** Handles any drag cleanup. */
onDragEnd(e: PointerEvent) {
const dragTarget = this.workspace.getDragTarget(e);
if (dragTarget) {
this.dragTarget?.onDrop(this.draggable as AnyDuringMigration);
}

if (this.shouldReturnToStart(e, this.draggable)) {
this.draggable.revertDrag();
}

this.draggable.endDrag(e);

if (
isDeletable(this.draggable) &&
this.wouldDeleteDraggable(e, this.draggable)
) {
(this.draggable as AnyDuringMigration).dispose();
}
}

/**
* Returns true if we should return the draggable to its original location
* at the end of the drag.
*/
protected newLoc(totalDelta: Coordinate): Coordinate {
protected shouldReturnToStart(e: PointerEvent, draggable: IDraggable) {
const dragTarget = this.workspace.getDragTarget(e);
if (!dragTarget) return false;
return dragTarget.shouldPreventMove(draggable as AnyDuringMigration);
}

protected pixelsToWorkspaceUnits(pixelCoord: Coordinate): Coordinate {
const result = new Coordinate(
totalDelta.x / this.workspace.scale,
totalDelta.y / this.workspace.scale,
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
Expand All @@ -71,7 +138,6 @@ export class Dragger implements IDragger {
const mainScale = this.workspace.options.parentWorkspace!.scale;
result.scale(1 / mainScale);
}
result.translate(this.startLoc.x, this.startLoc.y);
return result;
}
}
5 changes: 5 additions & 0 deletions core/interfaces/i_deletable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ export interface IDeletable {
*/
isDeletable(): boolean;
}

/** Returns whether the given object is an IDeletable. */
export function isDeletable(obj: any): obj is IDeletable {
return obj['isDeletable'] !== undefined;
}
33 changes: 11 additions & 22 deletions core/interfaces/i_draggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,29 @@
*/

import {Coordinate} from '../utils/coordinate';
import {IDragTarget} from './i_drag_target';

/**
* Represents an object that can be dragged.
*/
export interface IDraggable extends IDragStrategy {
/** Returns true iff the element is currently movable. */
isMovable(): boolean;

/**
* Returns the current location of the draggable in workspace
* coordinates.
*
* @returns Coordinate of current location on workspace.
*/
getLocation(): Coordinate;
getRelativeToSurfaceXY(): Coordinate;
}

/**
* Represents an object that can be dragged.
*/
export interface IDragStrategy {
/** Returns true iff the element is currently movable. */
isMovable(): boolean;

/**
* Handles any drag startup (e.g moving elements to the front of the
* workspace).
*
* @param e PointerEvent that started the drag; could be used to
* @param e PointerEvent that started the drag; can be used to
* check modifier keys, etc. May be missing when dragging is
* triggered programatically rather than by user.
*/
Expand All @@ -43,29 +39,22 @@ export interface IDragStrategy {
*
* @param newLoc Workspace coordinate to which the draggable has
* been dragged.
* @param e PointerEvent that continued the drag. Should be used to
* look up any IDragTarget the pointer is over; could also be
* @param e PointerEvent that continued the drag. Can be
* used to check modifier keys, etc.
* @param target The drag target the pointer is over, if any. Could
* be supplied as an alternative to providing a PointerEvent for
* programatic drags.
*/
drag(newLoc: Coordinate, e?: PointerEvent): void;
drag(newLoc: Coordinate, target: IDragTarget): void;

/**
* Handles any drag cleanup, including e.g. connecting or deleting
* blocks.
*
* @param newLoc Workspace coordinate at which the drag finished.
* been dragged.
* @param e PointerEvent that finished the drag. Should be used to
* look up any IDragTarget the pointer is over; could also be
* @param e PointerEvent that finished the drag. Can be
* used to check modifier keys, etc.
* @param target The drag target the pointer is over, if any. Could
* be supplied as an alternative to providing a PointerEvent for
* programatic drags.
*/
endDrag(newLoc: Coordinate, e?: PointerEvent): void;
endDrag(newLoc: Coordinate, target: IDragTarget): void;
endDrag(e?: PointerEvent): void;

/** Moves the draggable back to where it was at the start of the drag. */
revertDrag(): void;
}