mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-05 17:34:55 -04:00
1011 lines
30 KiB
JavaScript
1011 lines
30 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2017 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview The class representing an in-progress gesture, usually a drag
|
|
* or a tap.
|
|
* @author fenichel@google.com (Rachel Fenichel)
|
|
*/
|
|
'use strict';
|
|
|
|
goog.provide('Blockly.Gesture');
|
|
|
|
goog.require('Blockly.BlockAnimations');
|
|
goog.require('Blockly.BlockDragger');
|
|
goog.require('Blockly.BubbleDragger');
|
|
goog.require('Blockly.constants');
|
|
goog.require('Blockly.Events.Ui');
|
|
goog.require('Blockly.FlyoutDragger');
|
|
goog.require('Blockly.scratchBlocksUtils');
|
|
goog.require('Blockly.Tooltip');
|
|
goog.require('Blockly.Touch');
|
|
goog.require('Blockly.WorkspaceDragger');
|
|
|
|
goog.require('goog.asserts');
|
|
goog.require('goog.math.Coordinate');
|
|
|
|
|
|
/*
|
|
* 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.
|
|
* @param {!Event} e The event that kicked off this gesture.
|
|
* @param {!Blockly.WorkspaceSvg} creatorWorkspace The workspace that created
|
|
* this gesture and has a reference to it.
|
|
* @constructor
|
|
*/
|
|
Blockly.Gesture = function(e, creatorWorkspace) {
|
|
|
|
/**
|
|
* 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).
|
|
* @type {goog.math.Coordinate}
|
|
*/
|
|
this.mouseDownXY_ = null;
|
|
|
|
/**
|
|
* How far the mouse has moved during this drag, in pixel units.
|
|
* (0, 0) is at this.mouseDownXY_.
|
|
* @type {goog.math.Coordinate}
|
|
* @private
|
|
*/
|
|
this.currentDragDeltaXY_ = null;
|
|
|
|
/**
|
|
* The bubble that the gesture started on, or null if it did not start on a
|
|
* bubble.
|
|
* @type {Blockly.Bubble}
|
|
* @private
|
|
*/
|
|
this.startBubble_ = null;
|
|
|
|
/**
|
|
* The field that the gesture started on, or null if it did not start on a
|
|
* field.
|
|
* @type {Blockly.Field}
|
|
* @private
|
|
*/
|
|
this.startField_ = null;
|
|
|
|
/**
|
|
* The block that the gesture started on, or null if it did not start on a
|
|
* block.
|
|
* @type {Blockly.BlockSvg}
|
|
* @private
|
|
*/
|
|
this.startBlock_ = 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.
|
|
* @type {Blockly.BlockSvg}
|
|
* @private
|
|
*/
|
|
this.targetBlock_ = null;
|
|
|
|
/**
|
|
* The workspace that the gesture started on. There may be multiple
|
|
* workspaces on a page; this is more accurate than using
|
|
* Blockly.getMainWorkspace().
|
|
* @type {Blockly.WorkspaceSvg}
|
|
* @private
|
|
*/
|
|
this.startWorkspace_ = null;
|
|
|
|
/**
|
|
* The workspace that created this gesture. This workspace keeps a reference
|
|
* to the gesture, which will need to be cleared at deletion.
|
|
* This may be different from the start workspace. For instance, a flyout is
|
|
* a workspace, but its parent workspace manages gestures for it.
|
|
* @type {Blockly.WorkspaceSvg}
|
|
* @private
|
|
*/
|
|
this.creatorWorkspace_ = creatorWorkspace;
|
|
|
|
/**
|
|
* 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.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.hasExceededDragRadius_ = false;
|
|
|
|
/**
|
|
* Whether the workspace is currently being dragged.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.isDraggingWorkspace_ = false;
|
|
|
|
/**
|
|
* Whether the block is currently being dragged.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.isDraggingBlock_ = false;
|
|
|
|
/**
|
|
* Whether the bubble is currently being dragged.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.isDraggingBubble_ = false;
|
|
|
|
/**
|
|
* The event that most recently updated this gesture.
|
|
* @type {!Event}
|
|
* @private
|
|
*/
|
|
this.mostRecentEvent_ = e;
|
|
|
|
/**
|
|
* A handle to use to unbind a mouse move listener at the end of a drag.
|
|
* Opaque data returned from Blockly.bindEventWithChecks_.
|
|
* @type {Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.onMoveWrapper_ = null;
|
|
|
|
/**
|
|
* A handle to use to unbind a mouse up listener at the end of a drag.
|
|
* Opaque data returned from Blockly.bindEventWithChecks_.
|
|
* @type {Array.<!Array>}
|
|
* @private
|
|
*/
|
|
this.onUpWrapper_ = null;
|
|
|
|
/**
|
|
* The object tracking a bubble drag, or null if none is in progress.
|
|
* @type {Blockly.BubbleDragger}
|
|
* @private
|
|
*/
|
|
this.bubbleDragger_ = null;
|
|
|
|
/**
|
|
* The object tracking a block drag, or null if none is in progress.
|
|
* @type {Blockly.BlockDragger}
|
|
* @private
|
|
*/
|
|
this.blockDragger_ = null;
|
|
|
|
/**
|
|
* The object tracking a workspace or flyout workspace drag, or null if none
|
|
* is in progress.
|
|
* @type {Blockly.WorkspaceDragger}
|
|
* @private
|
|
*/
|
|
this.workspaceDragger_ = null;
|
|
|
|
/**
|
|
* The flyout a gesture started in, if any.
|
|
* @type {Blockly.Flyout}
|
|
* @private
|
|
*/
|
|
this.flyout_ = null;
|
|
|
|
/**
|
|
* Boolean for sanity-checking that some code is only called once.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.calledUpdateIsDragging_ = false;
|
|
|
|
/**
|
|
* Boolean for sanity-checking that some code is only called once.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.hasStarted_ = false;
|
|
|
|
/**
|
|
* Boolean used internally to break a cycle in disposal.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.isEnding_ = false;
|
|
|
|
/**
|
|
* True if dragging from the target block should duplicate the target block
|
|
* and drag the duplicate instead. This has a lot of side effects.
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
this.shouldDuplicateOnDrag_ = false;
|
|
};
|
|
|
|
/**
|
|
* Sever all links from this object.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.dispose = function() {
|
|
Blockly.Touch.clearTouchIdentifier();
|
|
Blockly.Tooltip.unblock();
|
|
// Clear the owner's reference to this gesture.
|
|
this.creatorWorkspace_.clearGesture();
|
|
|
|
if (this.onMoveWrapper_) {
|
|
Blockly.unbindEvent_(this.onMoveWrapper_);
|
|
}
|
|
if (this.onUpWrapper_) {
|
|
Blockly.unbindEvent_(this.onUpWrapper_);
|
|
}
|
|
|
|
|
|
this.startField_ = null;
|
|
this.startBlock_ = null;
|
|
this.targetBlock_ = null;
|
|
this.startWorkspace_ = null;
|
|
this.flyout_ = null;
|
|
|
|
if (this.blockDragger_) {
|
|
this.blockDragger_.dispose();
|
|
this.blockDragger_ = null;
|
|
}
|
|
if (this.workspaceDragger_) {
|
|
this.workspaceDragger_.dispose();
|
|
this.workspaceDragger_ = null;
|
|
}
|
|
if (this.bubbleDragger_) {
|
|
this.bubbleDragger_.dispose();
|
|
this.bubbleDragger_ = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update internal state based on an event.
|
|
* @param {!Event} e The most recent mouse or touch event.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateFromEvent_ = function(e) {
|
|
var currentXY = new goog.math.Coordinate(e.clientX, e.clientY);
|
|
var changed = this.updateDragDelta_(currentXY);
|
|
// Exceeded the drag radius for the first time.
|
|
if (changed) {
|
|
this.updateIsDragging_();
|
|
Blockly.longStop_();
|
|
}
|
|
this.mostRecentEvent_ = e;
|
|
};
|
|
|
|
/**
|
|
* DO MATH to set currentDragDeltaXY_ based on the most recent mouse position.
|
|
* @param {!goog.math.Coordinate} currentXY The most recent mouse/pointer
|
|
* position, in pixel units, with (0, 0) at the window's top left corner.
|
|
* @return {boolean} True if the drag just exceeded the drag radius for the
|
|
* first time.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) {
|
|
this.currentDragDeltaXY_ = goog.math.Coordinate.difference(currentXY,
|
|
this.mouseDownXY_);
|
|
|
|
if (!this.hasExceededDragRadius_) {
|
|
var currentDragDelta = goog.math.Coordinate.magnitude(
|
|
this.currentDragDeltaXY_);
|
|
|
|
// The flyout has a different drag radius from the rest of Blockly.
|
|
var limitRadius = this.flyout_ ? Blockly.FLYOUT_DRAG_RADIUS :
|
|
Blockly.DRAG_RADIUS;
|
|
|
|
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 {boolean} True if a block is being dragged from the flyout.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() {
|
|
// Disabled blocks may not be dragged from the flyout.
|
|
if (this.targetBlock_.disabled) {
|
|
return false;
|
|
}
|
|
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 (!Blockly.Events.getGroup()) {
|
|
Blockly.Events.setGroup(true);
|
|
}
|
|
// The start block is no longer relevant, because this is a drag.
|
|
this.startBlock_ = null;
|
|
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 {boolean} true if a bubble is being dragged.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateIsDraggingBubble_ = function() {
|
|
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 {boolean} true if a block is being dragged.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateIsDraggingBlock_ = function() {
|
|
if (!this.targetBlock_) {
|
|
return false;
|
|
}
|
|
|
|
if (this.flyout_) {
|
|
this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_();
|
|
} else if (this.targetBlock_.isMovable() || this.shouldDuplicateOnDrag_){
|
|
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 or FlyoutDragger and starts the drag.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.updateIsDraggingWorkspace_ = function() {
|
|
var wsMovable = this.flyout_ ? this.flyout_.isScrollable() :
|
|
this.startWorkspace_ && this.startWorkspace_.isDraggable();
|
|
|
|
if (!wsMovable) {
|
|
return;
|
|
}
|
|
|
|
if (this.flyout_) {
|
|
this.workspaceDragger_ = new Blockly.FlyoutDragger(this.flyout_);
|
|
} else {
|
|
this.workspaceDragger_ = new Blockly.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
|
|
*/
|
|
Blockly.Gesture.prototype.updateIsDragging_ = function() {
|
|
// Sanity check.
|
|
goog.asserts.assert(!this.calledUpdateIsDragging_,
|
|
'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
|
|
*/
|
|
Blockly.Gesture.prototype.startDraggingBlock_ = function() {
|
|
if (this.shouldDuplicateOnDrag_) {
|
|
this.duplicateOnDrag_();
|
|
}
|
|
this.blockDragger_ = new Blockly.BlockDragger(this.targetBlock_,
|
|
this.startWorkspace_);
|
|
this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_);
|
|
this.blockDragger_.dragBlock(this.mostRecentEvent_,
|
|
this.currentDragDeltaXY_);
|
|
};
|
|
|
|
/**
|
|
* Create a bubble dragger and start dragging the selected bubble.
|
|
* TODO (fenichel): Possibly combine this and startDraggingBlock_.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.startDraggingBubble_ = function() {
|
|
this.bubbleDragger_ = new Blockly.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 {!Event} e A mouse down or touch start event.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.doStart = function(e) {
|
|
if (Blockly.utils.isTargetInput(e)) {
|
|
this.cancel();
|
|
return;
|
|
}
|
|
this.hasStarted_ = true;
|
|
|
|
Blockly.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();
|
|
}
|
|
this.startWorkspace_.markFocused();
|
|
this.mostRecentEvent_ = e;
|
|
|
|
// Hide chaff also hides the flyout, so don't do it if the click is in a flyout.
|
|
Blockly.hideChaff(!!this.flyout_);
|
|
Blockly.Tooltip.block();
|
|
|
|
if (this.targetBlock_) {
|
|
this.targetBlock_.select();
|
|
}
|
|
|
|
if (Blockly.utils.isRightButton(e)) {
|
|
this.handleRightClick(e);
|
|
return;
|
|
}
|
|
|
|
if (goog.string.caseInsensitiveEquals(e.type, 'touchstart')) {
|
|
Blockly.longStart_(e, this);
|
|
}
|
|
|
|
this.mouseDownXY_ = new goog.math.Coordinate(e.clientX, e.clientY);
|
|
this.currentDragDeltaXY_ = new goog.math.Coordinate(0, 0);
|
|
|
|
this.bindMouseEvents(e);
|
|
};
|
|
|
|
/**
|
|
* Bind gesture events.
|
|
* @param {!Event} e A mouse down or touch start event.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.bindMouseEvents = function(e) {
|
|
this.onMoveWrapper_ = Blockly.bindEventWithChecks_(
|
|
document, 'mousemove', null, this.handleMove.bind(this));
|
|
this.onUpWrapper_ = Blockly.bindEventWithChecks_(
|
|
document, 'mouseup', null, this.handleUp.bind(this));
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
/**
|
|
* Handle a mouse move or touch move event.
|
|
* @param {!Event} e A mouse move or touch move event.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleMove = function(e) {
|
|
var stopPropagation = true;
|
|
this.updateFromEvent_(e);
|
|
if (this.isDraggingWorkspace_) {
|
|
this.workspaceDragger_.drag(this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBlock_) {
|
|
if (this.blockDragger_.dragBlock(
|
|
this.mostRecentEvent_, this.currentDragDeltaXY_)) {
|
|
stopPropagation = false;
|
|
}
|
|
} else if (this.isDraggingBubble_) {
|
|
this.bubbleDragger_.dragBubble(this.mostRecentEvent_,
|
|
this.currentDragDeltaXY_);
|
|
}
|
|
|
|
if (stopPropagation) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle a mouse up or touch end event.
|
|
* @param {!Event} e A mouse up or touch end event.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleUp = function(e) {
|
|
this.updateFromEvent_(e);
|
|
Blockly.longStop_();
|
|
|
|
if (this.isEnding_) {
|
|
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_.endBlockDrag(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.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.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.cancel = function() {
|
|
// 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_) {
|
|
console.log('Trying to cancel a gesture recursively.');
|
|
return;
|
|
}
|
|
this.isEnding_ = true;
|
|
Blockly.longStop_();
|
|
if (this.isDraggingBubble_) {
|
|
this.bubbleDragger_.endBubbleDrag(this.mostRecentEvent_,
|
|
this.currentDragDeltaXY_);
|
|
} else if (this.isDraggingBlock_) {
|
|
this.blockDragger_.endBlockDrag(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 {!Event} e A mouse move or touch move event.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleRightClick = function(e) {
|
|
if (this.targetBlock_) {
|
|
this.bringBlockToFront_();
|
|
Blockly.hideChaff(this.flyout_);
|
|
this.targetBlock_.showContextMenu_(e);
|
|
} else if (this.startBubble_) {
|
|
this.startBubble_.showContextMenu_(e);
|
|
} else if (this.startWorkspace_ && !this.flyout_) {
|
|
Blockly.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 {!Event} e A mouse down or touch start event.
|
|
* @param {!Blockly.Workspace} ws The workspace the event hit.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleWsStart = function(e, ws) {
|
|
goog.asserts.assert(!this.hasStarted_,
|
|
'Tried to call gesture.handleWsStart, but the gesture had already been ' +
|
|
'started.');
|
|
this.setStartWorkspace_(ws);
|
|
this.mostRecentEvent_ = e;
|
|
this.doStart(e);
|
|
};
|
|
|
|
/**
|
|
* Handle a mousedown/touchstart event on a flyout.
|
|
* @param {!Event} e A mouse down or touch start event.
|
|
* @param {!Blockly.Flyout} flyout The flyout the event hit.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleFlyoutStart = function(e, flyout) {
|
|
goog.asserts.assert(!this.hasStarted_,
|
|
'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 {!Event} e A mouse down or touch start event.
|
|
* @param {!Blockly.BlockSvg} block The block the event hit.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleBlockStart = function(e, block) {
|
|
goog.asserts.assert(!this.hasStarted_,
|
|
'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 {!Event} e A mouse down or touch start event.
|
|
* @param {!Blockly.Bubble} bubble The bubble the event hit.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.handleBubbleStart = function(e, bubble) {
|
|
goog.asserts.assert(!this.hasStarted_,
|
|
'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
|
|
*/
|
|
Blockly.Gesture.prototype.doBubbleClick_ = function() {
|
|
// TODO (github.com/google/blockly/issues/1673): Consistent handling of single
|
|
// clicks.
|
|
this.startBubble_.setFocus && this.startBubble_.setFocus();
|
|
this.startBubble_.select && this.startBubble_.select();
|
|
};
|
|
|
|
/**
|
|
* Execute a field click.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.doFieldClick_ = function() {
|
|
this.startField_.showEditor_();
|
|
this.bringBlockToFront_();
|
|
};
|
|
|
|
/**
|
|
* Execute a block click.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.doBlockClick_ = function() {
|
|
// Block click in an autoclosing flyout.
|
|
if (this.flyout_ && this.flyout_.autoClose) {
|
|
if (!this.targetBlock_.disabled) {
|
|
if (!Blockly.Events.getGroup()) {
|
|
Blockly.Events.setGroup(true);
|
|
}
|
|
var newBlock = this.flyout_.createBlock(this.targetBlock_);
|
|
newBlock.scheduleSnapAndBump();
|
|
}
|
|
} else {
|
|
// A field is being edited if either the WidgetDiv or DropDownDiv is currently open.
|
|
// If a field is being edited, don't fire any click events.
|
|
var fieldEditing = Blockly.WidgetDiv.isVisible() || Blockly.DropDownDiv.isVisible();
|
|
if (!fieldEditing) {
|
|
Blockly.Events.fire(
|
|
new Blockly.Events.Ui(this.startBlock_, 'click', undefined, undefined));
|
|
// Scratch-specific: also fire a "stack click" event for this stack.
|
|
// This is used to toggle the stack when any block in the stack is clicked.
|
|
var rootBlock = this.startBlock_.getRootBlock();
|
|
Blockly.Events.fire(
|
|
new Blockly.Events.Ui(rootBlock, 'stackclick', undefined, undefined));
|
|
}
|
|
}
|
|
this.bringBlockToFront_();
|
|
Blockly.Events.setGroup(false);
|
|
};
|
|
|
|
/**
|
|
* Execute a workspace click.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.doWorkspaceClick_ = function() {
|
|
if (Blockly.selected) {
|
|
Blockly.selected.unselect();
|
|
}
|
|
};
|
|
|
|
/* 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
|
|
*/
|
|
Blockly.Gesture.prototype.bringBlockToFront_ = function() {
|
|
// 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 {Blockly.Field} field The field the gesture started on.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.setStartField = function(field) {
|
|
goog.asserts.assert(!this.hasStarted_,
|
|
'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 {Blockly.Bubble} bubble The bubble the gesture started on.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.setStartBubble = function(bubble) {
|
|
if (!this.startBubble_) {
|
|
this.startBubble_ = bubble;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Record the block that a gesture started on, and set the target block
|
|
* appropriately.
|
|
* @param {Blockly.BlockSvg} block The block the gesture started on.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.setStartBlock = function(block) {
|
|
// If the gesture already went through a bubble, don't set the start block.
|
|
if (!this.startBlock_ && !this.startBubble_) {
|
|
this.startBlock_ = block;
|
|
this.shouldDuplicateOnDrag_ =
|
|
Blockly.scratchBlocksUtils.isShadowArgumentReporter(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 {Blockly.BlockSvg} block The block the gesture targets.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.setTargetBlock_ = function(block) {
|
|
if (block.isShadow() && !this.shouldDuplicateOnDrag_) {
|
|
this.setTargetBlock_(block.getParent());
|
|
} else {
|
|
this.targetBlock_ = block;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Record the workspace that a gesture started on.
|
|
* @param {Blockly.WorkspaceSvg} ws The workspace the gesture started on.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.setStartWorkspace_ = function(ws) {
|
|
if (!this.startWorkspace_) {
|
|
this.startWorkspace_ = ws;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Record the flyout that a gesture started on.
|
|
* @param {Blockly.Flyout} flyout The flyout the gesture started on.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.setStartFlyout_ = function(flyout) {
|
|
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 {boolean} whether this gesture was a click on a bubble.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.isBubbleClick_ = function() {
|
|
// A bubble click starts on a bubble and never escapes the drag radius.
|
|
var 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 {boolean} whether this gesture was a click on a block.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.isBlockClick_ = function() {
|
|
// A block click starts on a block, never escapes the drag radius, and is not
|
|
// a field click.
|
|
var 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 {boolean} whether this gesture was a click on a field.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.isFieldClick_ = function() {
|
|
var fieldEditable = this.startField_ ?
|
|
this.startField_.isCurrentlyEditable() : false;
|
|
return fieldEditable && !this.hasExceededDragRadius_;
|
|
};
|
|
|
|
/**
|
|
* Whether this gesture is a click on a workspace. This should only be called
|
|
* when ending a gesture (mouse up, touch end).
|
|
* @return {boolean} whether this gesture was a click on a workspace.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.isWorkspaceClick_ = function() {
|
|
var 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 {boolean} true if this gesture is a drag of a workspace or block.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.isDragging = function() {
|
|
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 {boolean} whether this gesture was a click on a workspace.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.hasStarted = function() {
|
|
return this.hasStarted_;
|
|
};
|
|
|
|
/* Scratch-specific */
|
|
|
|
/**
|
|
* Don't even think about using this function before talking to rachel-fenichel.
|
|
*
|
|
* Force a drag to start without clicking and dragging the block itself. Used
|
|
* to attach duplicated blocks to the mouse pointer.
|
|
* @param {!Object} fakeEvent An object with the properties needed to start a
|
|
* drag, including clientX and clientY.
|
|
* @param {!Blockly.BlockSvg} block The block to start dragging.
|
|
* @package
|
|
*/
|
|
Blockly.Gesture.prototype.forceStartBlockDrag = function(fakeEvent, block) {
|
|
this.handleBlockStart(fakeEvent, block);
|
|
this.handleWsStart(fakeEvent, block.workspace);
|
|
this.isDraggingBlock_ = true;
|
|
this.hasExceededDragRadius_ = true;
|
|
this.startDraggingBlock_();
|
|
};
|
|
|
|
/**
|
|
* Duplicate the target block and start dragging the duplicated block.
|
|
* This should be done once we are sure that it is a block drag, and no earlier.
|
|
* Specifically for argument reporters in custom block defintions.
|
|
* @private
|
|
*/
|
|
Blockly.Gesture.prototype.duplicateOnDrag_ = function() {
|
|
var newBlock = null;
|
|
Blockly.Events.disable();
|
|
try {
|
|
// Note: targetBlock_ should have no children. If it has children we would
|
|
// need to update shadow block IDs to avoid problems in the VM.
|
|
// Resizes will be reenabled at the end of the drag.
|
|
this.startWorkspace_.setResizesEnabled(false);
|
|
var xmlBlock = Blockly.Xml.blockToDom(this.targetBlock_);
|
|
newBlock = Blockly.Xml.domToBlock(xmlBlock, this.startWorkspace_);
|
|
|
|
// Move the duplicate to original position.
|
|
var xy = this.targetBlock_.getRelativeToSurfaceXY();
|
|
newBlock.moveBy(xy.x, xy.y);
|
|
newBlock.setShadow(false);
|
|
} finally {
|
|
Blockly.Events.enable();
|
|
}
|
|
if (!newBlock) {
|
|
// Something went wrong.
|
|
console.error('Something went wrong while duplicating a block.');
|
|
return;
|
|
}
|
|
if (Blockly.Events.isEnabled()) {
|
|
Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock));
|
|
}
|
|
newBlock.select();
|
|
this.targetBlock_ = newBlock;
|
|
};
|