mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
* Made workspace non-nullable. * chore: clang-format files. * fix: Fixed incorrect block disposal check in connection.ts
950 lines
31 KiB
TypeScript
950 lines
31 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview The class representing an in-progress gesture, usually a drag
|
|
* or a tap.
|
|
*/
|
|
|
|
/**
|
|
* The class representing an in-progress gesture, usually a drag
|
|
* or a tap.
|
|
* @class
|
|
*/
|
|
import * as goog from '../closure/goog/goog.js';
|
|
goog.declareModuleId('Blockly.Gesture');
|
|
|
|
// Unused import preserved for side-effects. Remove if unneeded.
|
|
// import './block_dragger.js';
|
|
// 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 {BubbleDragger} from './bubble_dragger.js';
|
|
import * as common from './common.js';
|
|
import {config} from './config.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 registry from './registry.js';
|
|
import * as Tooltip from './tooltip.js';
|
|
import * as Touch from './touch.js';
|
|
import {Coordinate} from './utils/coordinate.js';
|
|
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
|
|
import {WorkspaceDragger} from './workspace_dragger.js';
|
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
|
|
|
|
|
/**
|
|
* Note: In this file "start" refers to touchstart, mousedown, and pointerstart
|
|
* events. "End" refers to touchend, mouseup, and pointerend events.
|
|
*/
|
|
// TODO: Consider touchcancel/pointercancel.
|
|
/**
|
|
* Class for one gesture.
|
|
* @alias Blockly.Gesture
|
|
*/
|
|
export class Gesture {
|
|
/**
|
|
* The position of the mouse when the gesture started. Units are CSS
|
|
* pixels, with (0, 0) at the top left of the browser window (mouseEvent
|
|
* clientX/Y).
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'Coordinate'.
|
|
private mouseDownXY_: Coordinate = null as AnyDuringMigration;
|
|
private currentDragDeltaXY_: Coordinate;
|
|
|
|
/**
|
|
* The bubble that the gesture started on, or null if it did not start on a
|
|
* bubble.
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'IBubble'.
|
|
private startBubble_: IBubble = null as AnyDuringMigration;
|
|
|
|
/**
|
|
* The field that the gesture started on, or null if it did not start on a
|
|
* field.
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type 'Field'.
|
|
private startField_: Field = null as AnyDuringMigration;
|
|
|
|
/**
|
|
* The block that the gesture started on, or null if it did not start on a
|
|
* block.
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'BlockSvg'.
|
|
private startBlock_: BlockSvg = null as AnyDuringMigration;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'BlockSvg'.
|
|
private targetBlock_: BlockSvg = null as AnyDuringMigration;
|
|
|
|
/**
|
|
* The workspace that the gesture started on. There may be multiple
|
|
* workspaces on a page; this is more accurate than using
|
|
* Blockly.common.getMainWorkspace().
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'WorkspaceSvg'.
|
|
protected startWorkspace_: WorkspaceSvg = null as AnyDuringMigration;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/** Whether the workspace is currently being dragged. */
|
|
private isDraggingWorkspace_ = false;
|
|
|
|
/** Whether the block is currently being dragged. */
|
|
private isDraggingBlock_ = false;
|
|
|
|
/** Whether the bubble is currently being dragged. */
|
|
private isDraggingBubble_ = false;
|
|
|
|
/**
|
|
* A handle to use to unbind a mouse move listener at the end of a drag.
|
|
* Opaque data returned from Blockly.bindEventWithChecks_.
|
|
*/
|
|
protected onMoveWrapper_: browserEvents.Data|null = null;
|
|
|
|
/**
|
|
* A handle to use to unbind a mouse up listener at the end of a drag.
|
|
* Opaque data returned from Blockly.bindEventWithChecks_.
|
|
*/
|
|
protected onUpWrapper_: browserEvents.Data|null = null;
|
|
|
|
/** The object tracking a bubble drag, or null if none is in progress. */
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'BubbleDragger'.
|
|
private bubbleDragger_: BubbleDragger = null as AnyDuringMigration;
|
|
|
|
/** The object tracking a block drag, or null if none is in progress. */
|
|
private blockDragger_: IBlockDragger|null = null;
|
|
|
|
/**
|
|
* The object tracking a workspace or flyout workspace drag, or null if none
|
|
* is in progress.
|
|
*/
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'WorkspaceDragger'.
|
|
private workspaceDragger_: WorkspaceDragger = null as AnyDuringMigration;
|
|
|
|
/** The flyout a gesture started in, if any. */
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'IFlyout'.
|
|
private flyout_: IFlyout = null as AnyDuringMigration;
|
|
|
|
/** 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 hasStarted_ = 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_: Event;
|
|
|
|
/**
|
|
* @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: Event, private readonly creatorWorkspace: WorkspaceSvg) {
|
|
this.mostRecentEvent_ = e;
|
|
|
|
/**
|
|
* How far the mouse 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();
|
|
|
|
if (this.onMoveWrapper_) {
|
|
browserEvents.unbind(this.onMoveWrapper_);
|
|
}
|
|
if (this.onUpWrapper_) {
|
|
browserEvents.unbind(this.onUpWrapper_);
|
|
}
|
|
|
|
if (this.blockDragger_) {
|
|
this.blockDragger_.dispose();
|
|
}
|
|
if (this.workspaceDragger_) {
|
|
this.workspaceDragger_.dispose();
|
|
}
|
|
if (this.bubbleDragger_) {
|
|
this.bubbleDragger_.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update internal state based on an event.
|
|
* @param e The most recent mouse or touch event.
|
|
*/
|
|
private updateFromEvent_(e: Event) {
|
|
// AnyDuringMigration because: Property 'clientY' does not exist on type
|
|
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
|
|
// on type 'Event'.
|
|
const currentXY = new Coordinate(
|
|
(e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY);
|
|
const changed = this.updateDragDelta_(currentXY);
|
|
// Exceeded the drag radius for the first time.
|
|
if (changed) {
|
|
this.updateIsDragging_();
|
|
Touch.longStop();
|
|
}
|
|
this.mostRecentEvent_ = e;
|
|
}
|
|
|
|
/**
|
|
* DO MATH to set currentDragDeltaXY_ based on the most recent mouse position.
|
|
* @param currentXY The most recent mouse/pointer position, in pixel units,
|
|
* with (0, 0) at the window's top left corner.
|
|
* @return 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 mouse/touch move 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_.
|
|
* @return True if a block is being dragged from the flyout.
|
|
*/
|
|
private updateIsDraggingFromFlyout_(): boolean {
|
|
if (!this.targetBlock_) {
|
|
return false;
|
|
}
|
|
if (!this.flyout_.isBlockCreatable(this.targetBlock_)) {
|
|
return false;
|
|
}
|
|
if (!this.flyout_.isScrollable() ||
|
|
this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)) {
|
|
// AnyDuringMigration because: Type 'WorkspaceSvg | null' is not
|
|
// assignable to type 'WorkspaceSvg'.
|
|
this.startWorkspace_ = this.flyout_.targetWorkspace as AnyDuringMigration;
|
|
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.
|
|
// AnyDuringMigration because: Type 'null' is not assignable to type
|
|
// 'BlockSvg'.
|
|
this.startBlock_ = null as AnyDuringMigration;
|
|
this.targetBlock_ = this.flyout_.createBlock(this.targetBlock_);
|
|
this.targetBlock_.select();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether a bubble is being dragged.
|
|
* This function should be called on a mouse/touch move event the first time
|
|
* the drag radius is exceeded. It should be called no more than once per
|
|
* gesture. If a bubble should be dragged this function creates the necessary
|
|
* BubbleDragger and starts the drag.
|
|
* @return True if a bubble is being dragged.
|
|
*/
|
|
private updateIsDraggingBubble_(): boolean {
|
|
if (!this.startBubble_) {
|
|
return false;
|
|
}
|
|
|
|
this.isDraggingBubble_ = true;
|
|
this.startDraggingBubble_();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether a block is being dragged.
|
|
* This function should be called on a mouse/touch move 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, either from the flyout or in the
|
|
* workspace, this function creates the necessary BlockDragger and starts the
|
|
* drag.
|
|
* @return True if a block is being dragged.
|
|
*/
|
|
private updateIsDraggingBlock_(): boolean {
|
|
if (!this.targetBlock_) {
|
|
return false;
|
|
}
|
|
|
|
if (this.flyout_) {
|
|
this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_();
|
|
} else if (this.targetBlock_.isMovable()) {
|
|
this.isDraggingBlock_ = true;
|
|
}
|
|
|
|
if (this.isDraggingBlock_) {
|
|
this.startDraggingBlock_();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether a workspace is being dragged.
|
|
* This function should be called on a mouse/touch move 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_() {
|
|
const wsMovable = this.flyout_ ?
|
|
this.flyout_.isScrollable() :
|
|
this.startWorkspace_ && this.startWorkspace_.isDraggable();
|
|
|
|
if (!wsMovable) {
|
|
return;
|
|
}
|
|
|
|
this.workspaceDragger_ = new WorkspaceDragger((this.startWorkspace_));
|
|
|
|
this.isDraggingWorkspace_ = true;
|
|
this.workspaceDragger_.startDrag();
|
|
}
|
|
|
|
/**
|
|
* Update this gesture to record whether anything is being dragged.
|
|
* This function should be called on a mouse/touch move event the first time
|
|
* the drag radius is exceeded. It should be called no more than once per
|
|
* gesture.
|
|
*/
|
|
private updateIsDragging_() {
|
|
// Sanity check.
|
|
if (this.calledUpdateIsDragging_) {
|
|
throw Error('updateIsDragging_ should only be called once per gesture.');
|
|
}
|
|
this.calledUpdateIsDragging_ = true;
|
|
|
|
// First check if it was a bubble drag. Bubbles always sit on top of
|
|
// blocks.
|
|
if (this.updateIsDraggingBubble_()) {
|
|
return;
|
|
}
|
|
// Then check if it was a block drag.
|
|
if (this.updateIsDraggingBlock_()) {
|
|
return;
|
|
}
|
|
// Then check if it's a workspace drag.
|
|
this.updateIsDraggingWorkspace_();
|
|
}
|
|
|
|
/** Create a block dragger and start dragging the selected block. */
|
|
private startDraggingBlock_() {
|
|
const BlockDraggerClass = registry.getClassFromOptions(
|
|
registry.Type.BLOCK_DRAGGER, this.creatorWorkspace.options, true);
|
|
|
|
this.blockDragger_ =
|
|
new BlockDraggerClass!((this.targetBlock_), (this.startWorkspace_));
|
|
this.blockDragger_!.startDrag(this.currentDragDeltaXY_, this.healStack_);
|
|
this.blockDragger_!.drag(this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
}
|
|
|
|
// TODO (fenichel): Possibly combine this and startDraggingBlock_.
|
|
/** Create a bubble dragger and start dragging the selected bubble. */
|
|
private startDraggingBubble_() {
|
|
this.bubbleDragger_ =
|
|
new BubbleDragger((this.startBubble_), (this.startWorkspace_));
|
|
this.bubbleDragger_.startBubbleDrag();
|
|
this.bubbleDragger_.dragBubble(
|
|
this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
}
|
|
|
|
/**
|
|
* Start a gesture: update the workspace to indicate that a gesture is in
|
|
* progress and bind mousemove and mouseup handlers.
|
|
* @param e A mouse down or touch start event.
|
|
* @internal
|
|
*/
|
|
doStart(e: Event) {
|
|
if (browserEvents.isTargetInput(e)) {
|
|
this.cancel();
|
|
return;
|
|
}
|
|
this.hasStarted_ = 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();
|
|
}
|
|
// 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 (this.targetBlock_) {
|
|
this.targetBlock_.select();
|
|
}
|
|
|
|
if (browserEvents.isRightButton(e)) {
|
|
this.handleRightClick(e);
|
|
return;
|
|
}
|
|
|
|
// TODO(#6097): Make types accurate, possibly by refactoring touch handling.
|
|
const typelessEvent = e as AnyDuringMigration;
|
|
if ((e.type.toLowerCase() === 'touchstart' ||
|
|
e.type.toLowerCase() === 'pointerdown') &&
|
|
typelessEvent.pointerType !== 'mouse') {
|
|
Touch.longStart(typelessEvent, this);
|
|
}
|
|
|
|
// AnyDuringMigration because: Property 'clientY' does not exist on type
|
|
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
|
|
// on type 'Event'.
|
|
this.mouseDownXY_ = new Coordinate(
|
|
(e as AnyDuringMigration).clientX, (e as AnyDuringMigration).clientY);
|
|
// AnyDuringMigration because: Property 'metaKey' does not exist on type
|
|
// 'Event'. AnyDuringMigration because: Property 'ctrlKey' does not exist
|
|
// on type 'Event'. AnyDuringMigration because: Property 'altKey' does not
|
|
// exist on type 'Event'.
|
|
this.healStack_ = (e as AnyDuringMigration).altKey ||
|
|
(e as AnyDuringMigration).ctrlKey || (e as AnyDuringMigration).metaKey;
|
|
|
|
this.bindMouseEvents(e);
|
|
}
|
|
|
|
/**
|
|
* Bind gesture events.
|
|
* @param e A mouse down or touch start event.
|
|
* @internal
|
|
*/
|
|
bindMouseEvents(e: Event) {
|
|
this.onMoveWrapper_ = browserEvents.conditionalBind(
|
|
document, 'mousemove', null, this.handleMove.bind(this));
|
|
this.onUpWrapper_ = browserEvents.conditionalBind(
|
|
document, 'mouseup', null, this.handleUp.bind(this));
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* Handle a mouse move or touch move event.
|
|
* @param e A mouse move or touch move event.
|
|
* @internal
|
|
*/
|
|
handleMove(e: Event) {
|
|
this.updateFromEvent_(e);
|
|
if (this.isDraggingWorkspace_) {
|
|
this.workspaceDragger_.drag(this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBlock_) {
|
|
this.blockDragger_!.drag(this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBubble_) {
|
|
this.bubbleDragger_.dragBubble(
|
|
this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* Handle a mouse up or touch end event.
|
|
* @param e A mouse up or touch end event.
|
|
* @internal
|
|
*/
|
|
handleUp(e: Event) {
|
|
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 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.isDraggingBubble_) {
|
|
this.bubbleDragger_.endBubbleDrag(e, this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBlock_) {
|
|
this.blockDragger_!.endDrag(e, this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingWorkspace_) {
|
|
this.workspaceDragger_.endDrag(this.currentDragDeltaXY_);
|
|
} else if (this.isBubbleClick_()) {
|
|
// Bubbles are in front of all fields and blocks.
|
|
this.doBubbleClick_();
|
|
} else if (this.isFieldClick_()) {
|
|
this.doFieldClick_();
|
|
} else if (this.isBlockClick_()) {
|
|
this.doBlockClick_();
|
|
} else if (this.isWorkspaceClick_()) {
|
|
this.doWorkspaceClick_(e);
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.dispose();
|
|
}
|
|
|
|
/**
|
|
* 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.isDraggingBubble_) {
|
|
this.bubbleDragger_.endBubbleDrag(
|
|
this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBlock_) {
|
|
this.blockDragger_!.endDrag(
|
|
this.mostRecentEvent_, this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingWorkspace_) {
|
|
this.workspaceDragger_.endDrag(this.currentDragDeltaXY_);
|
|
}
|
|
this.dispose();
|
|
}
|
|
|
|
/**
|
|
* Handle a real or faked right-click event by showing a context menu.
|
|
* @param e A mouse move or touch move event.
|
|
* @internal
|
|
*/
|
|
handleRightClick(e: Event) {
|
|
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 mousedown/touchstart event on a workspace.
|
|
* @param e A mouse down or touch start event.
|
|
* @param ws The workspace the event hit.
|
|
* @internal
|
|
*/
|
|
handleWsStart(e: Event, ws: WorkspaceSvg) {
|
|
if (this.hasStarted_) {
|
|
throw Error(
|
|
'Tried to call gesture.handleWsStart, ' +
|
|
'but the gesture had already been started.');
|
|
}
|
|
this.setStartWorkspace_(ws);
|
|
this.mostRecentEvent_ = e;
|
|
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 mousedown/touchstart event on a flyout.
|
|
* @param e A mouse down or touch start event.
|
|
* @param flyout The flyout the event hit.
|
|
* @internal
|
|
*/
|
|
handleFlyoutStart(e: Event, flyout: IFlyout) {
|
|
if (this.hasStarted_) {
|
|
throw Error(
|
|
'Tried to call gesture.handleFlyoutStart, ' +
|
|
'but the gesture had already been started.');
|
|
}
|
|
this.setStartFlyout_(flyout);
|
|
this.handleWsStart(e, flyout.getWorkspace());
|
|
}
|
|
|
|
/**
|
|
* Handle a mousedown/touchstart event on a block.
|
|
* @param e A mouse down or touch start event.
|
|
* @param block The block the event hit.
|
|
* @internal
|
|
*/
|
|
handleBlockStart(e: Event, block: BlockSvg) {
|
|
if (this.hasStarted_) {
|
|
throw Error(
|
|
'Tried to call gesture.handleBlockStart, ' +
|
|
'but the gesture had already been started.');
|
|
}
|
|
this.setStartBlock(block);
|
|
this.mostRecentEvent_ = e;
|
|
}
|
|
|
|
/**
|
|
* Handle a mousedown/touchstart event on a bubble.
|
|
* @param e A mouse down or touch start event.
|
|
* @param bubble The bubble the event hit.
|
|
* @internal
|
|
*/
|
|
handleBubbleStart(e: Event, bubble: IBubble) {
|
|
if (this.hasStarted_) {
|
|
throw Error(
|
|
'Tried to call gesture.handleBubbleStart, ' +
|
|
'but the gesture had already been started.');
|
|
}
|
|
this.setStartBubble(bubble);
|
|
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 bubble click. */
|
|
private doBubbleClick_() {
|
|
// TODO (#1673): Consistent handling of single clicks.
|
|
if (this.startBubble_ instanceof WorkspaceCommentSvg) {
|
|
this.startBubble_.setFocus();
|
|
this.startBubble_.select();
|
|
}
|
|
}
|
|
|
|
/** Execute a field click. */
|
|
private doFieldClick_() {
|
|
this.startField_.showEditor(this.mostRecentEvent_);
|
|
this.bringBlockToFront_();
|
|
}
|
|
|
|
/** Execute a block click. */
|
|
private doBlockClick_() {
|
|
// Block click in an autoclosing flyout.
|
|
if (this.flyout_ && this.flyout_.autoClose) {
|
|
if (this.targetBlock_.isEnabled()) {
|
|
if (!eventUtils.getGroup()) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
const newBlock = this.flyout_.createBlock(this.targetBlock_);
|
|
newBlock.scheduleSnapAndBump();
|
|
}
|
|
} else {
|
|
// 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 mouse up or touch end event.
|
|
*/
|
|
private doWorkspaceClick_(_e: Event) {
|
|
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 mouse down. */
|
|
|
|
/**
|
|
* Record the field that a gesture started on.
|
|
* @param field The field the gesture started on.
|
|
* @internal
|
|
*/
|
|
setStartField(field: Field) {
|
|
if (this.hasStarted_) {
|
|
throw Error(
|
|
'Tried to call gesture.setStartField, ' +
|
|
'but the gesture had already been started.');
|
|
}
|
|
if (!this.startField_) {
|
|
this.startField_ = field;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 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;
|
|
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()) {
|
|
// AnyDuringMigration because: Argument of type 'BlockSvg | null' is not
|
|
// assignable to parameter of type 'BlockSvg'.
|
|
this.setTargetBlock_(block.getParent() as AnyDuringMigration);
|
|
} 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 mouse down. */
|
|
|
|
/* 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 (mouse up, touch end).
|
|
* @return 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 (mouse up, touch end).
|
|
* @return 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_();
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is a click on a field. This should only be called
|
|
* when ending a gesture (mouse up, touch end).
|
|
* @return Whether this gesture was a click on a field.
|
|
*/
|
|
private isFieldClick_(): boolean {
|
|
const fieldClickable =
|
|
this.startField_ ? this.startField_.isClickable() : false;
|
|
return fieldClickable && !this.hasExceededDragRadius_ &&
|
|
(!this.flyout_ || !this.flyout_.autoClose);
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture is a click on a workspace. This should only be called
|
|
* when ending a gesture (mouse up, touch end).
|
|
* @return 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).
|
|
* @return True if this gesture is a drag of a workspace or block.
|
|
* @internal
|
|
*/
|
|
isDragging(): boolean {
|
|
return this.isDraggingWorkspace_ || this.isDraggingBlock_ ||
|
|
this.isDraggingBubble_;
|
|
}
|
|
|
|
/**
|
|
* Whether this gesture has already been started. In theory every mouse down
|
|
* has a corresponding mouse up, but in reality it is possible to lose a
|
|
* mouse up, leaving an in-process gesture hanging.
|
|
* @return Whether this gesture was a click on a workspace.
|
|
* @internal
|
|
*/
|
|
hasStarted(): boolean {
|
|
return this.hasStarted_;
|
|
}
|
|
|
|
/**
|
|
* Get a list of the insertion markers that currently exist. Block drags have
|
|
* 0, 1, or 2 insertion markers.
|
|
* @return A possibly empty list of insertion marker blocks.
|
|
* @internal
|
|
*/
|
|
getInsertionMarkers(): BlockSvg[] {
|
|
if (this.blockDragger_) {
|
|
return this.blockDragger_.getInsertionMarkers();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Gets the current dragger if an item is being dragged. Null if nothing is
|
|
* being dragged.
|
|
* @return The dragger that is currently in use or null if no drag is in
|
|
* progress.
|
|
*/
|
|
getCurrentDragger(): WorkspaceDragger|BubbleDragger|IBlockDragger|null {
|
|
if (this.isDraggingBlock_) {
|
|
return this.blockDragger_;
|
|
} else if (this.isDraggingWorkspace_) {
|
|
return this.workspaceDragger_;
|
|
} else if (this.isDraggingBubble_) {
|
|
return this.bubbleDragger_;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Is a drag or other gesture currently in progress on any workspace?
|
|
* @return 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;
|
|
}
|
|
}
|