mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
* feat: change gestures to look at selected when dragging * chore: fix tests * chore: format * chore: PR comments
1201 lines
34 KiB
TypeScript
1201 lines
34 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* The class representing an in-progress gesture, e.g. a drag,
|
|
* tap, or pinch to zoom.
|
|
*
|
|
* @class
|
|
*/
|
|
// Former goog.module ID: Blockly.Gesture
|
|
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
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 * as common from './common.js';
|
|
import {config} from './config.js';
|
|
import * as dropDownDiv from './dropdowndiv.js';
|
|
import * as eventUtils from './events/utils.js';
|
|
import type {Field} from './field.js';
|
|
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
|
|
import type {IBubble} from './interfaces/i_bubble.js';
|
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
|
import * as internalConstants from './internal_constants.js';
|
|
import * as Tooltip from './tooltip.js';
|
|
import * as Touch from './touch.js';
|
|
import {Coordinate} from './utils/coordinate.js';
|
|
import {WorkspaceDragger} from './workspace_dragger.js';
|
|
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, isDraggable} from './interfaces/i_draggable.js';
|
|
import {RenderedWorkspaceComment} from './comments.js';
|
|
|
|
/**
|
|
* Note: In this file "start" refers to pointerdown
|
|
* events. "End" refers to pointerup events.
|
|
*/
|
|
|
|
/** A multiplier used to convert the gesture scale to a zoom in delta. */
|
|
const ZOOM_IN_MULTIPLIER = 5;
|
|
|
|
/** A multiplier used to convert the gesture scale to a zoom out delta. */
|
|
const ZOOM_OUT_MULTIPLIER = 6;
|
|
|
|
/**
|
|
* Class for one gesture.
|
|
*/
|
|
export class Gesture {
|
|
/**
|
|
* The position of the pointer when the gesture started. Units are CSS
|
|
* pixels, with (0, 0) at the top left of the browser window (pointer event
|
|
* clientX/Y).
|
|
*/
|
|
private mouseDownXY = new Coordinate(0, 0);
|
|
private currentDragDeltaXY: Coordinate;
|
|
|
|
/**
|
|
* The bubble that the gesture started on, or null if it did not start on a
|
|
* bubble.
|
|
*/
|
|
private startBubble: IBubble | null = null;
|
|
|
|
/**
|
|
* The field that the gesture started on, or null if it did not start on a
|
|
* field.
|
|
*/
|
|
private startField: Field | null = null;
|
|
|
|
/**
|
|
* The icon that the gesture started on, or null if it did not start on an
|
|
* icon.
|
|
*/
|
|
private startIcon: IIcon | null = null;
|
|
|
|
/**
|
|
* The block that the gesture started on, or null if it did not start on a
|
|
* block.
|
|
*/
|
|
private startBlock: BlockSvg | null = null;
|
|
|
|
/**
|
|
* The comment that the gesture started on, or null if it did not start on a
|
|
* comment.
|
|
*/
|
|
private startComment: RenderedWorkspaceComment | null = null;
|
|
|
|
/**
|
|
* The block that this gesture targets. If the gesture started on a
|
|
* shadow block, this is the first non-shadow parent of the block. If the
|
|
* gesture started in the flyout, this is the root block of the block group
|
|
* that was clicked or dragged.
|
|
*/
|
|
private targetBlock: BlockSvg | null = null;
|
|
|
|
/**
|
|
* The workspace that the gesture started on. There may be multiple
|
|
* workspaces on a page; this is more accurate than using
|
|
* Blockly.common.getMainWorkspace().
|
|
*/
|
|
protected startWorkspace_: WorkspaceSvg | null = null;
|
|
|
|
/**
|
|
* Whether the pointer has at any point moved out of the drag radius.
|
|
* A gesture that exceeds the drag radius is a drag even if it ends exactly
|
|
* at its start point.
|
|
*/
|
|
private hasExceededDragRadius = false;
|
|
|
|
/**
|
|
* Array holding info needed to unbind events.
|
|
* Used for disposing.
|
|
* Ex: [[node, name, func], [node, name, func]].
|
|
*/
|
|
private boundEvents: browserEvents.Data[] = [];
|
|
|
|
private dragger: IDragger | null = null;
|
|
|
|
/**
|
|
* The object tracking a workspace or flyout workspace drag, or null if none
|
|
* is in progress.
|
|
*/
|
|
private workspaceDragger: WorkspaceDragger | null = null;
|
|
|
|
/** Whether the gesture is dragging or not. */
|
|
private dragging: boolean = false;
|
|
|
|
/** The flyout a gesture started in, if any. */
|
|
private flyout: IFlyout | null = null;
|
|
|
|
/** Boolean for sanity-checking that some code is only called once. */
|
|
private calledUpdateIsDragging = false;
|
|
|
|
/** Boolean for sanity-checking that some code is only called once. */
|
|
private gestureHasStarted = false;
|
|
|
|
/** Boolean used internally to break a cycle in disposal. */
|
|
protected isEnding_ = false;
|
|
private healStack: boolean;
|
|
|
|
/** The event that most recently updated this gesture. */
|
|
private mostRecentEvent: PointerEvent;
|
|
|
|
/** Boolean for whether or not this gesture is a multi-touch gesture. */
|
|
private isMultiTouch_ = false;
|
|
|
|
/** A map of cached points used for tracking multi-touch gestures. */
|
|
private cachedPoints = new Map<string, Coordinate | null>();
|
|
|
|
/**
|
|
* This is the ratio between the starting distance between the touch points
|
|
* and the most recent distance between the touch points.
|
|
* Scales between 0 and 1 mean the most recent zoom was a zoom out.
|
|
* Scales above 1.0 mean the most recent zoom was a zoom in.
|
|
*/
|
|
private previousScale = 0;
|
|
|
|
/** The starting distance between two touch points. */
|
|
private startDistance = 0;
|
|
|
|
/** Boolean for whether or not the workspace supports pinch-zoom. */
|
|
private isPinchZoomEnabled: boolean | null = null;
|
|
|
|
/**
|
|
* The owner of the dropdownDiv when this gesture first starts.
|
|
* Needed because we'll close the dropdown before fields get to
|
|
* act on their events, and some fields care about who owns
|
|
* the dropdown.
|
|
*/
|
|
currentDropdownOwner: Field | null = null;
|
|
|
|
/**
|
|
* @param e The event that kicked off this gesture.
|
|
* @param creatorWorkspace The workspace that created this gesture and has a
|
|
* reference to it.
|
|
*/
|
|
constructor(
|
|
e: PointerEvent,
|
|
private readonly creatorWorkspace: WorkspaceSvg,
|
|
) {
|
|
this.mostRecentEvent = e;
|
|
|
|
/**
|
|
* How far the pointer has moved during this drag, in pixel units.
|
|
* (0, 0) is at this.mouseDownXY_.
|
|
*/
|
|
this.currentDragDeltaXY = new Coordinate(0, 0);
|
|
|
|
/**
|
|
* Boolean used to indicate whether or not to heal the stack after
|
|
* disconnecting a block.
|
|
*/
|
|
this.healStack = !internalConstants.DRAG_STACK;
|
|
}
|
|
|
|
/**
|
|
* Sever all links from this object.
|
|
*
|
|
* @internal
|
|
*/
|
|
dispose() {
|
|
Touch.clearTouchIdentifier();
|
|
Tooltip.unblock();
|
|
// Clear the owner's reference to this gesture.
|
|
this.creatorWorkspace.clearGesture();
|
|
|
|
for (const event of this.boundEvents) {
|
|
browserEvents.unbind(event);
|
|
}
|
|
this.boundEvents.length = 0;
|
|
|
|
if (this.workspaceDragger) {
|
|
this.workspaceDragger.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update internal state based on an event.
|
|
*
|
|
* @param e The most recent pointer event.
|
|
*/
|
|
private updateFromEvent(e: PointerEvent) {
|
|
const currentXY = new Coordinate(e.clientX, e.clientY);
|
|
const changed = this.updateDragDelta(currentXY);
|
|
// Exceeded the drag radius for the first time.
|
|
if (changed) {
|
|
this.updateIsDragging(e);
|
|
Touch.longStop();
|
|
}
|
|
this.mostRecentEvent = e;
|
|
}
|
|
|
|
/**
|
|
* DO MATH to set currentDragDeltaXY_ based on the most recent pointer
|
|
* position.
|
|
*
|
|
* @param currentXY The most recent pointer position, in pixel units,
|
|
* with (0, 0) at the window's top left corner.
|
|
* @returns True if the drag just exceeded the drag radius for the first time.
|
|
*/
|
|
private updateDragDelta(currentXY: Coordinate): boolean {
|
|
this.currentDragDeltaXY = Coordinate.difference(
|
|
currentXY,
|
|
this.mouseDownXY,
|
|
);
|
|
|
|
if (!this.hasExceededDragRadius) {
|
|
const currentDragDelta = Coordinate.magnitude(this.currentDragDeltaXY);
|
|
|
|
// The flyout has a different drag radius from the rest of Blockly.
|
|
const limitRadius = this.flyout
|
|
? config.flyoutDragRadius
|
|
: config.dragRadius;
|
|
|
|
this.hasExceededDragRadius = currentDragDelta > limitRadius;
|
|
return this.hasExceededDragRadius;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether a block is being dragged from the
|
|
* flyout.
|
|
* This function should be called on a pointermove event the first time
|
|
* the drag radius is exceeded. It should be called no more than once per
|
|
* gesture. If a block should be dragged from the flyout this function creates
|
|
* the new block on the main workspace and updates targetBlock_ and
|
|
* startWorkspace_.
|
|
*
|
|
* @returns True if a block is being dragged from the flyout.
|
|
*/
|
|
private updateIsDraggingFromFlyout(): boolean {
|
|
if (!this.targetBlock || !this.flyout?.isBlockCreatable(this.targetBlock)) {
|
|
return false;
|
|
}
|
|
if (!this.flyout.targetWorkspace) {
|
|
throw new Error(`Cannot update dragging from the flyout because the ' +
|
|
'flyout's target workspace is undefined`);
|
|
}
|
|
if (
|
|
!this.flyout.isScrollable() ||
|
|
this.flyout.isDragTowardWorkspace(this.currentDragDeltaXY)
|
|
) {
|
|
this.startWorkspace_ = this.flyout.targetWorkspace;
|
|
this.startWorkspace_.updateScreenCalculationsIfScrolled();
|
|
// Start the event group now, so that the same event group is used for
|
|
// block creation and block dragging.
|
|
if (!eventUtils.getGroup()) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
// The start block is no longer relevant, because this is a drag.
|
|
this.startBlock = null;
|
|
this.targetBlock = this.flyout.createBlock(this.targetBlock);
|
|
common.setSelected(this.targetBlock);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check whether to start a workspace drag. If a workspace is being dragged,
|
|
* create the necessary WorkspaceDragger and start the drag.
|
|
*
|
|
* This function should be called on a pointermove event the first time
|
|
* the drag radius is exceeded. It should be called no more than once per
|
|
* gesture. If a workspace is being dragged this function creates the
|
|
* necessary WorkspaceDragger and starts the drag.
|
|
*/
|
|
private updateIsDraggingWorkspace() {
|
|
if (!this.startWorkspace_) {
|
|
throw new Error(
|
|
'Cannot update dragging the workspace because the ' +
|
|
'start workspace is undefined',
|
|
);
|
|
}
|
|
|
|
const wsMovable = this.flyout
|
|
? this.flyout.isScrollable()
|
|
: this.startWorkspace_ && this.startWorkspace_.isDraggable();
|
|
if (!wsMovable) return;
|
|
|
|
this.dragging = true;
|
|
this.workspaceDragger = new WorkspaceDragger(this.startWorkspace_);
|
|
|
|
this.workspaceDragger.startDrag();
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether anything is being dragged.
|
|
* This function should be called on a pointermove event the first time
|
|
* the drag radius is exceeded. It should be called no more than once per
|
|
* gesture.
|
|
*/
|
|
private updateIsDragging(e: PointerEvent) {
|
|
if (!this.startWorkspace_) {
|
|
throw new Error(
|
|
'Cannot update dragging because the start workspace is undefined',
|
|
);
|
|
}
|
|
|
|
if (this.calledUpdateIsDragging) {
|
|
throw Error('updateIsDragging_ should only be called once per gesture.');
|
|
}
|
|
this.calledUpdateIsDragging = true;
|
|
|
|
// If we drag a block out of the flyout, it updates `common.getSelected`
|
|
// to return the new block.
|
|
if (this.flyout) this.updateIsDraggingFromFlyout();
|
|
|
|
const selected = common.getSelected();
|
|
if (selected && isDraggable(selected) && selected.isMovable()) {
|
|
this.dragging = true;
|
|
this.dragger = this.createDragger(selected, this.startWorkspace_);
|
|
this.dragger.onDragStart(e);
|
|
this.dragger.onDrag(e, this.currentDragDeltaXY);
|
|
} else {
|
|
this.updateIsDraggingWorkspace();
|
|
}
|
|
}
|
|
|
|
private createDragger(
|
|
draggable: IDraggable,
|
|
workspace: WorkspaceSvg,
|
|
): IDragger {
|
|
const DraggerClass = registry.getClassFromOptions(
|
|
registry.Type.DRAGGER,
|
|
this.creatorWorkspace.options,
|
|
true,
|
|
);
|
|
return new DraggerClass!(draggable, workspace);
|
|
}
|
|
|
|
/**
|
|
* Start a gesture: update the workspace to indicate that a gesture is in
|
|
* progress and bind pointermove and pointerup handlers.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @internal
|
|
*/
|
|
doStart(e: PointerEvent) {
|
|
if (!this.startWorkspace_) {
|
|
throw new Error(
|
|
'Cannot start the touch gesture becauase the start ' +
|
|
'workspace is undefined',
|
|
);
|
|
}
|
|
this.isPinchZoomEnabled =
|
|
this.startWorkspace_.options.zoomOptions &&
|
|
this.startWorkspace_.options.zoomOptions.pinch;
|
|
|
|
if (browserEvents.isTargetInput(e)) {
|
|
this.cancel();
|
|
return;
|
|
}
|
|
|
|
this.gestureHasStarted = true;
|
|
|
|
blockAnimations.disconnectUiStop();
|
|
|
|
this.startWorkspace_.updateScreenCalculationsIfScrolled();
|
|
if (this.startWorkspace_.isMutator) {
|
|
// Mutator's coordinate system could be out of date because the bubble was
|
|
// dragged, the block was moved, the parent workspace zoomed, etc.
|
|
this.startWorkspace_.resize();
|
|
}
|
|
|
|
// Keep track of which field owns the dropdown before we close it.
|
|
this.currentDropdownOwner = dropDownDiv.getOwner();
|
|
// Hide chaff also hides the flyout, so don't do it if the click is in a
|
|
// flyout.
|
|
this.startWorkspace_.hideChaff(!!this.flyout);
|
|
|
|
this.startWorkspace_.markFocused();
|
|
this.mostRecentEvent = e;
|
|
|
|
Tooltip.block();
|
|
|
|
if (browserEvents.isRightButton(e)) {
|
|
this.handleRightClick(e);
|
|
return;
|
|
}
|
|
|
|
if (e.type.toLowerCase() === 'pointerdown' && e.pointerType !== 'mouse') {
|
|
Touch.longStart(e, this);
|
|
}
|
|
|
|
this.mouseDownXY = new Coordinate(e.clientX, e.clientY);
|
|
this.healStack = e.altKey || e.ctrlKey || e.metaKey;
|
|
|
|
this.bindMouseEvents(e);
|
|
|
|
if (!this.isEnding_) {
|
|
this.handleTouchStart(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bind gesture events.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @internal
|
|
*/
|
|
bindMouseEvents(e: PointerEvent) {
|
|
this.boundEvents.push(
|
|
browserEvents.conditionalBind(
|
|
document,
|
|
'pointerdown',
|
|
null,
|
|
this.handleStart.bind(this),
|
|
/* opt_noCaptureIdentifier */ true,
|
|
),
|
|
);
|
|
this.boundEvents.push(
|
|
browserEvents.conditionalBind(
|
|
document,
|
|
'pointermove',
|
|
null,
|
|
this.handleMove.bind(this),
|
|
/* opt_noCaptureIdentifier */ true,
|
|
),
|
|
);
|
|
this.boundEvents.push(
|
|
browserEvents.conditionalBind(
|
|
document,
|
|
'pointerup',
|
|
null,
|
|
this.handleUp.bind(this),
|
|
/* opt_noCaptureIdentifier */ true,
|
|
),
|
|
);
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @internal
|
|
*/
|
|
handleStart(e: PointerEvent) {
|
|
if (this.isDragging()) {
|
|
// A drag has already started, so this can no longer be a pinch-zoom.
|
|
return;
|
|
}
|
|
this.handleTouchStart(e);
|
|
|
|
if (this.isMultiTouch()) {
|
|
Touch.longStop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a pointermove event.
|
|
*
|
|
* @param e A pointermove event.
|
|
* @internal
|
|
*/
|
|
handleMove(e: PointerEvent) {
|
|
if (
|
|
(this.isDragging() && Touch.shouldHandleEvent(e)) ||
|
|
!this.isMultiTouch()
|
|
) {
|
|
this.updateFromEvent(e);
|
|
if (this.workspaceDragger) {
|
|
this.workspaceDragger.drag(this.currentDragDeltaXY);
|
|
} else if (this.dragger) {
|
|
this.dragger.onDrag(this.mostRecentEvent, this.currentDragDeltaXY);
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
} else if (this.isMultiTouch()) {
|
|
this.handleTouchMove(e);
|
|
Touch.longStop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerup event.
|
|
*
|
|
* @param e A pointerup event.
|
|
* @internal
|
|
*/
|
|
handleUp(e: PointerEvent) {
|
|
if (!this.isDragging()) {
|
|
this.handleTouchEnd(e);
|
|
}
|
|
if (!this.isMultiTouch() || this.isDragging()) {
|
|
if (!Touch.shouldHandleEvent(e)) {
|
|
return;
|
|
}
|
|
this.updateFromEvent(e);
|
|
Touch.longStop();
|
|
|
|
if (this.isEnding_) {
|
|
console.log('Trying to end a gesture recursively.');
|
|
return;
|
|
}
|
|
this.isEnding_ = true;
|
|
// The ordering of these checks is important: drags have higher priority
|
|
// 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.dragger) {
|
|
this.dragger.onDragEnd(e, this.currentDragDeltaXY);
|
|
} else if (this.workspaceDragger) {
|
|
this.workspaceDragger.endDrag(this.currentDragDeltaXY);
|
|
} else if (this.isBubbleClick()) {
|
|
// Do nothing, bubbles don't currently respond to clicks.
|
|
} else if (this.isFieldClick()) {
|
|
this.doFieldClick();
|
|
} else if (this.isIconClick()) {
|
|
this.doIconClick();
|
|
} else if (this.isBlockClick()) {
|
|
this.doBlockClick();
|
|
} else if (this.isWorkspaceClick()) {
|
|
this.doWorkspaceClick(e);
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.dispose();
|
|
} else {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event and keep track of current
|
|
* pointers.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @internal
|
|
*/
|
|
handleTouchStart(e: PointerEvent) {
|
|
const pointerId = Touch.getTouchIdentifierFromEvent(e);
|
|
// store the pointerId in the current list of pointers
|
|
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
|
|
const pointers = Array.from(this.cachedPoints.keys());
|
|
// If two pointers are down, store info
|
|
if (pointers.length === 2) {
|
|
const point0 = this.cachedPoints.get(pointers[0])!;
|
|
const point1 = this.cachedPoints.get(pointers[1])!;
|
|
this.startDistance = Coordinate.distance(point0, point1);
|
|
this.isMultiTouch_ = true;
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle a pointermove event and zoom in/out if two pointers
|
|
* are on the screen.
|
|
*
|
|
* @param e A pointermove event.
|
|
* @internal
|
|
*/
|
|
handleTouchMove(e: PointerEvent) {
|
|
const pointerId = Touch.getTouchIdentifierFromEvent(e);
|
|
// Update the cache
|
|
this.cachedPoints.set(pointerId, this.getTouchPoint(e));
|
|
|
|
if (this.isPinchZoomEnabled && this.cachedPoints.size === 2) {
|
|
this.handlePinch(e);
|
|
} else {
|
|
this.handleMove(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle pinch zoom gesture.
|
|
*
|
|
* @param e A pointermove event.
|
|
*/
|
|
private handlePinch(e: PointerEvent) {
|
|
const pointers = Array.from(this.cachedPoints.keys());
|
|
// Calculate the distance between the two pointers
|
|
const point0 = this.cachedPoints.get(pointers[0])!;
|
|
const point1 = this.cachedPoints.get(pointers[1])!;
|
|
const moveDistance = Coordinate.distance(point0, point1);
|
|
const scale = moveDistance / this.startDistance;
|
|
|
|
if (this.previousScale > 0 && this.previousScale < Infinity) {
|
|
const gestureScale = scale - this.previousScale;
|
|
const delta =
|
|
gestureScale > 0
|
|
? gestureScale * ZOOM_IN_MULTIPLIER
|
|
: gestureScale * ZOOM_OUT_MULTIPLIER;
|
|
if (!this.startWorkspace_) {
|
|
throw new Error(
|
|
'Cannot handle a pinch because the start workspace ' + 'is undefined',
|
|
);
|
|
}
|
|
const workspace = this.startWorkspace_;
|
|
const position = browserEvents.mouseToSvg(
|
|
e,
|
|
workspace.getParentSvg(),
|
|
workspace.getInverseScreenCTM(),
|
|
);
|
|
workspace.zoom(position.x, position.y, delta);
|
|
}
|
|
this.previousScale = scale;
|
|
e.preventDefault();
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerup event and end the gesture.
|
|
*
|
|
* @param e A pointerup event.
|
|
* @internal
|
|
*/
|
|
handleTouchEnd(e: PointerEvent) {
|
|
const pointerId = Touch.getTouchIdentifierFromEvent(e);
|
|
if (this.cachedPoints.has(pointerId)) {
|
|
this.cachedPoints.delete(pointerId);
|
|
}
|
|
if (this.cachedPoints.size < 2) {
|
|
this.cachedPoints.clear();
|
|
this.previousScale = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function returning the current touch point coordinate.
|
|
*
|
|
* @param e A pointer event.
|
|
* @returns The current touch point coordinate
|
|
* @internal
|
|
*/
|
|
getTouchPoint(e: PointerEvent): Coordinate | null {
|
|
if (!this.startWorkspace_) {
|
|
return null;
|
|
}
|
|
return new Coordinate(e.pageX, e.pageY);
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is part of a multi-touch gesture.
|
|
*
|
|
* @returns Whether this gesture is part of a multi-touch gesture.
|
|
* @internal
|
|
*/
|
|
isMultiTouch(): boolean {
|
|
return this.isMultiTouch_;
|
|
}
|
|
|
|
/**
|
|
* Cancel an in-progress gesture. If a workspace or block drag is in
|
|
* progress, end the drag at the most recent location.
|
|
*
|
|
* @internal
|
|
*/
|
|
cancel() {
|
|
// Disposing of a block cancels in-progress drags, but dragging to a delete
|
|
// area disposes of a block and leads to recursive disposal. Break that
|
|
// cycle.
|
|
if (this.isEnding_) {
|
|
return;
|
|
}
|
|
Touch.longStop();
|
|
if (this.dragger) {
|
|
this.dragger.onDragEnd(this.mostRecentEvent, this.currentDragDeltaXY);
|
|
} else if (this.workspaceDragger) {
|
|
this.workspaceDragger.endDrag(this.currentDragDeltaXY);
|
|
}
|
|
this.dispose();
|
|
}
|
|
|
|
/**
|
|
* Handle a real or faked right-click event by showing a context menu.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @internal
|
|
*/
|
|
handleRightClick(e: PointerEvent) {
|
|
if (this.targetBlock) {
|
|
this.bringBlockToFront();
|
|
this.targetBlock.workspace.hideChaff(!!this.flyout);
|
|
this.targetBlock.showContextMenu(e);
|
|
} else if (this.startBubble) {
|
|
this.startBubble.showContextMenu(e);
|
|
} else if (this.startWorkspace_ && !this.flyout) {
|
|
this.startWorkspace_.hideChaff();
|
|
this.startWorkspace_.showContextMenu(e);
|
|
}
|
|
|
|
// TODO: Handle right-click on a bubble.
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.dispose();
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event on a workspace.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @param ws The workspace the event hit.
|
|
* @internal
|
|
*/
|
|
handleWsStart(e: PointerEvent, ws: WorkspaceSvg) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.handleWsStart, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
this.setStartWorkspace(ws);
|
|
this.mostRecentEvent = e;
|
|
|
|
if (!this.startBlock && !this.startBubble && !this.startComment) {
|
|
// Selection determines what things start drags. So to drag the workspace,
|
|
// we need to deselect anything that was previously selected.
|
|
common.setSelected(null);
|
|
}
|
|
|
|
this.doStart(e);
|
|
}
|
|
|
|
/**
|
|
* Fires a workspace click event.
|
|
*
|
|
* @param ws The workspace that a user clicks on.
|
|
*/
|
|
private fireWorkspaceClick(ws: WorkspaceSvg) {
|
|
eventUtils.fire(
|
|
new (eventUtils.get(eventUtils.CLICK))(null, ws.id, 'workspace'),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event on a flyout.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @param flyout The flyout the event hit.
|
|
* @internal
|
|
*/
|
|
handleFlyoutStart(e: PointerEvent, flyout: IFlyout) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.handleFlyoutStart, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
this.setStartFlyout(flyout);
|
|
this.handleWsStart(e, flyout.getWorkspace());
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event on a block.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @param block The block the event hit.
|
|
* @internal
|
|
*/
|
|
handleBlockStart(e: PointerEvent, block: BlockSvg) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.handleBlockStart, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
this.setStartBlock(block);
|
|
this.mostRecentEvent = e;
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event on a bubble.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @param bubble The bubble the event hit.
|
|
* @internal
|
|
*/
|
|
handleBubbleStart(e: PointerEvent, bubble: IBubble) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.handleBubbleStart, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
this.setStartBubble(bubble);
|
|
this.mostRecentEvent = e;
|
|
}
|
|
|
|
/**
|
|
* Handle a pointerdown event on a workspace comment.
|
|
*
|
|
* @param e A pointerdown event.
|
|
* @param comment The comment the event hit.
|
|
* @internal
|
|
*/
|
|
handleCommentStart(e: PointerEvent, comment: RenderedWorkspaceComment) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.handleCommentStart, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
this.setStartComment(comment);
|
|
this.mostRecentEvent = e;
|
|
}
|
|
|
|
/* Begin functions defining what actions to take to execute clicks on each
|
|
* type of target. Any developer wanting to add behaviour on clicks should
|
|
* modify only this code. */
|
|
|
|
/** Execute a field click. */
|
|
private doFieldClick() {
|
|
if (!this.startField) {
|
|
throw new Error(
|
|
'Cannot do a field click because the start field is undefined',
|
|
);
|
|
}
|
|
|
|
// Only show the editor if the field's editor wasn't already open
|
|
// right before this gesture started.
|
|
const dropdownAlreadyOpen = this.currentDropdownOwner === this.startField;
|
|
if (!dropdownAlreadyOpen) {
|
|
this.startField.showEditor(this.mostRecentEvent);
|
|
}
|
|
this.bringBlockToFront();
|
|
}
|
|
|
|
/** Execute an icon click. */
|
|
private doIconClick() {
|
|
if (!this.startIcon) {
|
|
throw new Error(
|
|
'Cannot do an icon click because the start icon is undefined',
|
|
);
|
|
}
|
|
this.bringBlockToFront();
|
|
this.startIcon.onClick();
|
|
}
|
|
|
|
/** Execute a block click. */
|
|
private doBlockClick() {
|
|
// Block click in an autoclosing flyout.
|
|
if (this.flyout && this.flyout.autoClose) {
|
|
if (!this.targetBlock) {
|
|
throw new Error(
|
|
'Cannot do a block click because the target block is ' + 'undefined',
|
|
);
|
|
}
|
|
if (this.targetBlock.isEnabled()) {
|
|
if (!eventUtils.getGroup()) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
const newBlock = this.flyout.createBlock(this.targetBlock);
|
|
newBlock.snapToGrid();
|
|
newBlock.bumpNeighbours();
|
|
}
|
|
} else {
|
|
if (!this.startWorkspace_) {
|
|
throw new Error(
|
|
'Cannot do a block click because the start workspace ' +
|
|
'is undefined',
|
|
);
|
|
}
|
|
// Clicks events are on the start block, even if it was a shadow.
|
|
const event = new (eventUtils.get(eventUtils.CLICK))(
|
|
this.startBlock,
|
|
this.startWorkspace_.id,
|
|
'block',
|
|
);
|
|
eventUtils.fire(event);
|
|
}
|
|
this.bringBlockToFront();
|
|
eventUtils.setGroup(false);
|
|
}
|
|
|
|
/**
|
|
* Execute a workspace click. When in accessibility mode shift clicking will
|
|
* move the cursor.
|
|
*
|
|
* @param _e A pointerup event.
|
|
*/
|
|
private doWorkspaceClick(_e: PointerEvent) {
|
|
const ws = this.creatorWorkspace;
|
|
if (common.getSelected()) {
|
|
common.getSelected()!.unselect();
|
|
}
|
|
this.fireWorkspaceClick(this.startWorkspace_ || ws);
|
|
}
|
|
|
|
/* End functions defining what actions to take to execute clicks on each type
|
|
* of target. */
|
|
|
|
// TODO (fenichel): Move bubbles to the front.
|
|
|
|
/**
|
|
* Move the dragged/clicked block to the front of the workspace so that it is
|
|
* not occluded by other blocks.
|
|
*/
|
|
private bringBlockToFront() {
|
|
// Blocks in the flyout don't overlap, so skip the work.
|
|
if (this.targetBlock && !this.flyout) {
|
|
this.targetBlock.bringToFront();
|
|
}
|
|
}
|
|
|
|
/* Begin functions for populating a gesture at pointerdown. */
|
|
|
|
/**
|
|
* Record the field that a gesture started on.
|
|
*
|
|
* @param field The field the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartField<T>(field: Field<T>) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.setStartField, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
if (!this.startField) {
|
|
this.startField = field as Field;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the icon that a gesture started on.
|
|
*
|
|
* @param icon The icon the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartIcon(icon: IIcon) {
|
|
if (this.gestureHasStarted) {
|
|
throw Error(
|
|
'Tried to call gesture.setStartIcon, ' +
|
|
'but the gesture had already been started.',
|
|
);
|
|
}
|
|
|
|
if (!this.startIcon) this.startIcon = icon;
|
|
}
|
|
|
|
/**
|
|
* Record the bubble that a gesture started on
|
|
*
|
|
* @param bubble The bubble the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartBubble(bubble: IBubble) {
|
|
if (!this.startBubble) {
|
|
this.startBubble = bubble;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the comment that a gesture started on
|
|
*
|
|
* @param comment The comment the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartComment(comment: RenderedWorkspaceComment) {
|
|
if (!this.startComment) {
|
|
this.startComment = comment;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the block that a gesture started on, and set the target block
|
|
* appropriately.
|
|
*
|
|
* @param block The block the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartBlock(block: BlockSvg) {
|
|
// If the gesture already went through a bubble, don't set the start block.
|
|
if (!this.startBlock && !this.startBubble) {
|
|
this.startBlock = block;
|
|
common.setSelected(this.startBlock);
|
|
if (block.isInFlyout && block !== block.getRootBlock()) {
|
|
this.setTargetBlock(block.getRootBlock());
|
|
} else {
|
|
this.setTargetBlock(block);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the block that a gesture targets, meaning the block that will be
|
|
* dragged if this turns into a drag. If this block is a shadow, that will be
|
|
* its first non-shadow parent.
|
|
*
|
|
* @param block The block the gesture targets.
|
|
*/
|
|
private setTargetBlock(block: BlockSvg) {
|
|
if (block.isShadow()) {
|
|
// Non-null assertion is fine b/c it is an invariant that shadows always
|
|
// have parents.
|
|
this.setTargetBlock(block.getParent()!);
|
|
} else {
|
|
this.targetBlock = block;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the workspace that a gesture started on.
|
|
*
|
|
* @param ws The workspace the gesture started on.
|
|
*/
|
|
private setStartWorkspace(ws: WorkspaceSvg) {
|
|
if (!this.startWorkspace_) {
|
|
this.startWorkspace_ = ws;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Record the flyout that a gesture started on.
|
|
*
|
|
* @param flyout The flyout the gesture started on.
|
|
*/
|
|
private setStartFlyout(flyout: IFlyout) {
|
|
if (!this.flyout) {
|
|
this.flyout = flyout;
|
|
}
|
|
}
|
|
|
|
/* End functions for populating a gesture at pointerdown. */
|
|
|
|
/* Begin helper functions defining types of clicks. Any developer wanting
|
|
* to change the definition of a click should modify only this code. */
|
|
|
|
/**
|
|
* Whether this gesture is a click on a bubble. This should only be called
|
|
* when ending a gesture (pointerup).
|
|
*
|
|
* @returns Whether this gesture was a click on a bubble.
|
|
*/
|
|
private isBubbleClick(): boolean {
|
|
// A bubble click starts on a bubble and never escapes the drag radius.
|
|
const hasStartBubble = !!this.startBubble;
|
|
return hasStartBubble && !this.hasExceededDragRadius;
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is a click on a block. This should only be called
|
|
* when ending a gesture (pointerup).
|
|
*
|
|
* @returns Whether this gesture was a click on a block.
|
|
*/
|
|
private isBlockClick(): boolean {
|
|
// A block click starts on a block, never escapes the drag radius, and is
|
|
// not a field click.
|
|
const hasStartBlock = !!this.startBlock;
|
|
return (
|
|
hasStartBlock &&
|
|
!this.hasExceededDragRadius &&
|
|
!this.isFieldClick() &&
|
|
!this.isIconClick()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is a click on a field that should be handled. This should only be called
|
|
* when ending a gesture (pointerup).
|
|
*
|
|
* @returns Whether this gesture was a click on a field.
|
|
*/
|
|
private isFieldClick(): boolean {
|
|
if (!this.startField) return false;
|
|
return (
|
|
this.startField.isClickable() &&
|
|
!this.hasExceededDragRadius &&
|
|
(!this.flyout ||
|
|
this.startField.isClickableInFlyout(this.flyout.autoClose))
|
|
);
|
|
}
|
|
|
|
/** @returns Whether this gesture is a click on an icon that should be handled. */
|
|
private isIconClick(): boolean {
|
|
if (!this.startIcon) return false;
|
|
const handleInFlyout =
|
|
!this.flyout ||
|
|
!this.startIcon.isClickableInFlyout ||
|
|
this.startIcon.isClickableInFlyout(this.flyout.autoClose);
|
|
return !this.hasExceededDragRadius && handleInFlyout;
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is a click on a workspace. This should only be called
|
|
* when ending a gesture (pointerup).
|
|
*
|
|
* @returns Whether this gesture was a click on a workspace.
|
|
*/
|
|
private isWorkspaceClick(): boolean {
|
|
const onlyTouchedWorkspace =
|
|
!this.startBlock && !this.startBubble && !this.startField;
|
|
return onlyTouchedWorkspace && !this.hasExceededDragRadius;
|
|
}
|
|
|
|
/* End helper functions defining types of clicks. */
|
|
|
|
/**
|
|
* Whether this gesture is a drag of either a workspace or block.
|
|
* This function is called externally to block actions that cannot be taken
|
|
* mid-drag (e.g. using the keyboard to delete the selected blocks).
|
|
*
|
|
* @returns True if this gesture is a drag of a workspace or block.
|
|
* @internal
|
|
*/
|
|
isDragging(): boolean {
|
|
return this.dragging;
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture has already been started. In theory every pointerdown
|
|
* has a corresponding pointerup, but in reality it is possible to lose a
|
|
* pointerup, leaving an in-process gesture hanging.
|
|
*
|
|
* @returns Whether this gesture was a click on a workspace.
|
|
* @internal
|
|
*/
|
|
hasStarted(): boolean {
|
|
return this.gestureHasStarted;
|
|
}
|
|
|
|
/**
|
|
* Gets the current dragger if an item is being dragged. Null if nothing is
|
|
* being dragged.
|
|
*
|
|
* @returns The dragger that is currently in use or null if no drag is in
|
|
* progress.
|
|
*/
|
|
getCurrentDragger(): WorkspaceDragger | IBlockDragger | null {
|
|
// TODO: Change this to return the `dragger`, when we get rid of the last
|
|
// other dragger.
|
|
return this.workspaceDragger;
|
|
}
|
|
|
|
/**
|
|
* Is a drag or other gesture currently in progress on any workspace?
|
|
*
|
|
* @returns True if gesture is occurring.
|
|
*/
|
|
static inProgress(): boolean {
|
|
const workspaces = common.getAllWorkspaces();
|
|
for (let i = 0, workspace; (workspace = workspaces[i]); i++) {
|
|
// Not actually necessarily a WorkspaceSvg, but it doesn't matter b/c
|
|
// we're just checking if the property exists. Theoretically we would
|
|
// want to use instanceof, but that causes a circular dependency.
|
|
if ((workspace as WorkspaceSvg).currentGesture_) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|