New dragging! Merge from google (#891)

Port of a major refactor from Blockly.  Dragging logic now lives in block_dragger.js, gesture.js, workspace_dragger.js, dragged_connection_manager.js (unused by scratch-blocks), and insertion_marker_manager.js (used only by scratch-blocks).
This commit is contained in:
Rachel Fenichel 2017-05-22 13:08:22 -07:00 committed by GitHub
parent 73630818c1
commit 6275e1137c
54 changed files with 3973 additions and 1473 deletions

View file

@ -142,6 +142,8 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
this.category_ = null;
/**
* The block's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
* @type {!goog.math.Coordinate}
* @private
*/
@ -339,7 +341,7 @@ Blockly.Block.prototype.getConnections_ = function() {
/**
* Walks down a stack of blocks and finds the last next connection on the stack.
* @return {Blockly.Connection} The last next connection on the stack, or null.
* @private
* @package
*/
Blockly.Block.prototype.lastConnectionInStack = function() {
var nextConnection = this.nextConnection;
@ -360,7 +362,6 @@ Blockly.Block.prototype.lastConnectionInStack = function() {
* connected should not coincidentally line up on screen.
* @private
*/
// TODO: Refactor to return early in headless mode.
Blockly.Block.prototype.bumpNeighbours_ = function() {
console.warn('Not expected to reach this bumpNeighbours_ function. The ' +
'BlockSvg function for bumpNeighbours_ was expected to be called instead.');
@ -1239,10 +1240,14 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) {
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (typeof token == 'number') {
goog.asserts.assert(token > 0 && token <= args.length,
'Message index %%s out of range.', token);
goog.asserts.assert(!indexDup[token],
'Message index %%s duplicated.', token);
if (token <= 0 || token > args.length) {
throw new Error('Block \"' + this.type + '\": ' +
'Message index %' + token + ' out of range.');
}
if (indexDup[token]) {
throw new Error('Block \"' + this.type + '\": ' +
'Message index %' + token + ' duplicated.');
}
indexDup[token] = true;
indexCount++;
elements.push(args[token - 1]);
@ -1253,8 +1258,10 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) {
}
}
}
goog.asserts.assert(indexCount == args.length,
'block "%s": Message does not reference all %s arg(s).', this.type, args.length);
if(indexCount != args.length) {
throw new Error('Block \"' + this.type + '\": ' +
'Message does not reference all ' + args.length + ' arg(s).');
}
// Add last dummy input if needed.
if (elements.length && (typeof elements[elements.length - 1] == 'string' ||
goog.string.startsWith(elements[elements.length - 1]['type'],

View file

@ -80,6 +80,15 @@ Blockly.BlockDragSurfaceSvg.prototype.container_ = null;
*/
Blockly.BlockDragSurfaceSvg.prototype.scale_ = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
* @type {goog.math.Coordinate}
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.surfaceXY_ = null;
/**
* Create the drag surface and inject it into the container.
*/
@ -109,6 +118,7 @@ Blockly.BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
this.surfaceXY_ = new goog.math.Coordinate(0, 0);
// This allows blocks to be dragged outside of the blockly svg space.
// This should be reset to hidden at the end of the block drag.
// Note that this behavior is different from blockly where block disappear
@ -118,10 +128,10 @@ Blockly.BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
};
/**
* Translate and scale the entire drag surface group to keep in sync with the
* workspace.
* @param {number} x X translation
* @param {number} y Y translation
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
* @param {number} x X translation in workspace coordinates.
* @param {number} y Y translation in workspace coordinates.
* @param {number} scale Scale of the group.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
@ -134,6 +144,23 @@ Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, sc
' scale(' + scale + ')');
};
/**
* Translate the drag surface's SVG based on its internal state.
* @private
*/
Blockly.BlockDragSurfaceSvg.prototype.translateSurfaceInternal_ = function() {
var x = this.surfaceXY_.x;
var y = this.surfaceXY_.y;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
this.SVG_.style.display = 'block';
Blockly.utils.setCssTransform(this.SVG_,
'translate3d(' + x + 'px, ' + y + 'px, 0px)');
};
/**
* Translate the entire drag surface during a drag.
* We translate the drag surface instead of the blocks inside the surface
@ -143,15 +170,8 @@ Blockly.BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, sc
* @param {number} y Y translation for the entire surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) {
x *= this.scale_;
y *= this.scale_;
// This is a work-around to prevent a the blocks from rendering
// fuzzy while they are being dragged on the drag surface.
x = x.toFixed(0);
y = y.toFixed(0);
this.SVG_.style.display = 'block';
Blockly.utils.setCssTransform(this.SVG_,
'translate3d(' + x + 'px, ' + y + 'px, 0px)');
this.surfaceXY_ = new goog.math.Coordinate(x * this.scale_, y * this.scale_);
this.translateSurfaceInternal_();
};
/**
@ -186,19 +206,28 @@ Blockly.BlockDragSurfaceSvg.prototype.getCurrentBlock = function() {
/**
* Clear the group and hide the surface; move the blocks off onto the provided
* element.
* @param {!Element} newSurface Surface the dragging blocks should be moved to.
* If the block is being deleted it doesn't need to go back to the original
* surface, since it would be removed immediately during dispose.
* @param {Element} opt_newSurface Surface the dragging blocks should be moved
* to, or null if the blocks should be removed from this surface without
* being moved to a different surface.
*/
Blockly.BlockDragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
// appendChild removes the node from this.dragGroup_
newSurface.appendChild(this.getCurrentBlock());
Blockly.BlockDragSurfaceSvg.prototype.clearAndHide = function(opt_newSurface) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
opt_newSurface.appendChild(this.getCurrentBlock());
} else {
this.dragGroup_.removeChild(this.getCurrentBlock());
}
this.SVG_.style.display = 'none';
goog.asserts.assert(this.dragGroup_.childNodes.length == 0,
'Drag group was not cleared.');
this.surfaceXY_ = null;
// Reset the overflow property back to hidden so that nothing appears outside
// of the blockly area.
// Note that this behavior is different from blockly. See note in
// setBlocksAndShow.
var injectionDiv = document.getElementsByClassName('injectionDiv')[0];
injectionDiv.style.overflow = 'hidden';
goog.asserts.assert(this.dragGroup_.childNodes.length == 0,
'Drag group was not cleared.');
};

324
core/block_dragger.js Normal file
View file

@ -0,0 +1,324 @@
/**
* @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 Methods for dragging a block visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.BlockDragger');
goog.require('Blockly.InsertionMarkerManager');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
* @param {!Blockly.Block} block The block to drag.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on.
* @constructor
*/
Blockly.BlockDragger = function(block, workspace) {
/**
* The top block in the stack that is being dragged.
* @type {!Blockly.BlockSvg}
* @private
*/
this.draggingBlock_ = block;
/**
* The workspace on which the block is being dragged.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Object that keeps track of connections on dragged blocks.
* @type {!Blockly.InsertionMarkerManager}
* @private
*/
this.draggedConnectionManager_ = new Blockly.InsertionMarkerManager(
this.draggingBlock_);
/**
* Which delete area the mouse pointer is over, if any.
* One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @type {?number}
* @private
*/
this.deleteArea_ = null;
/**
* Whether the block would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
/**
* The location of the top left corner of the dragging block at the beginning
* of the drag in workspace coordinates.
* @type {!goog.math.Coordinate}
* @private
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
/**
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @type {Array.<!Object>}
* @private
*/
this.dragIconData_ = Blockly.BlockDragger.initIconData_(block);
};
/**
* Sever all links from this object.
* @package
*/
Blockly.BlockDragger.prototype.dispose = function() {
this.draggingBlock_ = null;
this.workspace_ = null;
this.startWorkspace_ = null;
this.dragIconData_.length = 0;
if (this.draggedConnectionManager_) {
this.draggedConnectionManager_.dispose();
this.draggedConnectionManager_ = null;
}
};
/**
* Make a list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @param {!Blockly.BlockSvg} block The root block that is being dragged.
* @return {!Array.<!Object>} The list of all icons and their locations.
* @private
*/
Blockly.BlockDragger.initIconData_ = function(block) {
// Build a list of icons that need to be moved and where they started.
var dragIconData = [];
var descendants = block.getDescendants();
for (var i = 0, descendant; descendant = descendants[i]; i++) {
var icons = descendant.getIcons();
for (var j = 0; j < icons.length; j++) {
var data = {
// goog.math.Coordinate with x and y properties (workspace coordinates).
location: icons[j].getIconLocation(),
// Blockly.Icon
icon: icons[j]
};
dragIconData.push(data);
}
}
return dragIconData;
};
/**
* Start dragging a block. This includes moving it to the drag surface.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @package
*/
Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY) {
if (!Blockly.Events.getGroup()) {
Blockly.Events.setGroup(true);
}
this.workspace_.setResizesEnabled(false);
Blockly.BlockSvg.disconnectUiStop_();
if (this.draggingBlock_.getParent()) {
this.draggingBlock_.unplug();
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.translate(newLoc.x, newLoc.y);
this.draggingBlock_.disconnectUiEffect();
}
this.draggingBlock_.setDragging(true);
// For future consideration: we may be able to put moveToDragSurface inside
// the block dragger, which would also let the block not track the block drag
// surface.
this.draggingBlock_.moveToDragSurface_();
if (this.workspace_.toolbox_) {
this.workspace_.toolbox_.addDeleteStyle();
}
};
/**
* Execute a step of block dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
Blockly.BlockDragger.prototype.dragBlock = function(e, currentDragDeltaXY) {
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
this.dragIcons_(delta);
this.deleteArea_ = this.workspace_.isDeleteArea(e);
this.draggedConnectionManager_.update(delta, this.deleteArea_);
this.updateCursorDuringBlockDrag_();
};
/**
* Finish a block drag and put the block back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
*/
Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) {
// Make sure internal state is fresh.
this.dragBlock(e, currentDragDeltaXY);
this.dragIconData_ = [];
Blockly.BlockSvg.disconnectUiStop_();
var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveOffDragSurface_(newLoc);
var deleted = this.maybeDeleteBlock_();
if (!deleted) {
// These are expensive and don't need to be done if we're deleting.
this.draggingBlock_.moveConnections_(delta.x, delta.y);
this.draggingBlock_.setDragging(false);
this.draggedConnectionManager_.applyConnections();
this.draggingBlock_.render();
this.fireMoveEvent_();
this.draggingBlock_.scheduleSnapAndBump();
}
this.workspace_.setResizesEnabled(true);
if (this.workspace_.toolbox_) {
this.workspace_.toolbox_.removeDeleteStyle();
}
Blockly.Events.setGroup(false);
};
/**
* Fire a move event at the end of a block drag.
* @private
*/
Blockly.BlockDragger.prototype.fireMoveEvent_ = function() {
var event = new Blockly.Events.Move(this.draggingBlock_);
event.oldCoordinate = this.startXY_;
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Shut the trash can and, if necessary, delete the dragging block.
* Should be called at the end of a block drag.
* @return {boolean} whether the block was deleted.
* @private
*/
Blockly.BlockDragger.prototype.maybeDeleteBlock_ = function() {
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBlock_) {
if (trashcan) {
goog.Timer.callOnce(trashcan.close, 100, trashcan);
}
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBlock_.dispose(false, true);
} else if (trashcan) {
// Make sure the trash can is closed.
trashcan.close();
}
return this.wouldDeleteBlock_;
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging block would be deleted if released immediately.
* @private
*/
Blockly.BlockDragger.prototype.updateCursorDuringBlockDrag_ = function() {
this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBlock_) {
this.draggingBlock_.setDeleteStyle(true);
if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) {
trashcan.setOpen_(true);
}
} else {
this.draggingBlock_.setDeleteStyle(false);
if (trashcan) {
trashcan.setOpen_(false);
}
}
};
/**
* Convert a coordinate object from pixels to workspace units, including a
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!goog.math.Coordinate} pixelCoord A coordinate with x and y values
* in css pixel units.
* @return {!goog.math.Coordinate} The input coordinate divided by the workspace
* scale.
* @private
*/
Blockly.BlockDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
var result = new goog.math.Coordinate(pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same as
// the scale on the parent workspace.
// Fix that for dragging.
var mainScale = this.workspace_.options.parentWorkspace.scale;
result = result.scale(1 / mainScale);
}
return result;
};
/**
* Move all of the icons connected to this drag.
* @param {!goog.math.Coordinate} dxy How far to move the icons from their
* original positions, in workspace units.
* @private
*/
Blockly.BlockDragger.prototype.dragIcons_ = function(dxy) {
// Moving icons moves their associated bubbles.
for (var i = 0; i < this.dragIconData_.length; i++) {
var data = this.dragIconData_[i];
data.icon.setIconLocation(goog.math.Coordinate.sum(data.location, dxy));
}
};

View file

@ -158,9 +158,6 @@ Blockly.BlockSvg.prototype.initSvg = function() {
if (!this.workspace.options.readOnly && !this.eventsInit_) {
Blockly.bindEventWithChecks_(this.getSvgRoot(), 'mousedown', this,
this.onMouseDown_);
var thisBlock = this;
Blockly.bindEvent_(this.getSvgRoot(), 'touchstart', null,
function(e) {Blockly.longStart_(e, thisBlock);});
}
this.eventsInit_ = true;
@ -294,79 +291,6 @@ Blockly.BlockSvg.prototype.getIcons = function() {
return icons;
};
/**
* Wrapper function called when a mouseUp occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.BlockSvg.onMouseUpWrapper_ = null;
/**
* Wrapper function called when a mouseMove occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.BlockSvg.onMouseMoveWrapper_ = null;
/**
* Stop binding to the global mouseup and mousemove events.
* @package
*/
Blockly.BlockSvg.terminateDrag = function() {
if (Blockly.BlockSvg.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_);
Blockly.BlockSvg.onMouseUpWrapper_ = null;
}
if (Blockly.BlockSvg.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.BlockSvg.onMouseMoveWrapper_);
Blockly.BlockSvg.onMouseMoveWrapper_ = null;
}
var selected = Blockly.selected;
if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
// Terminate a drag operation.
if (selected) {
if (Blockly.replacementMarker_) {
Blockly.BlockSvg.removeReplacementMarker();
} else if (Blockly.insertionMarker_) {
Blockly.Events.disable();
if (Blockly.insertionMarkerConnection_) {
Blockly.BlockSvg.disconnectInsertionMarker();
}
Blockly.insertionMarker_.dispose();
Blockly.insertionMarker_ = null;
Blockly.Events.enable();
}
// Update the connection locations.
var xy = selected.getRelativeToSurfaceXY();
var dxy = goog.math.Coordinate.difference(xy, selected.dragStartXY_);
var event = new Blockly.Events.Move(selected);
event.oldCoordinate = selected.dragStartXY_;
event.recordNew();
Blockly.Events.fire(event);
selected.moveConnections_(dxy.x, dxy.y);
delete selected.draggedBubbles_;
selected.setDragging_(false);
selected.moveOffDragSurface_();
selected.render();
// Re-enable workspace resizing.
selected.workspace.setResizesEnabled(true);
// Ensure that any snap and bump are part of this move's event group.
var group = Blockly.Events.getGroup();
setTimeout(function() {
Blockly.Events.setGroup(group);
selected.snapToGrid();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY / 2);
setTimeout(function() {
Blockly.Events.setGroup(group);
selected.bumpNeighbours_();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY);
}
}
Blockly.dragMode_ = Blockly.DRAG_NONE;
};
/**
* Set parent of this block to be a new block or null.
* @param {Blockly.BlockSvg} newParent New parent block.
@ -408,6 +332,8 @@ Blockly.BlockSvg.prototype.setParent = function(newParent) {
/**
* Return the coordinates of the top-left corner of this block relative to the
* drawing surface's origin (0,0), in workspace units.
* If the block is on the workspace, (0, 0) is the origin of the workspace
* coordinate system.
* This does not change with workspace scale.
* @return {!goog.math.Coordinate} Object with .x and .y properties in
* workspace coordinates.
@ -488,6 +414,7 @@ Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
// This is in workspace coordinates.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y);
@ -499,19 +426,37 @@ Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
* Move this block back to the workspace block canvas.
* Generally should be called at the same time as setDragging_(false).
* Does nothing if useDragSurface_ is false.
* @param {!goog.math.Coordinate} newXY The position the block should take on
* on the workspace canvas, in workspace coordinates.
* @private
*/
Blockly.BlockSvg.prototype.moveOffDragSurface_ = function() {
Blockly.BlockSvg.prototype.moveOffDragSurface_ = function(newXY) {
if (!this.useDragSurface_) {
return;
}
// Translate to current position, turning off 3d.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.translate(xy.x, xy.y);
this.translate(newXY.x, newXY.y);
this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
};
/**
* Move this block during a drag, taking into account whether we are using a
* drag surface to translate blocks.
* This block must be a top-level block.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.BlockSvg.prototype.moveDuringDrag = function(newLoc) {
if (this.useDragSurface_) {
this.workspace.blockDragSurface_.translateSurface(newLoc.x, newLoc.y);
} else {
this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')';
this.svgGroup_.setAttribute('transform',
this.svgGroup_.translate_ + this.svgGroup_.skew_);
}
};
/**
* Clear the block of transform="..." attributes.
* Used when the block is switching from 3d to 2d transform or vice versa.
@ -528,7 +473,7 @@ Blockly.BlockSvg.prototype.snapToGrid = function() {
if (!this.workspace) {
return; // Deleted block.
}
if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
if (this.workspace.isDragging()) {
return; // Don't bump blocks during a drag.
}
if (this.getParent()) {
@ -556,6 +501,7 @@ Blockly.BlockSvg.prototype.snapToGrid = function() {
/**
* Returns the coordinates of a bounding box describing the dimensions of this
* block and any blocks stacked below it.
* Coordinate system: workspace coordinates.
* @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
* Object with top left and bottom right coordinates of the bounding box.
*/
@ -689,139 +635,9 @@ Blockly.BlockSvg.prototype.tab = function(start, forward) {
* @private
*/
Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
if (this.workspace.options.readOnly) {
return;
}
if (this.isInFlyout) {
// longStart's simulation of right-clicks for longpresses on touch devices
// calls the onMouseDown_ function defined on the prototype of the object
// the was longpressed (in this case, a Blockly.BlockSvg). In this case
// that behaviour is wrong, because Blockly.Flyout.prototype.blockMouseDown
// should be called for a mousedown on a block in the flyout, which blocks
// execution of the block's onMouseDown_ function.
if (e.type == 'touchstart' && Blockly.utils.isRightButton(e)) {
Blockly.Flyout.blockRightClick_(e, this);
e.stopPropagation();
e.preventDefault();
}
return;
}
if (this.isInMutator) {
// Mutator's coordinate system could be out of date because the bubble was
// dragged, the block was moved, the parent workspace zoomed, etc.
this.workspace.resize();
}
this.workspace.updateScreenCalculationsIfScrolled();
this.workspace.markFocused();
Blockly.terminateDrag_();
this.select();
Blockly.hideChaff();
Blockly.DropDownDiv.hideWithoutAnimation();
if (Blockly.utils.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
// Click, not drag, so stop waiting for other touches from this identifier.
Blockly.Touch.clearTouchIdentifier();
} else if (!this.isMovable()) {
// Allow immovable blocks to be selected and context menued, but not
// dragged. Let this event bubble up to document, so the workspace may be
// dragged instead.
return;
} else {
if (!Blockly.Events.getGroup()) {
Blockly.Events.setGroup(true);
}
// Left-click (or middle click)
this.dragStartXY_ = this.getRelativeToSurfaceXY();
this.workspace.startDrag(e, this.dragStartXY_);
Blockly.dragMode_ = Blockly.DRAG_STICKY;
Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
'mouseup', this, this.onMouseUp_);
Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(
document, 'mousemove', this, this.onMouseMove_);
// Build a list of bubbles that need to be moved and where they started.
this.draggedBubbles_ = [];
var descendants = this.getDescendants();
for (var i = 0, descendant; descendant = descendants[i]; i++) {
var icons = descendant.getIcons();
for (var j = 0; j < icons.length; j++) {
var data = icons[j].getIconLocation();
data.bubble = icons[j];
this.draggedBubbles_.push(data);
}
}
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
e.preventDefault();
};
/**
* Handle a mouse-up anywhere in the SVG pane. Is only registered when a
* block is clicked. We can't use mouseUp on the block since a fast-moving
* cursor can briefly escape the block before it catches up.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
// 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();
Blockly.Touch.clearTouchIdentifier();
if (Blockly.dragMode_ != Blockly.DRAG_FREE && !fieldEditing) {
// Move the block in front of the others. Do this at the end of a click
// instead of rearranging the dom on mousedown. This helps with
// performance and makes it easier to use psuedo element :active
// to set the cursor.
this.bringToFront_();
Blockly.Events.fire(
new Blockly.Events.Ui(this, '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.workspace.getBlockById(this.id).getRootBlock();
Blockly.Events.fire(
new Blockly.Events.Ui(rootBlock, 'stackclick', undefined, undefined));
}
Blockly.terminateDrag_();
var deleteArea = this.workspace.isDeleteArea(e);
// Connect to a nearby block, but not if it's over the toolbox.
if (Blockly.selected && Blockly.highlightedConnection_ &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX) {
// Connect two blocks together.
Blockly.localConnection_.connect(Blockly.highlightedConnection_);
if (this.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection = Blockly.localConnection_.isSuperior() ?
Blockly.highlightedConnection_ : Blockly.localConnection_;
inferiorConnection.getSourceBlock().connectionUiEffect();
}
if (this.workspace.trashcan) {
// Don't throw an object in the trash can if it just got connected.
this.workspace.trashcan.close();
}
} else if (deleteArea && !this.getParent() && Blockly.selected.isDeletable()) {
// We didn't connect the block, and it was over the trash can or the
// toolbox. Delete it.
var trashcan = this.workspace.trashcan;
if (trashcan) {
goog.Timer.callOnce(trashcan.close, 100, trashcan);
}
if (this.workspace.toolbox_) {
this.workspace.toolbox_.removeDeleteStyle();
}
Blockly.selected.dispose(false, true);
}
if (Blockly.highlightedConnection_) {
Blockly.highlightedConnection_ = null;
}
if (!Blockly.WidgetDiv.isVisible()) {
Blockly.Events.setGroup(false);
var gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleBlockStart(e, this);
}
};
@ -953,9 +769,9 @@ Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) {
/**
* Recursively adds or removes the dragging class to this node and its children.
* @param {boolean} adding True if adding, false if removing.
* @private
* @package
*/
Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
Blockly.BlockSvg.prototype.setDragging = function(adding) {
if (adding) {
var group = this.getSvgRoot();
group.translate_ = '';
@ -971,379 +787,7 @@ Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
}
// Recurse through all blocks attached under this one.
for (var i = 0; i < this.childBlocks_.length; i++) {
this.childBlocks_[i].setDragging_(adding);
}
};
/**
* Drag this block to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 &&
e.button == 0) {
/* HACK:
Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove
events on certain touch actions. Ignore events with these signatures.
This may result in a one-pixel blind spot in other browsers,
but this shouldn't be noticeable. */
e.stopPropagation();
return;
}
var oldXY = this.getRelativeToSurfaceXY();
var newXY = this.workspace.moveDrag(e);
if (Blockly.dragMode_ == Blockly.DRAG_STICKY) {
// Still dragging within the sticky DRAG_RADIUS.
var dr = goog.math.Coordinate.distance(oldXY, newXY) * this.workspace.scale;
if (dr > Blockly.DRAG_RADIUS) {
// Switch to unrestricted dragging.
Blockly.dragMode_ = Blockly.DRAG_FREE;
Blockly.longStop_();
// Disable workspace resizing as an optimization.
this.workspace.setResizesEnabled(false);
// Clear WidgetDiv/DropDownDiv without animating, in case blocks are moved
// around
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
if (this.parentBlock_) {
// Push this block to the very top of the stack.
this.unplug();
}
this.setDragging_(true);
this.moveToDragSurface_();
}
}
if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
this.handleDragFree_(oldXY, newXY, e);
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
e.preventDefault();
};
/**
* Handle a mouse movement when a block is already freely dragging.
* @param {!goog.math.Coordinate} oldXY The position of the block on screen
* before the most recent mouse movement.
* @param {!goog.math.Coordinate} newXY The new location after applying the
* mouse movement.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.BlockSvg.prototype.handleDragFree_ = function(oldXY, newXY, e) {
var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_);
var group = this.getSvgRoot();
if (this.useDragSurface_) {
this.workspace.blockDragSurface_.translateSurface(newXY.x, newXY.y);
} else {
group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')';
group.setAttribute('transform', group.translate_ + group.skew_);
}
// Drag all the nested bubbles.
for (var i = 0; i < this.draggedBubbles_.length; i++) {
var commentData = this.draggedBubbles_[i];
commentData.bubble.setIconLocation(
goog.math.Coordinate.sum(commentData, dxy));
}
// Check to see if any of this block's connections are within range of
// another block's connection.
var myConnections = this.getConnections_(false);
// Also check the last connection on this stack
var lastOnStack = this.lastConnectionInStack();
if (lastOnStack && lastOnStack != this.nextConnection) {
myConnections.push(lastOnStack);
}
var closestConnection = null;
var localConnection = null;
var radiusConnection = Blockly.SNAP_RADIUS;
// If there is already a connection highlighted,
// increase the radius we check for making new connections.
// Why? When a connection is highlighted, blocks move around when the insertion
// marker is created, which could cause the connection became out of range.
// By increasing radiusConnection when a connection already exists,
// we never "lose" the connection from the offset.
if (Blockly.localConnection_ && Blockly.highlightedConnection_) {
radiusConnection = Blockly.CONNECTING_SNAP_RADIUS;
}
for (i = 0; i < myConnections.length; i++) {
var myConnection = myConnections[i];
var neighbour = myConnection.closest(radiusConnection, dxy);
if (neighbour.connection) {
closestConnection = neighbour.connection;
localConnection = myConnection;
radiusConnection = neighbour.radius;
}
}
var updatePreviews = true;
if (localConnection && localConnection.type == Blockly.OUTPUT_VALUE) {
updatePreviews = true; // Always update previews for output connections.
} else if (Blockly.localConnection_ && Blockly.highlightedConnection_) {
var xDiff = Blockly.localConnection_.x_ + dxy.x -
Blockly.highlightedConnection_.x_;
var yDiff = Blockly.localConnection_.y_ + dxy.y -
Blockly.highlightedConnection_.y_;
var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
// Slightly prefer the existing preview over a new preview.
if (closestConnection && radiusConnection > curDistance -
Blockly.CURRENT_CONNECTION_PREFERENCE) {
updatePreviews = false;
}
}
if (updatePreviews) {
var candidateIsLast = (localConnection == lastOnStack);
this.updatePreviews(closestConnection, localConnection, radiusConnection,
e, newXY.x - this.dragStartXY_.x, newXY.y - this.dragStartXY_.y,
candidateIsLast);
}
};
/**
* Preview the results of the drag if the mouse is released immediately.
* @param {Blockly.Connection} closestConnection The closest connection found
* during the search
* @param {Blockly.Connection} localConnection The connection on the moving
* block.
* @param {number} radiusConnection The distance between closestConnection and
* localConnection.
* @param {!Event} e Mouse move event.
* @param {number} dx The x distance the block has moved onscreen up to this
* point in the drag.
* @param {number} dy The y distance the block has moved onscreen up to this
* point in the drag.
* @param {boolean} candidateIsLast True if the dragging stack is more than one
* block long and localConnection is the last connection on the stack.
*/
Blockly.BlockSvg.prototype.updatePreviews = function(closestConnection,
localConnection, radiusConnection, e, dx, dy, candidateIsLast) {
// Don't fire events for insertion marker creation or movement.
Blockly.Events.disable();
// Remove an insertion marker if needed. For Scratch-Blockly we are using
// grayed-out blocks instead of highlighting the connection; for compatibility
// with Web Blockly the name "highlightedConnection" will still be used.
if (Blockly.highlightedConnection_ &&
Blockly.highlightedConnection_ != closestConnection) {
if (Blockly.replacementMarker_) {
Blockly.BlockSvg.removeReplacementMarker();
} else if (Blockly.insertionMarker_ && Blockly.insertionMarkerConnection_) {
Blockly.BlockSvg.disconnectInsertionMarker();
}
// If there's already an insertion marker but it's representing the wrong
// block, delete it so we can create the correct one.
if (Blockly.insertionMarker_ &&
((candidateIsLast && Blockly.localConnection_.sourceBlock_ == this) ||
(!candidateIsLast && Blockly.localConnection_.sourceBlock_ != this))) {
Blockly.insertionMarker_.dispose();
Blockly.insertionMarker_ = null;
}
Blockly.highlightedConnection_ = null;
Blockly.localConnection_ = null;
}
var wouldDeleteBlock = this.updateCursor_(e, closestConnection);
// Add an insertion marker or replacement marker if needed.
if (!wouldDeleteBlock && closestConnection &&
closestConnection != Blockly.highlightedConnection_ &&
!closestConnection.sourceBlock_.isInsertionMarker()) {
Blockly.highlightedConnection_ = closestConnection;
Blockly.localConnection_ = localConnection;
// Dragging a block over a nexisting block in an input should replace the
// existing block and bump it out. Similarly, dragging a terminal block
// over another (connected) terminal block will replace, not insert.
var shouldReplace = (localConnection.type == Blockly.OUTPUT_VALUE ||
(localConnection.type == Blockly.PREVIOUS_STATEMENT &&
closestConnection.isConnected() &&
!this.nextConnection));
if (shouldReplace) {
this.addReplacementMarker_(localConnection, closestConnection);
} else { // Should insert
this.connectInsertionMarker_(localConnection, closestConnection);
}
}
// Reenable events.
Blockly.Events.enable();
// Provide visual indication of whether the block will be deleted if
// dropped here.
if (this.isDeletable()) {
this.workspace.isDeleteArea(e);
}
};
/**
* Add highlighting showing which block will be replaced.
* @param {Blockly.Connection} localConnection The connection on the dragging
* block.
* @param {Blockly.Connection} closestConnection The connnection to pretend to
* connect to.
*/
Blockly.BlockSvg.prototype.addReplacementMarker_ = function(localConnection,
closestConnection) {
if (closestConnection.targetBlock()) {
Blockly.replacementMarker_ = closestConnection.targetBlock();
Blockly.replacementMarker_.highlightForReplacement(true);
} else if(localConnection.type == Blockly.OUTPUT_VALUE) {
Blockly.replacementMarker_ = closestConnection.sourceBlock_;
Blockly.replacementMarker_.highlightShapeForInput(closestConnection,
true);
}
};
/**
* Get rid of the highlighting marking the block that will be replaced.
*/
Blockly.BlockSvg.removeReplacementMarker = function() {
// If there's no block in place, but we're still connecting to a value input,
// then we must be highlighting an input shape.
if (Blockly.highlightedConnection_.type == Blockly.INPUT_VALUE &&
!Blockly.highlightedConnection_.isConnected()) {
Blockly.replacementMarker_.highlightShapeForInput(
Blockly.highlightedConnection_, false);
} else {
Blockly.replacementMarker_.highlightForReplacement(false);
}
Blockly.replacementMarker_ = null;
};
/**
* Place and render an insertion marker to indicate what would happen if you
* release the drag right now.
* @param {Blockly.Connection} localConnection The connection on the dragging
* block.
* @param {Blockly.Connection} closestConnection The connnection to connect the
* insertion marker to.
*/
Blockly.BlockSvg.prototype.connectInsertionMarker_ = function(localConnection,
closestConnection) {
var insertingBlock = Blockly.localConnection_.sourceBlock_;
if (!Blockly.insertionMarker_) {
Blockly.insertionMarker_ =
this.workspace.newBlock(insertingBlock.type);
if (insertingBlock.mutationToDom) {
var oldMutationDom = insertingBlock.mutationToDom();
Blockly.insertionMarker_.domToMutation(oldMutationDom);
}
Blockly.insertionMarker_.setInsertionMarker(true, insertingBlock.width);
Blockly.insertionMarker_.initSvg();
}
var insertionMarker = Blockly.insertionMarker_;
var insertionMarkerConnection = insertionMarker.getMatchingConnection(
localConnection.sourceBlock_, localConnection);
if (insertionMarkerConnection != Blockly.insertionMarkerConnection_) {
insertionMarker.rendered = true;
// Render disconnected from everything else so that we have a valid
// connection location.
insertionMarker.render();
insertionMarker.getSvgRoot().setAttribute('visibility', 'visible');
this.positionNewBlock(insertionMarker,
insertionMarkerConnection, closestConnection);
if (insertionMarkerConnection.type == Blockly.PREVIOUS_STATEMENT &&
!insertionMarker.nextConnection) {
Blockly.bumpedConnection_ = closestConnection.targetConnection;
}
// Renders insertion marker.
insertionMarkerConnection.connect(closestConnection);
Blockly.insertionMarkerConnection_ = insertionMarkerConnection;
}
};
/**
* Disconnect the current insertion marker from the stack, and heal the stack to
* its previous state.
*/
Blockly.BlockSvg.disconnectInsertionMarker = function() {
// The insertion marker is the first block in a stack, either because it
// doesn't have a previous connection or because the previous connection is
// not connected. Unplug won't do anything in that case. Instead, unplug the
// following block.
if (Blockly.insertionMarkerConnection_ ==
Blockly.insertionMarker_.nextConnection &&
(!Blockly.insertionMarker_.previousConnection ||
!Blockly.insertionMarker_.previousConnection.targetConnection)) {
Blockly.insertionMarkerConnection_.targetBlock().unplug(false);
}
// Inside of a C-block, first statement connection.
else if (Blockly.insertionMarkerConnection_.type == Blockly.NEXT_STATEMENT &&
Blockly.insertionMarkerConnection_ !=
Blockly.insertionMarker_.nextConnection) {
var innerConnection = Blockly.insertionMarkerConnection_.targetConnection;
innerConnection.sourceBlock_.unplug(false);
var previousBlockNextConnection =
Blockly.insertionMarker_.previousConnection ?
Blockly.insertionMarker_.previousConnection.targetConnection : null;
Blockly.insertionMarker_.unplug(true);
if (previousBlockNextConnection) {
previousBlockNextConnection.connect(innerConnection);
}
}
else {
Blockly.insertionMarker_.unplug(true /* healStack */);
}
if (Blockly.insertionMarkerConnection_.targetConnection) {
throw 'insertionMarkerConnection still connected at the end of disconnectInsertionMarker';
}
Blockly.insertionMarkerConnection_ = null;
Blockly.insertionMarker_.getSvgRoot().setAttribute('visibility', 'hidden');
};
/**
* Provide visual indication of whether the block will be deleted if
* dropped here.
* Prefer connecting over dropping into the trash can, but prefer dragging to
* the toolbox over connecting to other blocks.
* @param {!Event} e Mouse move event.
* @param {Blockly.Connection} closestConnection The connection this block would
* potentially connect to if dropped here, or null.
* @return {boolean} True if the block would be deleted if dropped here,
* otherwise false.
* @private
*/
Blockly.BlockSvg.prototype.updateCursor_ = function(e, closestConnection) {
var deleteArea = this.workspace.isDeleteArea(e);
var wouldConnect = Blockly.selected && closestConnection &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = deleteArea && !this.getParent() &&
Blockly.selected.isDeletable();
var showDeleteCursor = wouldDelete && !wouldConnect;
if (showDeleteCursor) {
if (deleteArea == Blockly.DELETE_AREA_TRASH && this.workspace.trashcan) {
this.workspace.trashcan.setOpen_(true);
}
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggingDelete');
if (this.workspace.toolbox_) {
// Change the cursor to a hand with an 'x'
this.workspace.toolbox_.addDeleteStyle();
}
return true;
} else {
if (this.workspace.trashcan) {
this.workspace.trashcan.setOpen_(false);
}
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggingDelete');
if (this.workspace.toolbox_) {
// Change the cursor on the toolbox
this.workspace.toolbox_.removeDeleteStyle();
}
return false;
this.childBlocks_[i].setDragging(adding);
}
};
@ -1429,7 +873,7 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
// If this block is being dragged, unlink the mouse events.
if (Blockly.selected == this) {
this.unselect();
Blockly.terminateDrag_();
this.workspace.cancelCurrentGesture();
}
// If this block has a context menu open, close it.
if (Blockly.ContextMenu.currentBlock == this) {
@ -1518,6 +962,22 @@ Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) {
}
};
/**
* Play some UI effects (sound, animation) when disconnecting a block.
* No-op in scratch-blocks, which has no disconnect animation.
* @private
*/
Blockly.BlockSvg.prototype.disconnectUiEffect = function() {
};
/**
* Stop the disconnect UI animation immediately.
* No-op in scratch-blocks, which has no disconnect animation.
* @private
*/
Blockly.BlockSvg.disconnectUiStop_ = function() {
};
/**
* Enable or disable a block.
*/
@ -1587,7 +1047,7 @@ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
clearTimeout(this.setWarningText.pid_[id]);
delete this.setWarningText.pid_[id];
}
if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
if (this.workspace.isDragging()) {
// Don't change the warning text during a drag.
// Wait until the drag finishes.
var thisBlock = this;
@ -1663,6 +1123,22 @@ Blockly.BlockSvg.prototype.removeSelect = function() {
'blocklySelected');
};
/**
* Update the cursor over this block by adding or removing a class.
* @param {boolean} enable True if the delete cursor should be shown, false
* otherwise.
* @package
*/
Blockly.BlockSvg.prototype.setDeleteStyle = function(enable) {
if (enable) {
Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggingDelete');
} else {
Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggingDelete');
}
};
// Overrides of functions on Blockly.Block that take into account whether the
// block has been rendered.
@ -1684,14 +1160,14 @@ Blockly.BlockSvg.prototype.setColour = function(colour, colourSecondary,
}
};
/**
* Move this block to the front of the visible workspace.
* <g> tags do not respect z-index so svg renders them in the
* order that they are in the dom. By placing this block first within the
* block group's <g>, it will render on top of any other blocks.
* @package
*/
Blockly.BlockSvg.prototype.bringToFront_ = function() {
Blockly.BlockSvg.prototype.bringToFront = function() {
var block = this;
do {
var root = block.getSvgRoot();
@ -1699,6 +1175,7 @@ Blockly.BlockSvg.prototype.bringToFront_ = function() {
block = block.getParent();
} while (block);
};
/**
* Set whether this block can chain onto the bottom of another block.
* @param {boolean} newBoolean True if there can be a previous statement.
@ -1822,7 +1299,7 @@ Blockly.BlockSvg.prototype.appendInput_ = function(type, name) {
* Otherwise, for a non-rendered block return an empty list, and for a
* collapsed block don't return inputs connections.
* @return {!Array.<!Blockly.Connection>} Array of connections.
* @private
* @package
*/
Blockly.BlockSvg.prototype.getConnections_ = function(all) {
var myConnections = [];
@ -1902,3 +1379,26 @@ Blockly.BlockSvg.prototype.bumpNeighbours_ = function() {
}
}
};
/**
* Schedule snapping to grid and bumping neighbours to occur after a brief
* delay.
* @package
*/
Blockly.BlockSvg.prototype.scheduleSnapAndBump = function() {
var block = this;
// Ensure that any snap and bump are part of this move's event group.
var group = Blockly.Events.getGroup();
setTimeout(function() {
Blockly.Events.setGroup(group);
block.snapToGrid();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY / 2);
setTimeout(function() {
Blockly.Events.setGroup(group);
block.bumpNeighbours_();
Blockly.Events.setGroup(false);
}, Blockly.BUMP_DELAY);
};

View file

@ -79,20 +79,6 @@ Blockly.mainWorkspace = null;
*/
Blockly.selected = null;
/**
* Currently highlighted connection (during a drag).
* @type {Blockly.Connection}
* @private
*/
Blockly.highlightedConnection_ = null;
/**
* Connection on dragged block that matches the highlighted connection.
* @type {Blockly.Connection}
* @private
*/
Blockly.localConnection_ = null;
/**
* All of the connections on blocks that are currently being dragged.
* @type {!Array.<!Blockly.Connection>}
@ -100,38 +86,6 @@ Blockly.localConnection_ = null;
*/
Blockly.draggingConnections_ = [];
/**
* Connection on the insertion marker block that matches
* Blockly.localConnection_ on the dragged block.
* @type {Blockly.Connection}
* @private
*/
Blockly.insertionMarkerConnection_ = null;
/**
* Grayed-out block that indicates to the user what will happen if they release
* a drag immediately.
* @type {Blockly.Block}
* @private
*/
Blockly.insertionMarker_ = null;
/**
* The block that will be replaced if the drag is released immediately. Should
* be visually highlighted to indicate this to the user.
* @type {Blockly.Block}
* @private
*/
Blockly.replacementMarker_ = null;
/**
* Connection that was bumped out of the way by an insertion marker, and may
* need to be put back as the drag continues.
* @type {Blockly.Connection}
* @private
*/
Blockly.bumpedConnection_ = null;
/**
* Contents of the local clipboard.
* @type {Element}
@ -146,15 +100,6 @@ Blockly.clipboardXml_ = null;
*/
Blockly.clipboardSource_ = null;
/**
* Is the mouse dragging a block?
* DRAG_NONE - No drag operation.
* DRAG_STICKY - Still inside the sticky DRAG_RADIUS.
* DRAG_FREE - Freely draggable.
* @private
*/
Blockly.dragMode_ = Blockly.DRAG_NONE;
/**
* Cached value for whether 3D is supported.
* @type {!boolean}
@ -242,7 +187,15 @@ Blockly.onKeyDown_ = function(e) {
// Delete or backspace.
// Stop the browser from going back to the previous page.
e.preventDefault();
// Don't delete while dragging. Jeez.
if (Blockly.mainWorkspace.isDragging()) {
return;
}
} else if (e.altKey || e.ctrlKey || e.metaKey) {
// Don't use meta keys during drags.
if (Blockly.mainWorkspace.isDragging()) {
return;
}
if (Blockly.selected &&
Blockly.selected.isDeletable() && Blockly.selected.isMovable()) {
if (e.keyCode == 67) {
@ -253,12 +206,7 @@ Blockly.onKeyDown_ = function(e) {
// 'x' for cut.
Blockly.copy_(Blockly.selected);
Blockly.hideChaff();
var heal = Blockly.dragMode_ != Blockly.DRAG_FREE;
Blockly.selected.dispose(heal, true);
if (Blockly.highlightedConnection_) {
Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
}
Blockly.selected.dispose(/* heal */ true, true);
}
}
if (e.keyCode == 86) {
@ -276,15 +224,6 @@ Blockly.onKeyDown_ = function(e) {
}
};
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.terminateDrag_ = function() {
Blockly.BlockSvg.terminateDrag();
Blockly.Flyout.terminateDrag_();
};
/**
* Copy a block onto the local clipboard.
* @param {!Blockly.Block} block Block to be copied.
@ -337,7 +276,8 @@ Blockly.onContextMenu_ = function(e) {
*/
Blockly.hideChaff = function(opt_allowToolbox) {
Blockly.Tooltip.hide();
Blockly.WidgetDiv.hide();
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
if (!opt_allowToolbox) {
var workspace = Blockly.getMainWorkspace();
if (workspace.toolbox_ &&

View file

@ -240,12 +240,6 @@ Blockly.Connection.prototype.dispose = function() {
if (this.inDB_) {
this.db_.removeConnection_(this);
}
if (Blockly.highlightedConnection_ == this) {
Blockly.highlightedConnection_ = null;
}
if (Blockly.localConnection_ == this) {
Blockly.localConnection_ = null;
}
this.db_ = null;
this.dbOpposite_ = null;
};
@ -379,8 +373,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
// If the other side of this connection is the active insertion marker
// connection, we've obviously already decided that this is a good
// connection.
if (candidate.targetConnection ==
Blockly.insertionMarkerConnection_) {
if (candidate.targetBlock().isInsertionMarker()) {
return true;
} else {
return false;

View file

@ -32,6 +32,13 @@ goog.provide('Blockly.constants');
*/
Blockly.DRAG_RADIUS = 3;
/**
* Number of pixels the mouse must move before a drag/scroll starts from the
* flyout. Because the drag-intention is determined when this is reached, it is
* larger than Blockly.DRAG_RADIUS so that the drag-direction is clearer.
*/
Blockly.FLYOUT_DRAG_RADIUS = 10;
/**
* Maximum misalignment between connections for them to snap together.
*/
@ -268,6 +275,13 @@ Blockly.Categories = {
"more": "more"
};
/**
* ENUM representing that an event is not in any delete areas.
* Null for backwards compatibility reasons.
* @const
*/
Blockly.DELETE_AREA_NONE = null;
/**
* ENUM representing that an event is in the delete area of the trash can.
* @const

View file

@ -43,6 +43,12 @@ goog.require('goog.ui.MenuItem');
*/
Blockly.ContextMenu.currentBlock = null;
/**
* @type {Array.<!Array>} Opaque data that can be passed to unbindEvent_.
* @private
*/
Blockly.ContextMenu.eventWrapper_ = null;
/**
* Construct the menu based on the list of options and show the menu.
* @param {!Event} e Mouse event.
@ -55,12 +61,33 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
Blockly.ContextMenu.hide();
return;
}
var menu = Blockly.ContextMenu.populate_(options, rtl);
goog.events.listen(menu, goog.ui.Component.EventType.ACTION,
Blockly.ContextMenu.hide);
Blockly.ContextMenu.position_(menu, e, rtl);
// 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus.
setTimeout(function() {menu.getElement().focus();}, 1);
Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block.
};
/**
* Create the context menu object and populate it with the given options.
* @param {!Array.<!Object>} options Array of menu options.
* @param {boolean} rtl True if RTL, false if LTR.
* @return {!goog.ui.Menu} The menu that will be shown on right click.
* @private
*/
Blockly.ContextMenu.populate_ = function(options, rtl) {
/* Here's what one option object looks like:
{text: 'Make It So',
enabled: true,
callback: Blockly.MakeItSo}
*/
var menu = new goog.ui.Menu();
menu.setAllowAutoFocus(true);
menu.setRightToLeft(rtl);
for (var i = 0, option; option = options[i]; i++) {
var menuItem = new goog.ui.MenuItem(option.text);
@ -76,9 +103,19 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
};
}
}
goog.events.listen(menu, goog.ui.Component.EventType.ACTION,
Blockly.ContextMenu.hide);
// Record windowSize and scrollOffset before adding menu.
return menu;
};
/**
* Add the menu to the page and position it correctly.
* @param {!goog.ui.Menu} menu The menu to add and position.
* @param {!Event} e Mouse event for the right click that is making the context
* menu appear.
* @param {boolean} rtl True if RTL, false if LTR.
* @private
*/
Blockly.ContextMenu.position_ = function(menu, e, rtl) {
// Record windowSize and scrollOffset before adding menu.
var windowSize = goog.dom.getViewportSize();
var scrollOffset = goog.style.getViewportPageOffset(document);
var div = Blockly.WidgetDiv.DIV;
@ -109,12 +146,6 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
}
}
Blockly.WidgetDiv.position(x, y, windowSize, scrollOffset, rtl);
menu.setAllowAutoFocus(true);
// 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus.
setTimeout(function() {menuDom.focus();}, 1);
Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block.
};
/**
@ -123,6 +154,9 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
Blockly.ContextMenu.hide = function() {
Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);
Blockly.ContextMenu.currentBlock = null;
if (Blockly.ContextMenu.eventWrapper_) {
Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_);
}
};
/**

View file

@ -152,6 +152,7 @@ Blockly.Css.CONTENT = [
'height: 100%;',
'position: relative;',
'overflow: hidden;', /* So blocks in drag surface disappear at edges */
'touch-action: none',
'}',
'.blocklyNonSelectable {',

View file

@ -0,0 +1,237 @@
/**
* @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 Class that controls updates to connections during drags.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.DraggedConnectionManager');
goog.require('Blockly.RenderedConnection');
goog.require('goog.math.Coordinate');
/**
* Class that controls updates to connections during drags. It is primarily
* responsible for finding the closest eligible connection and highlighting or
* unhiglighting it as needed during a drag.
* @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
* @constructor
*/
Blockly.DraggedConnectionManager = function(block) {
Blockly.selected = block;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
* @type {!Blockly.Block}
* @private
*/
this.topBlock_ = block;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = block.workspace;
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as well
* as the last connection on the block stack.
* Does not change during a drag.
* @type {!Array.<!Blockly.RenderedConnection>}
* @private
*/
this.availableConnections_ = this.initAvailableConnections_();
/**
* The connection that this block would connect to if released immediately.
* Updated on every mouse move.
* @type {Blockly.RenderedConnection}
* @private
*/
this.closestConnection_ = null;
/**
* The connection that would connect to this.closestConnection_ if this block
* were released immediately.
* Updated on every mouse move.
* @type {Blockly.RenderedConnection}
* @private
*/
this.localConnection_ = null;
/**
* The distance between this.closestConnection_ and this.localConnection_,
* in workspace units.
* Updated on every mouse move.
* @type {number}
* @private
*/
this.radiusConnection_ = 0;
/**
* Whether the block would be deleted if it were dropped immediately.
* Updated on every mouse move.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
};
/**
* Sever all links from this object.
* @package
*/
Blockly.DraggedConnectionManager.prototype.dispose = function() {
this.topBlock_ = null;
this.workspace_ = null;
this.availableConnections_.length = 0;
this.closestConnection_ = null;
this.localConnection_ = null;
};
/**
* Return whether the block would be deleted if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} true if the block would be deleted if dropped immediately.
* @package
*/
Blockly.DraggedConnectionManager.prototype.wouldDeleteBlock = function() {
return this.wouldDeleteBlock_;
};
/**
* Connect to the closest connection and render the results.
* This should be called at the end of a drag.
* @package
*/
Blockly.DraggedConnectionManager.prototype.applyConnections = function() {
if (this.closestConnection_) {
// Connect two blocks together.
this.localConnection_.connect(this.closestConnection_);
if (this.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection = this.localConnection_.isSuperior() ?
this.closestConnection_ : this.localConnection_;
inferiorConnection.getSourceBlock().connectionUiEffect();
}
this.removeHighlighting_();
}
};
/**
* Update highlighted connections based on the most recent move location.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @package
*/
Blockly.DraggedConnectionManager.prototype.update = function(dxy, deleteArea) {
var oldClosestConnection = this.closestConnection_;
var closestConnectionChanged = this.updateClosest_(dxy);
if (closestConnectionChanged && oldClosestConnection) {
oldClosestConnection.unhighlight();
}
// Prefer connecting over dropping into the trash can, but prefer dragging to
// the toolbox over connecting to other blocks.
var wouldConnect = !!this.closestConnection_ &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = !!deleteArea && !this.topBlock_.getParent() &&
this.topBlock_.isDeletable();
this.wouldDeleteBlock_ = wouldDelete && !wouldConnect;
if (!this.wouldDeleteBlock_ && closestConnectionChanged &&
this.closestConnection_) {
this.addHighlighting_();
}
};
/**
* Remove highlighting from the currently highlighted connection, if it exists.
* @private
*/
Blockly.DraggedConnectionManager.prototype.removeHighlighting_ = function() {
if (this.closestConnection_) {
this.closestConnection_.unhighlight();
}
};
/**
* Add highlighting to the closest connection, if it exists.
* @private
*/
Blockly.DraggedConnectionManager.prototype.addHighlighting_ = function() {
if (this.closestConnection_) {
this.closestConnection_.highlight();
}
};
/**
* Populate the list of available connections on this block stack. This should
* only be called once, at the beginning of a drag.
* @return {!Array.<!Blockly.RenderedConnection>} a list of available
* connections.
* @private
*/
Blockly.DraggedConnectionManager.prototype.initAvailableConnections_ = function() {
var available = this.topBlock_.getConnections_(false);
// Also check the last connection on this stack
var lastOnStack = this.topBlock_.lastConnectionInStack();
if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) {
available.push(lastOnStack);
}
return available;
};
/**
* Find the new closest connection, and update internal state in response.
* @param {!goog.math.Coordinate} dxy Position relative to the drag start,
* in workspace units.
* @return {boolean} Whether the closest connection has changed.
* @private
*/
Blockly.DraggedConnectionManager.prototype.updateClosest_ = function(dxy) {
var oldClosestConnection = this.closestConnection_;
this.closestConnection_ = null;
this.localConnection_ = null;
this.radiusConnection_ = Blockly.SNAP_RADIUS;
for (var i = 0; i < this.availableConnections_.length; i++) {
var myConnection = this.availableConnections_[i];
var neighbour = myConnection.closest(this.radiusConnection_, dxy);
if (neighbour.connection) {
this.closestConnection_ = neighbour.connection;
this.localConnection_ = myConnection;
this.radiusConnection_ = neighbour.radius;
}
}
return oldClosestConnection != this.closestConnection_;
};

View file

@ -822,7 +822,7 @@ Blockly.Events.disableOrphans = function(event) {
child.setDisabled(false);
}
} else if ((block.outputConnection || block.previousConnection) &&
Blockly.dragMode_ == Blockly.DRAG_NONE) {
!workspace.isDragging()) {
do {
block.setDisabled(true);
block = block.getNextBlock();

View file

@ -28,6 +28,8 @@
goog.provide('Blockly.Field');
goog.require('Blockly.Gesture');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.math.Size');
@ -173,12 +175,12 @@ Blockly.Field.prototype.init = function() {
this.updateEditable();
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
this.mouseUpWrapper_ =
Blockly.bindEventWithChecks_(this.getClickTarget_(), 'mouseup', this,
this.onMouseUp_);
// Force a render.
this.render_();
this.size_.width = 0;
this.mouseDownWrapper_ =
Blockly.bindEventWithChecks_(this.fieldGroup_, 'mousedown', this,
this.onMouseDown_);
};
/**
@ -192,9 +194,9 @@ Blockly.Field.prototype.initModel = function() {
* Dispose of all DOM objects belonging to this editable field.
*/
Blockly.Field.prototype.dispose = function() {
if (this.mouseUpWrapper_) {
Blockly.unbindEvent_(this.mouseUpWrapper_);
this.mouseUpWrapper_ = null;
if (this.mouseDownWrapper_) {
Blockly.unbindEvent_(this.mouseDownWrapper_);
this.mouseDownWrapper_ = null;
}
this.sourceBlock_ = null;
goog.dom.removeNode(this.fieldGroup_);
@ -628,32 +630,21 @@ Blockly.Field.prototype.setValue = function(newValue) {
};
/**
* Handle a mouse up event on an editable field.
* @param {!Event} e Mouse up event.
* Handle a mouse down event on a field.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Field.prototype.onMouseUp_ = function(e) {
if ((goog.userAgent.IPHONE || goog.userAgent.IPAD) &&
!goog.userAgent.isVersionOrHigher('537.51.2') &&
e.layerX !== 0 && e.layerY !== 0) {
// Old iOS spawns a bogus event on the next touch after a 'prompt()' edit.
// Unlike the real events, these have a layerX and layerY set.
Blockly.Field.prototype.onMouseDown_ = function(e) {
if (!this.sourceBlock_ || !this.sourceBlock_.workspace) {
return;
} else if (Blockly.utils.isRightButton(e)) {
// Right-click.
return;
} else if (this.sourceBlock_.workspace.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
} else if (this.sourceBlock_.isEditable()) {
// Non-abstract sub-classes must define a showEditor_ method.
this.showEditor_();
// The field is handling the touch, but we also want the blockSvg onMouseUp
// handler to fire, so we will leave the touch identifier as it is.
// The next onMouseUp is responsible for nulling it out.
}
var gesture = this.sourceBlock_.workspace.getGesture(e);
if (gesture) {
gesture.setStartField(this);
}
};
/**
* Change the tooltip text for this field.
* @param {string|!Element} newTip Text for tooltip or a parent element to

View file

@ -149,11 +149,11 @@ Blockly.FieldAngle.prototype.showEditor_ = function() {
}, svg);
this.gauge_ = Blockly.utils.createSvgElement('path',
{'class': 'blocklyAngleGauge'}, svg);
this.line_ = Blockly.utils.createSvgElement('line',
{'x1': Blockly.FieldAngle.HALF,
'y1': Blockly.FieldAngle.HALF,
'class': 'blocklyAngleLine'
}, svg);
this.line_ = Blockly.utils.createSvgElement('line',{
'x1': Blockly.FieldAngle.HALF,
'y1': Blockly.FieldAngle.HALF,
'class': 'blocklyAngleLine'
}, svg);
// Draw markers around the edge.
for (var angle = 0; angle < 360; angle += 15) {
Blockly.utils.createSvgElement('line', {

View file

@ -100,28 +100,6 @@ Blockly.FieldDropdown.prototype.imageElement_ = null;
*/
Blockly.FieldDropdown.prototype.imageJson_ = null;
/**
* Language-neutral currently selected string or image object.
* @type {string|!Object}
* @private
*/
Blockly.FieldDropdown.prototype.value_ = '';
/**
* SVG image element if currently selected option is an image, or null.
* @type {SVGElement}
* @private
*/
Blockly.FieldDropdown.prototype.imageElement_ = null;
/**
* Object with src, height, width, and alt attributes if currently selected
* option is an image, or null.
* @type {Object}
* @private
*/
Blockly.FieldDropdown.prototype.imageJson_ = null;
/**
* Install this dropdown on a block.
*/

View file

@ -490,7 +490,7 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
}
thisField.setText(text);
// Rerender the field now that the text has changed.
thisField.sourceBlock_.rendered && thisField.render_();
thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);

View file

@ -135,30 +135,36 @@ Blockly.FieldVariable.prototype.setValue = function(newValue) {
* @this {Blockly.FieldVariable}
*/
Blockly.FieldVariable.dropdownCreate = function() {
var variableNameList = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
// Get a copy of the list, so that adding rename and new variable options
// doesn't modify the workspace's list.
var variableList = this.sourceBlock_.workspace.variableList.slice(0);
} else {
var variableList = [];
var variableModelList = this.sourceBlock_.workspace.getVariablesOfType('');
for (var i = 0; i < variableModelList.length; i++) {
variableNameList.push(variableModelList[i].name);
}
}
// Ensure that the currently selected variable is an option.
var name = this.getText();
if (name && variableList.indexOf(name) == -1) {
variableList.push(name);
if (name && variableNameList.indexOf(name) == -1) {
variableNameList.push(name);
}
variableList.sort(goog.string.caseInsensitiveCompare);
variableNameList.sort(goog.string.caseInsensitiveCompare);
this.renameVarItemIndex_ = variableList.length;
variableList.push(Blockly.Msg.RENAME_VARIABLE);
this.renameVarItemIndex_ = variableNameList.length;
variableNameList.push(Blockly.Msg.RENAME_VARIABLE);
this.deleteVarItemIndex_ = variableList.length;
variableList.push(Blockly.Msg.DELETE_VARIABLE.replace('%1', name));
this.deleteVarItemIndex_ = variableNameList.length;
variableNameList.push(Blockly.Msg.DELETE_VARIABLE.replace('%1', name));
// Variables are not language-specific, use the name as both the user-facing
// text and the internal representation.
var options = [];
for (var i = 0; i < variableList.length; i++) {
options[i] = [variableList[i], variableList[i]];
for (var i = 0; i < variableNameList.length; i++) {
// TODO(marisaleung): Set options[i] to [name, uuid]. This requires
// changes where the variable gets set since the initialized value would be
// id.
options[i] = [variableNameList[i], variableNameList[i]];
}
return options;
};

View file

@ -32,6 +32,7 @@ goog.require('Blockly.Block');
goog.require('Blockly.Comment');
goog.require('Blockly.Events');
goog.require('Blockly.FlyoutButton');
goog.require('Blockly.Gesture');
goog.require('Blockly.Touch');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
@ -112,20 +113,6 @@ Blockly.Flyout = function(workspaceOptions) {
*/
this.permanentlyDisabled_ = [];
/**
* y coordinate of mousedown - used to calculate scroll distances.
* @type {number}
* @private
*/
this.startDragMouseY_ = 0;
/**
* x coordinate of mousedown - used to calculate scroll distances.
* @type {number}
* @private
*/
this.startDragMouseX_ = 0;
/**
* The toolbox that this flyout belongs to, or none if tihs is a simple
* workspace.
@ -135,51 +122,6 @@ Blockly.Flyout = function(workspaceOptions) {
this.parentToolbox_ = null;
};
/**
* When a flyout drag is in progress, this is a reference to the flyout being
* dragged. This is used by Flyout.terminateDrag_ to reset dragMode_.
* @type {Blockly.Flyout}
* @private
*/
Blockly.Flyout.startFlyout_ = null;
/**
* Event that started a drag. Used to determine the drag distance/direction and
* also passed to BlockSvg.onMouseDown_() after creating a new block.
* @type {Event}
* @private
*/
Blockly.Flyout.startDownEvent_ = null;
/**
* Flyout block where the drag/click was initiated. Used to fire click events or
* create a new block.
* @type {Blockly.Block}
* @private
*/
Blockly.Flyout.startBlock_ = null;
/**
* Wrapper function called when a mouseup occurs during a background or block
* drag operation.
* @type {function}
* private
*/
Blockly.Flyout.onMouseUpWrapper_ = null;
/**
* Wrapper function called when a mousemove occurs during a background drag.
* @type {function}
* @private
*/
Blockly.Flyout.onMouseMoveWrapper_ = null;
/**
* Wrapper function called when a mousemove occurs during a block drag.
* @private {Array.<!Array>}
*/
Blockly.Flyout.onMouseMoveBlockWrapper_ = null;
/**
* Does the flyout automatically close when a block is created?
* @type {boolean}
@ -207,13 +149,6 @@ Blockly.Flyout.prototype.containerVisible_ = true;
*/
Blockly.Flyout.prototype.CORNER_RADIUS = 0;
/**
* Number of pixels the mouse must move before a drag/scroll starts. Because the
* drag-intention is determined when this is reached, it is larger than
* Blockly.DRAG_RADIUS so that the drag-direction is clearer.
*/
Blockly.Flyout.prototype.DRAG_RADIUS = 10;
/**
* Margin around the edges of the blocks in the flyout.
* @type {number}
@ -295,16 +230,6 @@ Blockly.Flyout.prototype.verticalOffset_ = 0;
*/
Blockly.Flyout.prototype.dragAngleRange_ = 70;
/**
* Is the flyout dragging (scrolling)?
* 0 - DRAG_NONE - no drag is ongoing or state is undetermined
* 1 - DRAG_STICKY - still within the sticky drag radius
* 2 - DRAG_FREE - in scroll mode (never create a new block)
* @private
*/
Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE;
/**
* Creates the flyout's DOM. Only needs to be called once. The flyout can
* either exist as its own svg element or be a g element nested inside a
@ -345,10 +270,14 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.position();
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.wheel_));
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this, this.wheel_));
// Dragging the flyout up and down (or left and right).
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_));
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this, this.onMouseDown_));
// A flyout connected to a workspace doesn't have its own current gesture.
this.workspace_.getGesture =
this.targetWorkspace_.getGesture.bind(this.targetWorkspace_);
};
/**
@ -403,7 +332,8 @@ Blockly.Flyout.prototype.getHeight = function() {
/**
* Get the flyout's workspace.
* @return {!Blockly.Workspace} Workspace on which this flyout's blocks are placed.
* @return {!Blockly.WorkspaceSvg} The workspace inside the flyout.
* @package
*/
Blockly.Flyout.prototype.getWorkspace = function() {
return this.workspace_;
@ -627,22 +557,6 @@ Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
block.removeSelect));
};
/**
* Actions to take when a block in the flyout is right-clicked.
* @param {!Event} e Event that triggered the right-click. Could originate from
* a long-press in a touch environment.
* @param {Blockly.BlockSvg} block The block that was clicked.
*/
Blockly.Flyout.blockRightClick_ = function(e, block) {
Blockly.terminateDrag_();
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.hideChaff(true);
block.showContextMenu_(e);
// This was a right-click, so end the gesture immediately.
Blockly.Touch.clearTouchIdentifier();
};
/**
* Handle a mouse-down on an SVG block in a non-closing flyout.
* @param {!Blockly.Block} block The flyout block to copy.
@ -652,29 +566,11 @@ Blockly.Flyout.blockRightClick_ = function(e, block) {
Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
var flyout = this;
return function(e) {
if (Blockly.utils.isRightButton(e)) {
Blockly.Flyout.blockRightClick_(e, block);
} else {
flyout.dragMode_ = Blockly.DRAG_NONE;
Blockly.terminateDrag_();
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.hideChaff();
// Left-click (or middle click)
// Record the current mouse position.
flyout.startDragMouseY_ = e.clientY;
flyout.startDragMouseX_ = e.clientX;
Blockly.Flyout.startDownEvent_ = e;
Blockly.Flyout.startBlock_ = block;
Blockly.Flyout.startFlyout_ = flyout;
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document,
'mouseup', flyout, flyout.onMouseUp_);
Blockly.Flyout.onMouseMoveBlockWrapper_ = Blockly.bindEvent_(document,
'mousemove', flyout, flyout.onMouseMoveBlock_);
var gesture = flyout.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.setStartBlock(block);
gesture.handleFlyoutStart(e, flyout);
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
e.preventDefault();
};
};
@ -684,220 +580,47 @@ Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
* @private
*/
Blockly.Flyout.prototype.onMouseDown_ = function(e) {
this.dragMode_ = Blockly.DRAG_FREE;
if (Blockly.utils.isRightButton(e)) {
// Don't start drags with right clicks.
Blockly.Touch.clearTouchIdentifier();
return;
}
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.hideChaff(true);
this.dragMode_ = Blockly.DRAG_FREE;
this.startDragMouseY_ = e.clientY;
this.startDragMouseX_ = e.clientX;
Blockly.Flyout.startFlyout_ = this;
Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove',
this, this.onMouseMove_);
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup',
this, Blockly.Flyout.terminateDrag_);
// This event has been handled. No need to bubble up to the document.
e.preventDefault();
e.stopPropagation();
};
/**
* Handle a mouse-up anywhere in the SVG pane. Is only registered when a
* block is clicked. We can't use mouseUp on the block since a fast-moving
* cursor can briefly escape the block before it catches up.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Flyout.prototype.onMouseUp_ = function(/*e*/) {
if (!this.workspace_.isDragging()) {
// This was a click, not a drag. End the gesture.
Blockly.Touch.clearTouchIdentifier();
// 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 (this.autoClose) {
this.createBlockFunc_(Blockly.Flyout.startBlock_)(
Blockly.Flyout.startDownEvent_);
} else if (!fieldEditing) {
Blockly.Events.fire(
new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'click',
undefined, undefined));
Blockly.Events.fire(
new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'stackclick',
undefined, undefined));
}
}
Blockly.terminateDrag_();
};
/**
* Handle a mouse-move to vertically drag the flyout.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Flyout.prototype.onMouseMove_ = function(e) {
var metrics = this.getMetrics_();
if (this.horizontalLayout_) {
if (metrics.contentWidth - metrics.viewWidth < 0) {
return;
}
var dx = e.clientX - this.startDragMouseX_;
this.startDragMouseX_ = e.clientX;
var x = metrics.viewLeft - dx;
x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth);
this.scrollbar_.set(x);
} else {
if (metrics.contentHeight - metrics.viewHeight < 0) {
return;
}
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var y = metrics.viewTop - dy;
y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight);
this.scrollbar_.set(y);
}
};
/**
* Mouse button is down on a block in a non-closing flyout. Create the block
* if the mouse moves beyond a small radius. This allows one to play with
* fields without instantiating blocks that instantly self-destruct.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) {
if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 &&
e.button == 0) {
/* HACK:
Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove events
on certain touch actions. Ignore events with these signatures.
This may result in a one-pixel blind spot in other browsers,
but this shouldn't be noticeable. */
e.stopPropagation();
return;
}
var dx = e.clientX - Blockly.Flyout.startDownEvent_.clientX;
var dy = e.clientY - Blockly.Flyout.startDownEvent_.clientY;
var createBlock = this.determineDragIntention_(dx, dy);
Blockly.longStop_();
if (createBlock) {
this.createBlockFunc_(Blockly.Flyout.startBlock_)(
Blockly.Flyout.startDownEvent_);
} else if (this.dragMode_ == Blockly.DRAG_FREE) {
// Do a scroll.
this.onMouseMove_(e);
}
e.stopPropagation();
};
/**
* Determine the intention of a drag.
* Updates dragMode_ based on a drag delta and the current mode,
* and returns true if we should create a new block.
* @param {number} dx X delta of the drag.
* @param {number} dy Y delta of the drag.
* @return {boolean} True if a new block should be created.
* @private
*/
Blockly.Flyout.prototype.determineDragIntention_ = function(dx, dy) {
if (this.dragMode_ == Blockly.DRAG_FREE) {
// Once in free mode, always stay in free mode and never create a block.
return false;
}
var dragDistance = Math.sqrt(dx * dx + dy * dy);
if (dragDistance < this.DRAG_RADIUS) {
// Still within the sticky drag radius.
this.dragMode_ = Blockly.DRAG_STICKY;
return false;
} else {
if (this.isDragTowardWorkspace_(dx, dy) || !this.scrollbar_.isVisible()) {
// Immediately create a block.
return true;
} else {
// Immediately move to free mode - the drag is away from the workspace.
this.dragMode_ = Blockly.DRAG_FREE;
return false;
}
var gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.handleFlyoutStart(e, this);
}
};
/**
* Create a copy of this block on the workspace.
* @param {!Blockly.Block} originBlock The flyout block to copy.
* @return {!Function} Function to call when block is clicked.
* @private
* @param {!Blockly.BlockSvg} originalBlock The block to copy from the flyout.
* @return {Blockly.BlockSvg} The newly created block, or null if something
* went wrong with deserialization.
* @package
*/
Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
var flyout = this;
return function(e) {
// Hide drop-downs and animating WidgetDiv immediately
Blockly.WidgetDiv.hide(true);
Blockly.DropDownDiv.hideWithoutAnimation();
if (Blockly.utils.isRightButton(e)) {
// Right-click. Don't create a block, let the context menu show.
return;
Blockly.Flyout.prototype.createBlock = function(originalBlock) {
var newBlock = null;
Blockly.Events.disable();
this.targetWorkspace_.setResizesEnabled(false);
try {
newBlock = this.placeNewBlock_(originalBlock);
//Force a render on IE and Edge to get around the issue described in
//Blockly.Field.getCachedWidth
if (goog.userAgent.IE || goog.userAgent.EDGE) {
var blocks = newBlock.getDescendants();
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].render(false);
}
}
if (originBlock.disabled) {
// Beyond capacity.
return;
}
Blockly.Events.disable();
// Disable workspace resizing. Reenable at the end of the drag. This avoids
// a spurious resize between creating the new block and placing it in the
// workspace.
flyout.targetWorkspace_.setResizesEnabled(false);
try {
var block = flyout.placeNewBlock_(originBlock);
} finally {
Blockly.Events.enable();
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.setGroup(true);
Blockly.Events.fire(new Blockly.Events.Create(block));
}
if (flyout.autoClose) {
flyout.hide();
}
// Start a dragging operation on the new block.
block.onMouseDown_(e);
Blockly.dragMode_ = Blockly.DRAG_FREE;
block.setDragging_(true);
block.moveToDragSurface_();
};
};
// Close the flyout.
Blockly.hideChaff();
} finally {
Blockly.Events.enable();
}
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.Flyout.terminateDrag_ = function() {
if (Blockly.Flyout.startFlyout_) {
// User was dragging the flyout background, and has stopped.
if (Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) {
Blockly.Touch.clearTouchIdentifier();
}
Blockly.Flyout.startFlyout_.dragMode_ = Blockly.DRAG_NONE;
Blockly.Flyout.startFlyout_ = null;
if (Blockly.Events.isEnabled()) {
Blockly.Events.setGroup(true);
Blockly.Events.fire(new Blockly.Events.Create(newBlock));
}
if (Blockly.Flyout.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.Flyout.onMouseUpWrapper_);
Blockly.Flyout.onMouseUpWrapper_ = null;
if (this.autoClose) {
this.hide();
}
if (Blockly.Flyout.onMouseMoveBlockWrapper_) {
Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveBlockWrapper_);
Blockly.Flyout.onMouseMoveBlockWrapper_ = null;
}
if (Blockly.Flyout.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveWrapper_);
Blockly.Flyout.onMouseMoveWrapper_ = null;
}
Blockly.Flyout.startDownEvent_ = null;
Blockly.Flyout.startBlock_ = null;
return newBlock;
};
/**
@ -914,3 +637,12 @@ Blockly.Flyout.prototype.reflow = function() {
this.workspace_.addChangeListener(this.reflowWrapper_);
}
};
/**
* @return {boolean} True if this flyout may be scrolled with a scrollbar or by
* dragging.
* @package
*/
Blockly.Flyout.prototype.isScrollable = function() {
return this.scrollbar_ ? this.scrollbar_.isVisible() : false;
};

View file

@ -115,6 +115,13 @@ Blockly.FlyoutButton.prototype.width = 0;
*/
Blockly.FlyoutButton.prototype.height = 40; // Can't be computed like the width
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {Array.<!Array>}
* @private
*/
Blockly.FlyoutButton.prototype.onMouseUpWrapper_ = null;
/**
* Create the button elements.
* @return {!Element} The button's SVG group.
@ -164,6 +171,9 @@ Blockly.FlyoutButton.prototype.createDom = function() {
svgText.setAttribute('y', this.height / 2);
this.updateTransform_();
this.mouseUpWrapper_ = Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup',
this, this.onMouseUp_);
return this.svgGroup_;
};
@ -208,6 +218,9 @@ Blockly.FlyoutButton.prototype.getTargetWorkspace = function() {
* Dispose of this button.
*/
Blockly.FlyoutButton.prototype.dispose = function() {
if (this.onMouseUpWrapper_) {
Blockly.unbindEvent_(this.onMouseUpWrapper_);
}
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
@ -219,15 +232,13 @@ Blockly.FlyoutButton.prototype.dispose = function() {
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.FlyoutButton.prototype.onMouseUp = function(e) {
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
// Stop binding to mouseup and mousemove events--flyout mouseup would normally
// do this, but we're skipping that.
Blockly.Flyout.terminateDrag_();
Blockly.FlyoutButton.prototype.onMouseUp_ = function(e) {
var gesture = this.targetWorkspace_.getGesture(e);
if (gesture) {
gesture.cancel();
}
// Call the callback registered to this button.
if (this.callback_) {

83
core/flyout_dragger.js Normal file
View file

@ -0,0 +1,83 @@
/**
* @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 Methods for dragging a flyout visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.FlyoutDragger');
goog.require('Blockly.WorkspaceDragger');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* Class for a flyout dragger. It moves a flyout workspace around when it is
* being dragged by a mouse or touch.
* Note that the workspace itself manages whether or not it has a drag surface
* and how to do translations based on that. This simply passes the right
* commands based on events.
* @param {!Blockly.Flyout} flyout The flyout to drag.
* @constructor
*/
Blockly.FlyoutDragger = function(flyout) {
Blockly.FlyoutDragger.superClass_.constructor.call(this,
flyout.getWorkspace());
/**
* The scrollbar to update to move the flyout.
* Unlike the main workspace, the flyout has only one scrollbar, in either the
* horizontal or the vertical direction.
* @type {!Blockly.Scrollbar}
* @private
*/
this.scrollbar_ = flyout.scrollbar_;
/**
* Whether the flyout scrolls horizontally. If false, the flyout scrolls
* vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = flyout.horizontalLayout_;
};
goog.inherits(Blockly.FlyoutDragger, Blockly.WorkspaceDragger);
/**
* Move the appropriate scrollbar to drag the flyout.
* Since flyouts only scroll in one direction at a time, this will discard one
* of the calculated values.
* x and y are in pixels.
* @param {number} x The new x position to move the scrollbar to.
* @param {number} y The new y position to move the scrollbar to.
* @private
*/
Blockly.FlyoutDragger.prototype.updateScroll_ = function(x, y) {
// Move the scrollbar and the flyout will scroll automatically.
if (this.horizontalLayout_) {
this.scrollbar_.set(x);
} else {
this.scrollbar_.set(y);
}
};

View file

@ -326,7 +326,11 @@ Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
var buttonSvg = button.createDom();
button.moveTo(cursorX, cursorY);
button.show();
Blockly.bindEvent_(buttonSvg, 'mouseup', button, button.onMouseUp);
// Clicking on a flyout button or label is a lot like clicking on the
// flyout background.
this.listeners_.push(Blockly.bindEventWithChecks_(buttonSvg, 'mousedown',
this, this.onMouseDown_));
this.buttons_.push(button);
cursorX += (button.width + gaps[i]);
@ -334,33 +338,18 @@ Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
}
};
/**
* Handle a mouse-move to drag the flyout.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.HorizontalFlyout.prototype.onMouseMove_ = function(e) {
var metrics = this.getMetrics_();
if (metrics.contentWidth - metrics.viewWidth < 0) {
return;
}
var dx = e.clientX - this.startDragMouseX_;
this.startDragMouseX_ = e.clientX;
var x = metrics.viewLeft - dx;
x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth);
this.scrollbar_.set(x);
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {number} dx X delta of the drag.
* @param {number} dy Y delta of the drag.
* and orientation of the flyout. This to decide if a new block should be
* created or if the flyout should scroll.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} true if the drag is toward the workspace.
* @private
* @package
*/
Blockly.HorizontalFlyout.prototype.isDragTowardWorkspace_ = function(dx, dy) {
Blockly.HorizontalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
var dx = currentDragDeltaXY.x;
var dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;

View file

@ -447,7 +447,10 @@ Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) {
var buttonSvg = button.createDom();
button.moveTo(cursorX, cursorY);
button.show();
Blockly.bindEvent_(buttonSvg, 'mouseup', button, button.onMouseUp);
// Clicking on a flyout button or label is a lot like clicking on the
// flyout background.
this.listeners_.push(Blockly.bindEventWithChecks_(buttonSvg, 'mousedown',
this, this.onMouseDown_));
this.buttons_.push(button);
cursorY += button.height + gaps[i];
@ -557,33 +560,18 @@ Blockly.VerticalFlyout.prototype.checkboxClicked_ = function(checkboxObj) {
};
};
/**
* Handle a mouse-move to vertically drag the flyout.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.VerticalFlyout.prototype.onMouseMove_ = function(e) {
var metrics = this.getMetrics_();
if (metrics.contentHeight - metrics.viewHeight < 0) {
return;
}
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var y = -this.workspace_.scrollY - dy;
y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight);
this.scrollbar_.set(y);
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {number} dx X delta of the drag.
* @param {number} dy Y delta of the drag.
* and orientation of the flyout. This to decide if a new block should be
* created or if the flyout should scroll.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} true if the drag is toward the workspace.
* @private
* @package
*/
Blockly.VerticalFlyout.prototype.isDragTowardWorkspace_ = function(dx, dy) {
Blockly.VerticalFlyout.prototype.isDragTowardWorkspace = function(currentDragDeltaXY) {
var dx = currentDragDeltaXY.x;
var dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;

763
core/gesture.js Normal file
View file

@ -0,0 +1,763 @@
/**
* @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.BlockDragger');
goog.require('Blockly.constants');
goog.require('Blockly.FlyoutDragger');
goog.require('Blockly.Tooltip');
goog.require('Blockly.Touch');
goog.require('Blockly.WorkspaceDragger');
goog.require('goog.asserts');
goog.require('goog.math.Coordinate');
/**
* NB: 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_ = 0;
/**
* 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 block on a
* field.
* @type {Blockly.BlockSvg}
* @private
*/
this.startBlock_ = 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;
/**
* 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 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;
};
/**
* 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.startWorkspace_ = null;
this.flyout_ = null;
if (this.blockDragger_) {
this.blockDragger_.dispose();
this.blockDragger_ = null;
}
if (this.workspaceDragger_) {
this.workspaceDragger_.dispose();
this.workspaceDragger_ = 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 startBlock_ 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.startBlock_.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);
}
this.startBlock_ = this.flyout_.createBlock(this.startBlock_);
this.startBlock_.select();
return true;
}
return false;
};
/**
* 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.startBlock_) {
return false;
}
if (this.flyout_) {
this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_();
} else if (this.startBlock_.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 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 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() {
this.blockDragger_ = new Blockly.BlockDragger(this.startBlock_,
this.startWorkspace_);
this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_);
this.blockDragger_.dragBlock(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.BlockSvg.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 by default.
Blockly.hideChaff(!!this.flyout_);
Blockly.Tooltip.block();
if (this.startBlock_) {
this.startBlock_.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.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) {
this.updateFromEvent_(e);
if (this.isDraggingWorkspace_) {
this.workspaceDragger_.drag(this.currentDragDeltaXY_);
} else if (this.isDraggingBlock_) {
this.blockDragger_.dragBlock(this.mostRecentEvent_,
this.currentDragDeltaXY_);
}
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_) {
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.
if (this.isDraggingBlock_) {
this.blockDragger_.endBlockDrag(e, this.currentDragDeltaXY_);
} else if (this.isDraggingWorkspace_) {
this.workspaceDragger_.endDrag(this.currentDragDeltaXY_);
} 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_) {
return;
}
Blockly.longStop_();
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.startBlock_) {
this.bringBlockToFront_();
Blockly.hideChaff(this.flyout_);
this.startBlock_.showContextMenu_(e);
} else if (this.startWorkspace_ && !this.flyout_) {
Blockly.hideChaff();
this.startWorkspace_.showContextMenu_(e);
}
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;
};
/* 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
*/
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 (!Blockly.Events.getGroup()) {
Blockly.Events.setGroup(true);
}
var newBlock = this.flyout_.createBlock(this.startBlock_);
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. */
/**
* 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.startBlock_ && !this.flyout_) {
this.startBlock_.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 block that a gesture started on.
* If the block is a shadow, record the parent. If the block is in the flyout,
* use the root block from the block group.
* @param {Blockly.BlockSvg} block The block the gesture started on.
* @package
*/
Blockly.Gesture.prototype.setStartBlock = function(block) {
if (!this.startBlock_) {
if (block.isShadow()) {
this.setStartBlock(block.getParent());
} else if (block.isInFlyout && block != block.getRootBlock()) {
this.setStartBlock(block.getRootBlock());
} else {
this.startBlock_ = 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 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.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_;
};
/**
* 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_;
};

View file

@ -170,7 +170,7 @@ Blockly.Icon.prototype.renderIcon = function(cursorX) {
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {!goog.math.Coordinate} xy Absolute location.
* @param {!goog.math.Coordinate} xy Absolute location in workspace coordinates.
*/
Blockly.Icon.prototype.setIconLocation = function(xy) {
this.iconXY_ = xy;
@ -197,7 +197,8 @@ Blockly.Icon.prototype.computeIconLocation = function() {
/**
* Returns the center of the block's icon relative to the surface.
* @return {!goog.math.Coordinate} Object with x and y properties.
* @return {!goog.math.Coordinate} Object with x and y properties in workspace
* coordinates.
*/
Blockly.Icon.prototype.getIconLocation = function() {
return this.iconXY_;

View file

@ -166,7 +166,6 @@ Blockly.createDom_ = function(container, options) {
'operator': 'in', 'result': 'outGlow'}, replacementGlowFilter);
Blockly.utils.createSvgElement('feComposite',
{'in': 'SourceGraphic', 'in2': 'outGlow', 'operator': 'over'}, replacementGlowFilter);
/*
<pattern id="blocklyDisabledPattern837493" patternUnits="userSpaceOnUse"
width="10" height="10">
@ -237,7 +236,7 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, workspac
if (!options.readOnly && !options.hasScrollbars) {
var workspaceChanged = function() {
if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
if (!mainWorkspace.isDragging()) {
var metrics = mainWorkspace.getMetrics();
var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
var edgeTop = metrics.viewTop + metrics.absoluteTop;
@ -371,10 +370,6 @@ Blockly.inject.bindDocumentEvents_ = function() {
// should run regardless of what other touch event handlers have run.
Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_);
Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_);
// Don't use bindEvent_ for document's mouseup since that would create a
// corresponding touch handler that would squelch the ability to interact
// with non-Blockly elements.
document.addEventListener('mouseup', Blockly.onMouseUp_, false);
// Some iPad versions don't fire resize after portrait to landscape change.
if (goog.userAgent.IPAD) {
Blockly.bindEventWithChecks_(window, 'orientationchange', document,

View file

@ -0,0 +1,645 @@
/**
* @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 Class that controls updates to connections during drags.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.InsertionMarkerManager');
goog.require('Blockly.RenderedConnection');
goog.require('goog.math.Coordinate');
/**
* Class that controls updates to connections during drags. It is primarily
* responsible for finding the closest eligible connection and highlighting or
* unhiglighting it as needed during a drag.
* @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
* @constructor
*/
Blockly.InsertionMarkerManager = function(block) {
Blockly.selected = block;
/**
* The top block in the stack being dragged.
* Does not change during a drag.
* @type {!Blockly.Block}
* @private
*/
this.topBlock_ = block;
/**
* The workspace on which these connections are being dragged.
* Does not change during a drag.
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = block.workspace;
/**
* The last connection on the stack, if it's not the last connection on the
* first block.
* Set in initAvailableConnections, if at all.
* @type {Blockly.RenderedConnection}
* @private
*/
this.lastOnStack_ = null;
/**
* The insertion marker corresponding to the last block in the stack, if
* that's not the same as the first block in the stack.
* Set in initAvailableConnections, if at all
* @type {Blockly.BlockSvg}
* @private
*/
this.lastMarker_ = null;
/**
* The insertion marker that shows up between blocks to show where a block
* would go if dropped immediately.
* This is the scratch-blocks equivalent of connection highlighting.
* @type {Blockly.BlockSvg}
* @private
*/
this.firstMarker_ = this.createMarkerBlock_(this.topBlock_);
/**
* The connection that this block would connect to if released immediately.
* Updated on every mouse move.
* This is not on any of the blocks that are being dragged.
* @type {Blockly.RenderedConnection}
* @private
*/
this.closestConnection_ = null;
/**
* The connection that would connect to this.closestConnection_ if this block
* were released immediately.
* Updated on every mouse move.
* This is on the top block that is being dragged or the last block in the
* dragging stack.
* @type {Blockly.RenderedConnection}
* @private
*/
this.localConnection_ = null;
/**
* Whether the block would be deleted if it were dropped immediately.
* Updated on every mouse move.
* @type {boolean}
* @private
*/
this.wouldDeleteBlock_ = false;
/**
* Connection on the insertion marker block that corresponds to
* this.localConnection_ on the currently dragged block.
* This is part of the scratch-blocks equivalent of connection highlighting.
* @type {Blockly.RenderedConnection}
* @private
*/
this.markerConnection_ = null;
/**
* Whether we are currently highlighting the block (shadow or real) that would
* be replaced if the drag were released immediately.
* @type {boolean}
* @private
*/
this.highlightingBlock_ = false;
/**
* The block that is being highlighted for replacement, or null.
* @type {Blockly.BlockSvg}
* @private
*/
this.highlightedBlock_ = null;
/**
* The connections on the dragging blocks that are available to connect to
* other blocks. This includes all open connections on the top block, as well
* as the last connection on the block stack.
* Does not change during a drag.
* @type {!Array.<!Blockly.RenderedConnection>}
* @private
*/
this.availableConnections_ = this.initAvailableConnections_();
};
/**
* Sever all links from this object.
* @package
*/
Blockly.InsertionMarkerManager.prototype.dispose = function() {
this.topBlock_ = null;
this.workspace_ = null;
this.availableConnections_.length = 0;
this.closestConnection_ = null;
this.localConnection_ = null;
if (this.firstMarker_) {
this.firstMarker_.dispose();
this.firstMarker_ = null;
}
if (this.lastMarker_) {
this.lastMarker_.dispose();
this.lastMarker_ = null;
}
this.highlightedBlock_ = null;
};
/**
* Return whether the block would be deleted if dropped immediately, based on
* information from the most recent move event.
* @return {boolean} true if the block would be deleted if dropped immediately.
* @package
*/
Blockly.InsertionMarkerManager.prototype.wouldDeleteBlock = function() {
return this.wouldDeleteBlock_;
};
/**
* Connect to the closest connection and render the results.
* This should be called at the end of a drag.
* @package
*/
Blockly.InsertionMarkerManager.prototype.applyConnections = function() {
if (this.closestConnection_) {
// Don't fire events for insertion markers.
Blockly.Events.disable();
this.hidePreview_();
Blockly.Events.enable();
// Connect two blocks together.
this.localConnection_.connect(this.closestConnection_);
if (this.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection = this.localConnection_.isSuperior() ?
this.closestConnection_ : this.localConnection_;
inferiorConnection.getSourceBlock().connectionUiEffect();
}
}
};
/**
* Update highlighted connections based on the most recent move location.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @package
*/
Blockly.InsertionMarkerManager.prototype.update = function(dxy, deleteArea) {
var candidate = this.getCandidate_(dxy);
this.wouldDeleteBlock_ = this.shouldDelete_(candidate, deleteArea);
var shouldUpdate = this.wouldDeleteBlock_ ||
this.shouldUpdatePreviews_(candidate, dxy);
if (shouldUpdate) {
// Don't fire events for insertion marker creation or movement.
Blockly.Events.disable();
this.maybeHidePreview_(candidate);
this.maybeShowPreview_(candidate);
Blockly.Events.enable();
}
};
/**** Begin initialization functions ****/
/**
* Create an insertion marker that represents the given block.
* @param {!Blockly.BlockSvg} sourceBlock The block that the insertion marker
* will represent.
* @return {!Blockly.BlockSvg} The insertion marker that represents the given
* block.
* @private
*/
Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlock) {
var imType = sourceBlock.type;
var result = this.workspace_.newBlock(imType);
if (sourceBlock.mutationToDom) {
var oldMutationDom = sourceBlock.mutationToDom();
result.domToMutation(oldMutationDom);
}
result.setInsertionMarker(true, sourceBlock.width);
result.initSvg();
return result;
};
/**
* Populate the list of available connections on this block stack. This should
* only be called once, at the beginning of a drag.
* If the stack has more than one block, this function will populate
* lastOnStack_ and create the corresponding insertion marker.
* @return {!Array.<!Blockly.RenderedConnection>} a list of available
* connections.
* @private
*/
Blockly.InsertionMarkerManager.prototype.initAvailableConnections_ = function() {
var available = this.topBlock_.getConnections_(false);
// Also check the last connection on this stack
var lastOnStack = this.topBlock_.lastConnectionInStack();
if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) {
available.push(lastOnStack);
this.lastOnStack_ = lastOnStack;
this.lastMarker_ = this.createMarkerBlock_(lastOnStack.sourceBlock_);
}
return available;
};
/**** End initialization functions ****/
/**
* Whether the previews (insertion marker and replacement marker) should be
* updated based on the closest candidate and the current drag distance.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius. Returned by getCandidate_.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @return {boolean} whether the preview should be updated.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldUpdatePreviews_ = function(
candidate, dxy) {
var candidateLocal = candidate.local;
var candidateClosest = candidate.closest;
var radius = candidate.radius;
// Found a connection!
if (candidateLocal && candidateClosest) {
if (candidateLocal.type == Blockly.OUTPUT_VALUE) {
// Always update previews for output connections.
return true;
}
// We're already showing an insertion marker.
// Decide whether the new connection has higher priority.
if (this.localConnection_ && this.closestConnection_) {
// The connection was the same as the current connection.
if (this.closestConnection_ == candidateClosest) {
return false;
}
var xDiff = this.localConnection_.x_ + dxy.x - this.closestConnection_.x_;
var yDiff = this.localConnection_.y_ + dxy.y - this.closestConnection_.y_;
var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
// Slightly prefer the existing preview over a new preview.
return !(candidateClosest && radius > curDistance -
Blockly.CURRENT_CONNECTION_PREFERENCE);
} else if (!this.localConnection_ && !this.closestConnection_) {
// We weren't showing a preview before, but we should now.
return true;
} else {
console.error('Only one of localConnection_ and closestConnection_ was set.');
}
} else { // No connection found.
// Only need to update if we were showing a preview before.
return !!(this.localConnection_ && this.closestConnection_);
}
console.error('Returning true from shouldUpdatePreviews, but it\'s not clear why.');
return true;
};
/**
* Find the nearest valid connection, which may be the same as the current
* closest connection.
* @param {!goog.math.Coordinate} dxy Position relative to drag start,
* in workspace units.
* @return {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
*/
Blockly.InsertionMarkerManager.prototype.getCandidate_ = function(dxy) {
var radius = this.getStartRadius_();
var candidateClosest = null;
var candidateLocal = null;
for (var i = 0; i < this.availableConnections_.length; i++) {
var myConnection = this.availableConnections_[i];
var neighbour = myConnection.closest(radius, dxy);
if (neighbour.connection) {
candidateClosest = neighbour.connection;
candidateLocal = myConnection;
radius = neighbour.radius;
}
}
return {
closest: candidateClosest,
local: candidateLocal,
radius: radius
};
};
/**
* Decide the radius at which to start searching for the closest connection.
* @return {number} The radius at which to start the search for the closest
* connection.
* @private
*/
Blockly.InsertionMarkerManager.prototype.getStartRadius_ = function() {
// If there is already a connection highlighted,
// increase the radius we check for making new connections.
// Why? When a connection is highlighted, blocks move around when the insertion
// marker is created, which could cause the connection became out of range.
// By increasing radiusConnection when a connection already exists,
// we never "lose" the connection from the offset.
if (this.closestConnection_ && this.localConnection_) {
return Blockly.CONNECTING_SNAP_RADIUS;
}
return Blockly.SNAP_RADIUS;
};
/**
* Whether ending the drag would replace a block or insert a block.
* @return {boolean} True if dropping the block immediately would replace
* another block. False if dropping the block immediately would result in
* the block being inserted in a block stack.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldReplace_ = function() {
var closest = this.closestConnection_;
var local = this.localConnection_;
// Dragging a block over an existing block in an input should replace the
// existing block and bump it out.
if (local.type == Blockly.OUTPUT_VALUE) {
return true; // Replace.
}
// Connecting to a statement input of c-block is an insertion, even if that
// c-block is terminal (e.g. forever).
if (local == local.sourceBlock_.getFirstStatementConnection()) {
return false; // Insert.
}
// Dragging a terminal block over another (connected) terminal block will
// replace, not insert.
var isTerminalBlock = !this.topBlock_.nextConnection;
var isConnectedTerminal = isTerminalBlock &&
local.type == Blockly.PREVIOUS_STATEMENT && closest.isConnected();
if (isConnectedTerminal) {
return true; // Replace.
}
// Otherwise it's an insertion.
return false;
};
/**
* Whether ending the drag would delete the block.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @return {boolean} True if dropping the block immediately would replace
* delete the block. False otherwise.
* @private
*/
Blockly.InsertionMarkerManager.prototype.shouldDelete_ = function(candidate,
deleteArea) {
// Prefer connecting over dropping into the trash can, but prefer dragging to
// the toolbox over connecting to other blocks.
var wouldConnect = candidate && !!candidate.closest &&
deleteArea != Blockly.DELETE_AREA_TOOLBOX;
var wouldDelete = !!deleteArea && !this.topBlock_.getParent() &&
this.topBlock_.isDeletable();
return wouldDelete && !wouldConnect;
};
/**** Begin preview visibility functions ****/
/**
* Show an insertion marker or replacement highlighting during a drag, if
* needed.
* At the beginning of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @private
*/
Blockly.InsertionMarkerManager.prototype.maybeShowPreview_ = function(candidate) {
// Nope, don't add a marker.
if (this.wouldDeleteBlock_) {
return;
}
var closest = candidate.closest;
var local = candidate.local;
// Nothing to connect to.
if (!closest) {
return;
}
// Something went wrong and we're trying to connect to an invalid connection.
if (closest == this.closestConnection_ ||
closest.sourceBlock_.isInsertionMarker()) {
return;
}
// Add an insertion marker or replacement marker.
this.closestConnection_ = closest;
this.localConnection_ = local;
this.showPreview_();
};
/**
* A preview should be shown. This function figures out if it should be a block
* highlight or an insertion marker, and shows the appropriate one.
* @private
*/
Blockly.InsertionMarkerManager.prototype.showPreview_ = function() {
if (this.shouldReplace_()) {
this.highlightBlock_();
} else { // Should insert
this.connectMarker_();
}
};
/**
* Show an insertion marker or replacement highlighting during a drag, if
* needed.
* At the end of this function, this.localConnection_ and
* this.closestConnection_ should both be null.
* @param {!Object} candidate An object containing a local connection, a closest
* connection, and a radius.
* @private
*/
Blockly.InsertionMarkerManager.prototype.maybeHidePreview_ = function(candidate) {
// If there's no new preview, remove the old one but don't bother deleting it.
// We might need it later, and this saves disposing of it and recreating it.
if (!candidate.closest) {
this.hidePreview_();
}
// If there's a new preview and there was an preview before, and either
// connection has changed, remove the old preview.
var hadPreview = this.closestConnection_ && this.localConnection_;
var closestChanged = this.closestConnection_ != candidate.closest;
var localChanged = this.localConnection_ != candidate.local;
// Also hide if we had a preview before but now we're going to delete instead.
if (hadPreview && (closestChanged || localChanged || this.wouldDeleteBlock_)) {
this.hidePreview_();
}
// Either way, clear out old state.
this.markerConnection_ = null;
this.closestConnection_ = null;
this.localConnection_ = null;
};
/**
* A preview should be hidden. This function figures out if it is a block
* highlight or an insertion marker, and hides the appropriate one.
* @private
*/
Blockly.InsertionMarkerManager.prototype.hidePreview_ = function() {
if (this.highlightingBlock_) {
this.unhighlightBlock_();
} else if (this.markerConnection_) {
this.disconnectMarker_();
}
};
/**** End preview visibility functions ****/
/**** Begin block highlighting functions ****/
/**
* Add highlighting showing which block will be replaced.
* Scratch-specific code, where "highlighting" applies to a block rather than
* a connection.
*/
Blockly.InsertionMarkerManager.prototype.highlightBlock_ = function() {
var closest = this.closestConnection_;
var local = this.localConnection_;
if (closest.targetBlock()) {
this.highlightedBlock_ = closest.targetBlock();
closest.targetBlock().highlightForReplacement(true);
} else if(local.type == Blockly.OUTPUT_VALUE) {
this.highlightedBlock_ = closest.sourceBlock_;
closest.sourceBlock_.highlightShapeForInput(closest, true);
}
this.highlightingBlock_ = true;
};
/**
* Get rid of the highlighting marking the block that will be replaced.
* Scratch-specific code, where "highlighting" applies to a block rather than
* a connection.
*/
Blockly.InsertionMarkerManager.prototype.unhighlightBlock_ = function() {
var closest = this.closestConnection_;
// If there's no block in place, but we're still connecting to a value input,
// then we must have been highlighting an input shape.
if (closest.type == Blockly.INPUT_VALUE && !closest.isConnected()) {
this.highlightedBlock_.highlightShapeForInput(closest, false);
} else {
this.highlightedBlock_.highlightForReplacement(false);
}
this.highlightedBlock_ = null;
this.highlightingBlock_ = false;
};
/**** End block highlighting functions ****/
/**** Begin insertion marker display functions ****/
/**
* Disconnect the insertion marker block in a manner that returns the stack to
* original state.
* @private
*/
Blockly.InsertionMarkerManager.prototype.disconnectMarker_ = function() {
if (!this.markerConnection_) {
console.log('No insertion marker connection to disconnect');
return;
}
var imConn = this.markerConnection_;
var imBlock = imConn.sourceBlock_;
var markerNext = imBlock.nextConnection;
var markerPrev = imBlock.previousConnection;
// The insertion marker is the first block in a stack, either because it
// doesn't have a previous connection or because the previous connection is
// not connected. Unplug won't do anything in that case. Instead, unplug the
// following block.
if (imConn == markerNext && !(markerPrev && markerPrev.targetConnection)) {
imConn.targetBlock().unplug(false);
}
// Inside of a C-block, first statement connection.
else if (imConn.type == Blockly.NEXT_STATEMENT && imConn != markerNext) {
var innerConnection = imConn.targetConnection;
innerConnection.sourceBlock_.unplug(false);
var previousBlockNextConnection =
markerPrev ? markerPrev.targetConnection : null;
imBlock.unplug(true);
if (previousBlockNextConnection) {
previousBlockNextConnection.connect(innerConnection);
}
} else {
imBlock.unplug(true /* healStack */);
}
if (imConn.targetConnection) {
throw 'markerConnection_ still connected at the end of disconnectInsertionMarker';
}
this.markerConnection_ = null;
imBlock.getSvgRoot().setAttribute('visibility', 'hidden');
};
/**
* Add an insertion marker connected to the appropriate blocks.
* @private
*/
Blockly.InsertionMarkerManager.prototype.connectMarker_ = function() {
var local = this.localConnection_;
var closest = this.closestConnection_;
var isLastInStack = this.lastOnStack_ && local == this.lastOnStack_;
var imBlock = isLastInStack ? this.lastMarker_ : this.firstMarker_;
var imConn = imBlock.getMatchingConnection(local.sourceBlock_, local);
goog.asserts.assert(imConn != this.markerConnection_,
'Made it to connectMarker_ even though the marker isn\'t changing');
// Render disconnected from everything else so that we have a valid
// connection location.
imBlock.render();
imBlock.rendered = true;
imBlock.getSvgRoot().setAttribute('visibility', 'visible');
// TODO: positionNewBlock should be on Blockly.BlockSvg, not prototype,
// because it doesn't rely on anything in the block it's called on.
imBlock.positionNewBlock(imBlock, imConn, closest);
// Connect() also renders the insertion marker.
imConn.connect(closest);
this.markerConnection_ = imConn;
};
/**** End insertion marker display functions ****/

View file

@ -294,7 +294,7 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
* @private
*/
Blockly.Mutator.prototype.workspaceChanged_ = function() {
if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
if (!this.workspace_.isDragging()) {
var blocks = this.workspace_.getTopBlocks(false);
var MARGIN = 20;
for (var b = 0, block; block = blocks[b]; b++) {
@ -338,7 +338,11 @@ Blockly.Mutator.prototype.workspaceChanged_ = function() {
if (block.rendered) {
block.render();
}
this.resizeBubble_();
// Don't update the bubble until the drag has ended, to avoid moving blocks
// under the cursor.
if (!this.workspace_.isDragging()) {
this.resizeBubble_();
}
Blockly.Events.setGroup(false);
}
};

View file

@ -69,7 +69,7 @@ Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) {
* @private
*/
Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) {
if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
if (this.sourceBlock_.workspace.isDragging()) {
// Don't move blocks around while the user is doing the same.
return;
}
@ -168,6 +168,7 @@ Blockly.RenderedConnection.prototype.tighten_ = function() {
if (!svgRoot) {
throw 'block is not rendered.';
}
// Workspace coordinates.
var xy = Blockly.utils.getRelativeXY(svgRoot);
block.getSvgRoot().setAttribute('transform',
'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
@ -177,7 +178,7 @@ Blockly.RenderedConnection.prototype.tighten_ = function() {
/**
* Find the closest compatible connection to this connection.
* All parameters are in workspace units
* All parameters are in workspace units.
* @param {number} maxLimit The maximum radius to another connection.
* @param {!goog.math.Coordinate} dxy Offset between this connection's location
* in the database and the current location (as a result of dragging).

View file

@ -248,6 +248,7 @@ Blockly.Scrollbar.prototype.originHasChanged_ = true;
/**
* The size of the area within which the scrollbar handle can move.
* Coordinate system: pixel coordinates.
* @type {number}
* @private
*/
@ -255,6 +256,7 @@ Blockly.Scrollbar.prototype.scrollViewSize_ = 0;
/**
* The length of the scrollbar handle.
* Coordinate system: pixel coordinates.
* @type {number}
* @private
*/
@ -262,6 +264,7 @@ Blockly.Scrollbar.prototype.handleLength_ = 0;
/**
* The offset of the start of the handle from the start of the scrollbar range.
* Coordinate system: pixel coordinates.
* @type {number}
* @private
*/
@ -284,7 +287,6 @@ Blockly.Scrollbar.prototype.containerVisible_ = true;
/**
* Width of vertical scrollbar or height of horizontal scrollbar.
* Increase the size of scrollbars on touch devices.
* Don't define if there is no document object (e.g. node.js).
*/
Blockly.Scrollbar.scrollbarThickness = 11;
if (goog.events.BrowserFeature.TOUCH_ENABLED) {

View file

@ -257,6 +257,24 @@ Blockly.Toolbox.prototype.removeDeleteStyle = function() {
'blocklyToolboxDelete');
};
/**
* Adds styles on the toolbox indicating blocks will be deleted.
* @package
*/
Blockly.Toolbox.prototype.addDeleteStyle = function() {
Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv),
'blocklyToolboxDelete');
};
/**
* Remove styles from the toolbox that indicate blocks will be deleted.
* @package
*/
Blockly.Toolbox.prototype.removeDeleteStyle = function() {
Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv),
'blocklyToolboxDelete');
};
/**
* Return the deletion rectangle for this toolbox.
* @return {goog.math.Rect} Rectangle in which to delete.

View file

@ -44,6 +44,13 @@ goog.require('goog.dom.TagName');
*/
Blockly.Tooltip.visible = false;
/**
* Is someone else blocking the tooltip from being shown?
* @type {boolean}
* @private
*/
Blockly.Tooltip.blocked_ = false;
/**
* Maximum width (in characters) of a tooltip.
*/
@ -153,6 +160,10 @@ Blockly.Tooltip.bindMouseEvents = function(element) {
* @private
*/
Blockly.Tooltip.onMouseOver_ = function(e) {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
// If the tooltip is an object, treat it as a pointer to the next object in
// the chain to look at. Terminate when a string or function is found.
var element = e.target;
@ -174,6 +185,10 @@ Blockly.Tooltip.onMouseOver_ = function(e) {
* @private
*/
Blockly.Tooltip.onMouseOut_ = function(/*e*/) {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
// Moving from one element to another (overlapping or with no gap) generates
// a mouseOut followed instantly by a mouseOver. Fork off the mouseOut
// event and kill it if a mouseOver is received immediately.
@ -196,12 +211,13 @@ Blockly.Tooltip.onMouseMove_ = function(e) {
if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) {
// No tooltip here to show.
return;
} else if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
// Don't display a tooltip during a drag.
return;
} else if (Blockly.WidgetDiv.isVisible()) {
// Don't display a tooltip if a widget is open (tooltip would be under it).
return;
} else if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips. We are probably handling a
// user gesture, such as a click or drag.
return;
}
if (Blockly.Tooltip.visible) {
// Compute the distance between the mouse position when the tooltip was
@ -235,11 +251,34 @@ Blockly.Tooltip.hide = function() {
clearTimeout(Blockly.Tooltip.showPid_);
};
/**
* Hide any in-progress tooltips and block showing new tooltips until the next
* call to unblock().
* @package
*/
Blockly.Tooltip.block = function() {
Blockly.Tooltip.hide();
Blockly.Tooltip.blocked_ = true;
};
/**
* Unblock tooltips: allow them to be scheduled and shown according to their own
* logic.
* @package
*/
Blockly.Tooltip.unblock = function() {
Blockly.Tooltip.blocked_ = false;
};
/**
* Create the tooltip and show it.
* @private
*/
Blockly.Tooltip.show_ = function() {
if (Blockly.Tooltip.blocked_) {
// Someone doesn't want us to show tooltips.
return;
}
Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_;
if (!Blockly.Tooltip.DIV) {
return;

View file

@ -41,13 +41,6 @@ goog.require('goog.string');
*/
Blockly.Touch.touchIdentifier_ = null;
/**
* Wrapper function called when a touch mouseUp occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.Touch.onTouchUpWrapper_ = null;
/**
* The TOUCH_MAP lookup dictionary specifies additional touch events to fire,
* in conjunction with mouse events.
@ -75,11 +68,10 @@ Blockly.longPid_ = 0;
* which after about a second opens the context menu. The tasks is killed
* if the touch event terminates early.
* @param {!Event} e Touch start event.
* @param {!Blockly.Block|!Blockly.WorkspaceSvg} uiObject The block or workspace
* under the touchstart event.
* @param {Blockly.Gesture} gesture The gesture that triggered this longStart.
* @private
*/
Blockly.longStart_ = function(e, uiObject) {
Blockly.longStart_ = function(e, gesture) {
Blockly.longStop_();
// Punt on multitouch events.
if (e.changedTouches.length != 1) {
@ -90,7 +82,11 @@ Blockly.longStart_ = function(e, uiObject) {
// e was a touch event. It needs to pretend to be a mouse event.
e.clientX = e.changedTouches[0].clientX;
e.clientY = e.changedTouches[0].clientY;
uiObject.onMouseDown_(e);
// Let the gesture route the right-click correctly.
if (gesture) {
gesture.handleRightClick(e);
}
}, Blockly.LONGPRESS);
};
@ -106,56 +102,6 @@ Blockly.longStop_ = function() {
}
};
/**
* Handle a mouse-up anywhere on the page.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.onMouseUp_ = function(/* e */) {
var workspace = Blockly.getMainWorkspace();
if (workspace.dragMode_ == Blockly.DRAG_NONE) {
return;
}
Blockly.Touch.clearTouchIdentifier();
// TODO(#781): Check whether this needs to be called for all drag modes.
workspace.resetDragSurface();
workspace.dragMode_ = Blockly.DRAG_NONE;
// Unbind the touch event if it exists.
if (Blockly.Touch.onTouchUpWrapper_) {
Blockly.unbindEvent_(Blockly.Touch.onTouchUpWrapper_);
Blockly.Touch.onTouchUpWrapper_ = null;
}
if (Blockly.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.onMouseMoveWrapper_);
Blockly.onMouseMoveWrapper_ = null;
}
};
/**
* Handle a mouse-move on SVG drawing surface.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.onMouseMove_ = function(e) {
var workspace = Blockly.getMainWorkspace();
if (workspace.dragMode_ != Blockly.DRAG_NONE) {
var dx = e.clientX - workspace.startDragMouseX;
var dy = e.clientY - workspace.startDragMouseY;
var x = workspace.startScrollX + dx;
var y = workspace.startScrollY + dy;
workspace.scroll(x, y);
// Cancel the long-press if the drag has moved too far.
if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
Blockly.longStop_();
workspace.dragMode_ = Blockly.DRAG_FREE;
}
e.stopPropagation();
e.preventDefault();
}
};
/**
* Clear the touch identifier that tracks which touch stream to pay attention
* to. This ends the current drag/gesture and allows other pointers to be

View file

@ -281,7 +281,7 @@ Blockly.utils.getRelativeXY.XY_2D_REGEX_ =
* @param {Element} parent Optional parent on which to append the element.
* @return {!SVGElement} Newly created SVG element.
*/
Blockly.utils.createSvgElement = function(name, attrs, parent) {
Blockly.utils.createSvgElement = function(name, attrs, parent /*, opt_workspace */) {
var e = /** @type {!SVGElement} */ (
document.createElementNS(Blockly.SVG_NS, name));
for (var key in attrs) {

226
core/variable_map.js Normal file
View file

@ -0,0 +1,226 @@
/**
* @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 Object representing a map of variables and their types.
* @author marisaleung@google.com (Marisa Leung)
*/
'use strict';
goog.provide('Blockly.VariableMap');
goog.require('Blockly.VariableModel');
/**
* Class for a variable map. This contains a dictionary data structure with
* variable types as keys and lists of variables as values. The list of
* variables are the type indicated by the key.
* @constructor
*/
Blockly.VariableMap = function() {
/**
* @type {!Object<string, !Array.<Blockly.VariableModel>>}
* A map from variable type to list of variable names. The lists contain all
* of the named variables in the workspace, including variables
* that are not currently in use.
* @private
*/
this.variableMap_ = {};
};
/**
* Clear the variable map.
*/
Blockly.VariableMap.prototype.clear = function() {
this.variableMap_ = new Object(null);
};
/**
* Rename the given variable by updating its name in the variable map.
* TODO: #468
* @param {?Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
*/
Blockly.VariableMap.prototype.renameVariable = function(variable, newName) {
var newVariable = this.getVariable(newName);
var variableIndex = -1;
var newVariableIndex = -1;
var type = '';
if (variable || newVariable) {
type = (variable || newVariable).type;
}
var variableList = this.getVariablesOfType(type);
if (variable) {
variableIndex = variableList.indexOf(variable);
}
if (newVariable){ // see if I can get rid of newVariable dependency
newVariableIndex = variableList.indexOf(newVariable);
}
if (variableIndex == -1 && newVariableIndex == -1) {
this.createVariable(newName, '');
console.log('Tried to rename an non-existent variable.');
} else if (variableIndex == newVariableIndex ||
variableIndex != -1 && newVariableIndex == -1) {
// Only changing case, or renaming to a completely novel name.
this.variableMap_[type][variableIndex].name = newName;
} else if (variableIndex != -1 && newVariableIndex != -1) {
// Renaming one existing variable to another existing variable.
// The case might have changed, so we update the destination ID.
this.variableMap_[type][newVariableIndex].name = newName;
this.variableMap_[type].splice(variableIndex, 1);
}
};
/**
* Create a variable with a given name, optional type, and optional id.
* @param {!string} name The name of the variable. This must be unique across
* variables and procedures.
* @param {?string} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {?string} opt_id The unique id of the variable. This will default to
* a UUID.
* @return {?Blockly.VariableModel} The newly created variable.
*/
Blockly.VariableMap.prototype.createVariable = function(name, opt_type, opt_id) {
var variable = this.getVariable(name);
if (variable) {
if (opt_type && variable.type != opt_type) {
throw Error('Variable "' + name + '" is already in use and its type is "'
+ variable.type + '" which conflicts with the passed in ' +
'type, "' + opt_type + '".');
}
if (opt_id && variable.getId() != opt_id) {
throw Error('Variable "' + name + '" is already in use and its id is "'
+ variable.getId() + '" which conflicts with the passed in ' +
'id, "' + opt_id + '".');
}
// The variable already exists and has the same id and type.
return variable;
}
if (opt_id && this.getVariableById(opt_id)) {
throw Error('Variable id, "' + opt_id + '", is already in use.');
}
opt_id = opt_id || Blockly.utils.genUid();
opt_type = opt_type || '';
variable = new Blockly.VariableModel(name, opt_type, opt_id);
// If opt_type is not a key, create a new list.
if (!this.variableMap_[opt_type]) {
this.variableMap_[opt_type] = [variable];
} else {
// Else append the variable to the preexisting list.
this.variableMap_[opt_type].push(variable);
}
return variable;
};
/**
* Delete a variable.
* @param {Blockly.VariableModel} variable Variable to delete.
*/
Blockly.VariableMap.prototype.deleteVariable = function(variable) {
var variableList = this.variableMap_[variable.type];
for (var i = 0, tempVar; tempVar = variableList[i]; i++) {
if (tempVar.getId() == variable.getId()) {
variableList.splice(i, 1);
return;
}
}
};
/**
* Find the variable by the given name and return it. Return null if it is not
* found.
* @param {!string} name The name to check for.
* @return {?Blockly.VariableModel} The variable with the given name, or null if
* it was not found.
*/
Blockly.VariableMap.prototype.getVariable = function(name) {
var keys = Object.keys(this.variableMap_);
for (var i = 0; i < keys.length; i++ ) {
var key = keys[i];
for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) {
if (Blockly.Names.equals(variable.name, name)) {
return variable;
}
}
}
return null;
};
/**
* Find the variable by the given id and return it. Return null if it is not
* found.
* @param {!string} id The id to check for.
* @return {?Blockly.VariableModel} The variable with the given id.
*/
Blockly.VariableMap.prototype.getVariableById = function(id) {
var keys = Object.keys(this.variableMap_);
for (var i = 0; i < keys.length; i++ ) {
var key = keys[i];
for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) {
if (variable.getId() == id) {
return variable;
}
}
}
return null;
};
/**
* Get a list containing all of the variables of a specified type. If type is
* null, return list of variables with empty string type.
* @param {?string} type Type of the variables to find.
* @return {Array.<Blockly.VariableModel>} The sought after variables of the
* passed in type. An empty array if none are found.
*/
Blockly.VariableMap.prototype.getVariablesOfType = function(type) {
type = type || '';
var variable_list = this.variableMap_[type];
if (variable_list) {
return variable_list;
}
return [];
};
/**
* Return all variable types.
* @return {!Array.<string>} List of variable types.
*/
Blockly.VariableMap.prototype.getVariableTypes = function() {
return Object.keys(this.variableMap_);
};
/**
* Return all variables of all types.
* @return {!Array.<Blockly.VariableModel>} List of variable models.
*/
Blockly.VariableMap.prototype.getAllVariables = function() {
var all_variables = [];
var keys = Object.keys(this.variableMap_);
for (var i = 0; i < keys.length; i++ ) {
all_variables = all_variables.concat(this.variableMap_[keys[i]]);
}
return all_variables;
};

View file

@ -90,7 +90,7 @@ Blockly.Variables.allUsedVariables = function(root) {
* Find all variables that the user has created through the workspace or
* toolbox. For use by generators.
* @param {!Blockly.Workspace} root The workspace to inspect.
* @return {!Array.<string>} Array of variable names.
* @return {!Array.<Blockly.VariableModel>} Array of variable models.
*/
Blockly.Variables.allVariables = function(root) {
if (root instanceof Blockly.Block) {
@ -98,8 +98,9 @@ Blockly.Variables.allVariables = function(root) {
console.warn('Deprecated call to Blockly.Variables.allVariables ' +
'with a block instead of a workspace. You may want ' +
'Blockly.Variables.allUsedVariables');
return {};
}
return root.variableList;
return root.getAllVariables();
};
/**
@ -108,8 +109,12 @@ Blockly.Variables.allVariables = function(root) {
* @return {!Array.<!Element>} Array of XML block elements.
*/
Blockly.Variables.flyoutCategory = function(workspace) {
var variableList = workspace.variableList;
variableList.sort(goog.string.caseInsensitiveCompare);
var variableNameList = [];
var variableModelList = workspace.getVariablesOfType('');
for (var i = 0; i < variableModelList.length; i++) {
variableNameList.push(variableModelList[i].name);
}
variableNameList.sort(goog.string.caseInsensitiveCompare);
var xmlList = [];
var button = goog.dom.createDom('button');
@ -122,8 +127,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
xmlList.push(button);
for (var i = 0; i < variableList.length; i++) {
for (var i = 0; i < variableNameList.length; i++) {
if (Blockly.Blocks['data_variable']) {
// <block type="data_variable">
// <field name="VARIABLE">variablename</field>
@ -132,7 +136,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
block.setAttribute('type', 'data_variable');
block.setAttribute('gap', 8);
var field = goog.dom.createDom('field', null, variableList[i]);
var field = goog.dom.createDom('field', null, variableNameList[i]);
field.setAttribute('name', 'VARIABLE');
block.appendChild(field);
@ -157,7 +161,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
var block = goog.dom.createDom('block');
block.setAttribute('type', 'data_setvariableto');
block.setAttribute('gap', 8);
block.appendChild(Blockly.Variables.createVariableDom_(variableList[0]));
block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
block.appendChild(Blockly.Variables.createTextDom_());
xmlList.push(block);
}
@ -175,7 +179,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
var block = goog.dom.createDom('block');
block.setAttribute('type', 'data_changevariableby');
block.setAttribute('gap', 8);
block.appendChild(Blockly.Variables.createVariableDom_(variableList[0]));
block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
block.appendChild(Blockly.Variables.createMathNumberDom_());
xmlList.push(block);
}
@ -188,7 +192,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
var block = goog.dom.createDom('block');
block.setAttribute('type', 'data_showvariable');
block.setAttribute('gap', 8);
block.appendChild(Blockly.Variables.createVariableDom_(variableList[0]));
block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
xmlList.push(block);
}
if (Blockly.Blocks['data_hidevariable']) {
@ -199,7 +203,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
// </block>
var block = goog.dom.createDom('block');
block.setAttribute('type', 'data_hidevariable');
block.appendChild(Blockly.Variables.createVariableDom_(variableList[0]));
block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
xmlList.push(block);
}
}
@ -307,7 +311,7 @@ Blockly.Variables.noVariableText = function() {
* @return {string} New variable name.
*/
Blockly.Variables.generateUniqueName = function(workspace) {
var variableList = workspace.variableList;
var variableList = workspace.getAllVariables();
var newName = '';
if (variableList.length) {
var nameSuffix = 1;
@ -317,7 +321,7 @@ Blockly.Variables.generateUniqueName = function(workspace) {
while (!newName) {
var inUse = false;
for (var i = 0; i < variableList.length; i++) {
if (variableList[i].toLowerCase() == potName) {
if (variableList[i].name.toLowerCase() == potName) {
// This potential name is already used.
inUse = true;
break;
@ -360,13 +364,21 @@ Blockly.Variables.createVariable = function(workspace, opt_callback) {
Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, defaultName,
function(text) {
if (text) {
if (workspace.variableIndexOf(text) != -1) {
if (workspace.getVariable(text)) {
Blockly.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1',
text.toLowerCase()),
function() {
promptAndCheckWithAlert(text); // Recurse
});
} else {
}
else if (!Blockly.Procedures.isLegalName_(text, workspace)) {
Blockly.alert(Blockly.Msg.PROCEDURE_ALREADY_EXISTS.replace('%1',
text.toLowerCase()),
function() {
promptAndCheckWithAlert(text); // Recurse
});
}
else {
workspace.createVariable(text);
if (opt_callback) {
opt_callback(text);

View file

@ -26,6 +26,7 @@
goog.provide('Blockly.Workspace');
goog.require('Blockly.VariableMap');
goog.require('goog.array');
goog.require('goog.math');
@ -80,12 +81,15 @@ Blockly.Workspace = function(opt_options) {
* @private
*/
this.blockDB_ = Object.create(null);
/*
* @type {!Array.<string>}
* A list of all of the named variables in the workspace, including variables
/**
* @type {!Blockly.VariableMap}
* A map from variable type to list of variable names. The lists contain all
* of the named variables in the workspace, including variables
* that are not currently in use.
* @private
*/
this.variableList = [];
this.variableMap_ = new Blockly.VariableMap();
};
/**
@ -121,19 +125,20 @@ Blockly.Workspace.SCAN_ANGLE = 3;
/**
* Add a block to the list of top blocks.
* @param {!Blockly.Block} block Block to remove.
* @param {!Blockly.Block} block Block to add.
*/
Blockly.Workspace.prototype.addTopBlock = function(block) {
this.topBlocks_.push(block);
if (this.isFlyout) {
// This is for the (unlikely) case where you have a variable in a block in
// an always-open flyout. It needs to be possible to edit the block in the
// flyout, so the contents of the dropdown need to be correct.
var variables = Blockly.Variables.allUsedVariables(block);
for (var i = 0; i < variables.length; i++) {
if (this.variableList.indexOf(variables[i]) == -1) {
this.variableList.push(variables[i]);
}
if (!this.isFlyout) {
return;
}
// This is for the (unlikely) case where you have a variable in a block in
// an always-open flyout. It needs to be possible to edit the block in the
// flyout, so the contents of the dropdown need to be correct.
var variableNames = Blockly.Variables.allUsedVariables(block);
for (var i = 0, name; name = variableNames[i]; i++) {
if (!this.getVariable(name)) {
this.createVariable(name);
}
}
};
@ -197,7 +202,7 @@ Blockly.Workspace.prototype.clear = function() {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
this.variableList.length = 0;
this.variableMap_.clear();
// Any block with a drop-down or WidgetDiv was disposed.
if (Blockly.DropDownDiv) {
Blockly.DropDownDiv.hideWithoutAnimation();
@ -208,83 +213,119 @@ Blockly.Workspace.prototype.clear = function() {
};
/**
* Walk the workspace and update the list of variables to include all variables
* in use on the workspace. Use when loading new workspaces from disk.
* @param {boolean} clearList True if the old variable list should be cleared.
* Walk the workspace and update the map of variables to only contain ones in
* use on the workspace. Use when loading new workspaces from disk.
* @param {boolean} clear True if the old variable map should be cleared.
*/
Blockly.Workspace.prototype.updateVariableList = function(clearList) {
Blockly.Workspace.prototype.updateVariableStore = function(clear) {
// TODO: Sort
if (!this.isFlyout) {
// Update the list in place so that the flyout's references stay correct.
if (clearList) {
this.variableList.length = 0;
if (this.isFlyout) {
return;
}
var variableNames = Blockly.Variables.allUsedVariables(this);
var varList = [];
for (var i = 0, name; name = variableNames[i]; i++) {
// Get variable model with the used variable name.
var tempVar = this.getVariable(name);
if (tempVar) {
varList.push({'name': tempVar.name, 'type': tempVar.type,
'id': tempVar.getId()});
}
var allVariables = Blockly.Variables.allUsedVariables(this);
for (var i = 0; i < allVariables.length; i++) {
this.createVariable(allVariables[i]);
else {
varList.push({'name': name, 'type': null, 'id': null});
// TODO(marisaleung): Use variable.type and variable.getId() once variable
// instances are storing more than just name.
}
}
if (clear) {
this.variableMap_.clear();
}
// Update the list in place so that the flyout's references stay correct.
for (var i = 0, varDict; varDict = varList[i]; i++) {
if (!this.getVariable(varDict.name)) {
this.createVariable(varDict.name, varDict.type, varDict.id);
}
}
};
/**
* Rename a variable by updating its name in the variable list.
* Rename a variable by updating its name in the variable map. Identify the
* variable to rename with the given variable.
* TODO: #468
* @param {string} oldName Variable to rename.
* @param {?Blockly.VariableModel} variable Variable to rename.
* @param {string} newName New variable name.
*/
Blockly.Workspace.prototype.renameVariable = function(oldName, newName) {
// Find the old name in the list.
var variableIndex = this.variableIndexOf(oldName);
var newVariableIndex = this.variableIndexOf(newName);
Blockly.Workspace.prototype.renameVariableInternal_ = function(variable, newName) {
var newVariable = this.getVariable(newName);
var oldCase;
// We might be renaming to an existing name but with different case. If so,
// we will also update all of the blocks using the new name to have the
// correct case.
if (newVariableIndex != -1 &&
this.variableList[newVariableIndex] != newName) {
var oldCase = this.variableList[newVariableIndex];
// If they are different types, throw an error.
if (variable && newVariable && variable.type != newVariable.type) {
throw Error('Variable "' + variable.name + '" is type "' + variable.type +
'" and variable "' + newName + '" is type "' + newVariable.type +
'". Both must be the same type.');
}
// Find if newVariable case is different.
if (newVariable && newVariable.name != newName) {
oldCase = newVariable.name;
}
Blockly.Events.setGroup(true);
var blocks = this.getAllBlocks();
// Iterate through every block.
// Iterate through every block and update name.
for (var i = 0; i < blocks.length; i++) {
blocks[i].renameVar(oldName, newName);
blocks[i].renameVar(variable.name, newName);
if (oldCase) {
blocks[i].renameVar(oldCase, newName);
}
}
Blockly.Events.setGroup(false);
this.variableMap_.renameVariable(variable, newName);
};
if (variableIndex == newVariableIndex ||
variableIndex != -1 && newVariableIndex == -1) {
// Only changing case, or renaming to a completely novel name.
this.variableList[variableIndex] = newName;
} else if (variableIndex != -1 && newVariableIndex != -1) {
// Renaming one existing variable to another existing variable.
// The case might have changed, so we update the destination ID.
this.variableList[newVariableIndex] = newName;
this.variableList.splice(variableIndex, 1);
} else {
this.variableList.push(newName);
console.log('Tried to rename an non-existent variable.');
}
/**
* Rename a variable by updating its name in the variable map. Identify the
* variable to rename with the given name.
* TODO: #468
* @param {string} oldName Variable to rename.
* @param {string} newName New variable name.
*/
Blockly.Workspace.prototype.renameVariable = function(oldName, newName) {
// Warning: Prefer to use renameVariableById.
var variable = this.getVariable(oldName);
this.renameVariableInternal_(variable, newName);
};
/**
* Create a variable with the given name.
* TODO: #468
* @param {string} name The new variable's name.
* Rename a variable by updating its name in the variable map. Identify the
* variable to rename with the given id.
* @param {string} id Id of the variable to rename.
* @param {string} newName New variable name.
*/
Blockly.Workspace.prototype.createVariable = function(name) {
Blockly.Workspace.prototype.renameVariableById = function(id, newName) {
var variable = this.getVariableById(id);
this.renameVariableInternal_(variable, newName);
};
/**
* Create a variable with a given name, optional type, and optional id.
* @param {!string} name The name of the variable. This must be unique across
* variables and procedures.
* @param {?string} opt_type The type of the variable like 'int' or 'string'.
* Does not need to be unique. Field_variable can filter variables based on
* their type. This will default to '' which is a specific type.
* @param {?string} opt_id The unique id of the variable. This will default to
* a UUID.
* @return {?Blockly.VariableModel} The newly created variable.
*/
Blockly.Workspace.prototype.createVariable = function(name, opt_type, opt_id) {
if (name.toLowerCase() == Blockly.Variables.noVariableText()) {
return;
}
var index = this.variableIndexOf(name);
if (index == -1) {
this.variableList.push(name);
}
return this.variableMap_.createVariable(name, opt_type, opt_id);
};
/**
@ -312,15 +353,11 @@ Blockly.Workspace.prototype.getVariableUses = function(name) {
};
/**
* Delete a variables and all of its uses from this workspace. May prompt the
* user for confirmation.
* Delete a variable by the passed in name and all of its uses from this
* workspace. May prompt the user for confirmation.
* @param {string} name Name of variable to delete.
*/
Blockly.Workspace.prototype.deleteVariable = function(name) {
var variableIndex = this.variableIndexOf(name);
if (variableIndex == -1) {
return;
}
// Check whether this variable is a function parameter before deleting.
var uses = this.getVariableUses(name);
for (var i = 0, block; block = uses[i]; i++) {
@ -336,6 +373,7 @@ Blockly.Workspace.prototype.deleteVariable = function(name) {
}
var workspace = this;
var variable = workspace.getVariable(name);
if (uses.length > 1) {
// Confirm before deleting multiple blocks.
Blockly.confirm(
@ -343,30 +381,41 @@ Blockly.Workspace.prototype.deleteVariable = function(name) {
replace('%2', name),
function(ok) {
if (ok) {
workspace.deleteVariableInternal_(name);
workspace.deleteVariableInternal_(variable);
}
});
} else {
// No confirmation necessary for a single block.
this.deleteVariableInternal_(name);
this.deleteVariableInternal_(variable);
}
};
/**
* Delete a variables by the passed in id and all of its uses from this
* workspace. May prompt the user for confirmation.
* @param {string} id Id of variable to delete.
*/
Blockly.Workspace.prototype.deleteVariableById = function(id) {
var variable = this.getVariableById(id);
if (variable) {
this.deleteVariableInternal_(variable);
}
};
/**
* Deletes a variable and all of its uses from this workspace without asking the
* user for confirmation.
* @param {string} name The name of the variable to delete
* @param {Blockly.VariableModel} variable Variable to delete.
* @private
*/
Blockly.Workspace.prototype.deleteVariableInternal_ = function(name) {
var uses = this.getVariableUses(name);
var variableIndex = this.variableIndexOf(name);
Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable) {
var uses = this.getVariableUses(variable.name);
Blockly.Events.setGroup(true);
for (var i = 0; i < uses.length; i++) {
uses[i].dispose(true, false);
}
Blockly.Events.setGroup(false);
this.variableList.splice(variableIndex, 1);
this.variableMap_.deleteVariable(variable);
};
/**
@ -375,16 +424,34 @@ Blockly.Workspace.prototype.deleteVariableInternal_ = function(name) {
* @param {string} name The name to check for.
* @return {number} The index of the name in the variable list, or -1 if it is
* not present.
* @deprecated April 2017
*/
Blockly.Workspace.prototype.variableIndexOf = function(name) {
for (var i = 0, varname; varname = this.variableList[i]; i++) {
if (Blockly.Names.equals(varname, name)) {
return i;
}
}
Blockly.Workspace.prototype.variableIndexOf = function(/* name */) {
console.warn(
'Deprecated call to Blockly.Workspace.prototype.variableIndexOf');
return -1;
};
/**
* Find the variable by the given name and return it. Return null if it is not
* found.
* @param {!string} name The name to check for.
* @return {?Blockly.VariableModel} the variable with the given name.
*/
Blockly.Workspace.prototype.getVariable = function(name) {
return this.variableMap_.getVariable(name);
};
/**
* Find the variable by the given id and return it. Return null if it is not
* found.
* @param {!string} id The id to check for.
* @return {?Blockly.VariableModel} The variable with the given id.
*/
Blockly.Workspace.prototype.getVariableById = function(id) {
return this.variableMap_.getVariableById(id);
};
/**
* Returns the horizontal offset of the workspace.
* Intended for LTR/RTL compatibility in XML.
@ -523,6 +590,50 @@ Blockly.Workspace.prototype.getFlyout = function() {
return null;
};
/**
* Checks whether all value and statement inputs in the workspace are filled
* with blocks.
* @param {boolean=} opt_shadowBlocksAreFilled An optional argument controlling
* whether shadow blocks are counted as filled. Defaults to true.
* @return {boolean} True if all inputs are filled, false otherwise.
*/
Blockly.Workspace.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) {
var blocks = this.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
if (!block.allInputsFilled(opt_shadowBlocksAreFilled)) {
return false;
}
}
return true;
};
/**
* Find the variable with the specified type. If type is null, return list of
* variables with empty string type.
* @param {?string} type Type of the variables to find.
* @return {Array.<Blockly.VariableModel>} The sought after variables of the
* passed in type. An empty array if none are found.
*/
Blockly.Workspace.prototype.getVariablesOfType = function(type) {
return this.variableMap_.getVariablesOfType(type);
};
/**
* Return all variable types.
* @return {!Array.<string>} List of variable types.
*/
Blockly.Workspace.prototype.getVariableTypes = function() {
return this.variableMap_.getVariableTypes();
};
/**
* Return all variables of all types.
* @return {!Array.<Blockly.VariableModel>} List of variable models.
*/
Blockly.Workspace.prototype.getAllVariables = function() {
return this.variableMap_.getAllVariables();
};
/**
* Database of all workspaces.
* @private

132
core/workspace_dragger.js Normal file
View file

@ -0,0 +1,132 @@
/**
* @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 Methods for dragging a workspace visually.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceDragger');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* Class for a workspace dragger. It moves the workspace around when it is
* being dragged by a mouse or touch.
* Note that the workspace itself manages whether or not it has a drag surface
* and how to do translations based on that. This simply passes the right
* commands based on events.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag.
* @constructor
*/
Blockly.WorkspaceDragger = function(workspace) {
/**
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* The workspace's metrics object at the beginning of the drag. Contains size
* and position metrics of a workspace.
* Coordinate system: pixel coordinates.
* @type {!Object}
* @private
*/
this.startDragMetrics_ = workspace.getMetrics();
/**
* The scroll position of the workspace at the beginning of the drag.
* Coordinate system: pixel coordinates.
* @type {!goog.math.Coordinate}
* @private
*/
this.startScrollXY_ = new goog.math.Coordinate(workspace.scrollX,
workspace.scrollY);
};
/**
* Sever all links from this object.
* @package
*/
Blockly.WorkspaceDragger.prototype.dispose = function() {
this.workspace_ = null;
};
/**
* Start dragging the workspace.
* @package
*/
Blockly.WorkspaceDragger.prototype.startDrag = function() {
if (Blockly.selected) {
Blockly.selected.unselect();
}
this.workspace_.setupDragSurface();
};
/**
* Finish dragging the workspace and put everything back where it belongs.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel coordinates.
* @package
*/
Blockly.WorkspaceDragger.prototype.endDrag = function(currentDragDeltaXY) {
// Make sure everything is up to date.
this.drag(currentDragDeltaXY);
this.workspace_.resetDragSurface();
};
/**
* Move the workspace based on the most recent mouse movements.
* @param {!goog.math.Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel coordinates.
* @package
*/
Blockly.WorkspaceDragger.prototype.drag = function(currentDragDeltaXY) {
var metrics = this.startDragMetrics_;
var newXY = goog.math.Coordinate.sum(this.startScrollXY_, currentDragDeltaXY);
// Bound the new XY based on workspace bounds.
var x = Math.min(newXY.x, -metrics.contentLeft);
var y = Math.min(newXY.y, -metrics.contentTop);
x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
metrics.contentWidth);
y = Math.max(y, metrics.viewHeight - metrics.contentTop -
metrics.contentHeight);
x = -x - metrics.contentLeft;
y = -y - metrics.contentTop;
this.updateScroll_(x, y);
};
/**
* Move the scrollbars to drag the workspace.
* x and y are in pixels.
* @param {number} x The new x position to move the scrollbar to.
* @param {number} y The new y position to move the scrollbar to.
* @private
*/
Blockly.WorkspaceDragger.prototype.updateScroll_ = function(x, y) {
this.workspace_.scrollbar.set(x, y);
};

View file

@ -34,6 +34,7 @@ goog.require('Blockly.constants');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.Events');
//goog.require('Blockly.HorizontalFlyout');
goog.require('Blockly.Gesture');
goog.require('Blockly.Options');
goog.require('Blockly.ScrollbarPair');
goog.require('Blockly.Touch');
@ -69,27 +70,6 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface
options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_;
Blockly.ConnectionDB.init(this);
if (opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
}
if (opt_wsDragSurface) {
this.workspaceDragSurface_ = opt_wsDragSurface;
}
this.useWorkspaceDragSurface_ =
this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
if (opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
}
if (opt_wsDragSurface) {
this.workspaceDragSurface_ = opt_wsDragSurface;
}
this.useWorkspaceDragSurface_ =
this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
if (opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
@ -151,15 +131,6 @@ Blockly.WorkspaceSvg.prototype.isFlyout = false;
*/
Blockly.WorkspaceSvg.prototype.isMutator = false;
/**
* Is this workspace currently being dragged around?
* DRAG_NONE - No drag operation.
* DRAG_BEGIN - Still inside the initial DRAG_RADIUS.
* DRAG_FREE - Workspace has been dragged further than DRAG_RADIUS.
* @private
*/
Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
/**
* Whether this workspace has resizes enabled.
* Disable during batch operations for a performance improvement.
@ -169,25 +140,25 @@ Blockly.WorkspaceSvg.prototype.dragMode_ = Blockly.DRAG_NONE;
Blockly.WorkspaceSvg.prototype.resizesEnabled_ = true;
/**
* Current horizontal scrolling offset.
* Current horizontal scrolling offset in pixel units.
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.scrollX = 0;
/**
* Current vertical scrolling offset.
* Current vertical scrolling offset in pixel units.
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.scrollY = 0;
/**
* Horizontal scroll value when scrolling started.
* Horizontal scroll value when scrolling started in pixel units.
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.startScrollX = 0;
/**
* Vertical scroll value when scrolling started.
* Vertical scroll value when scrolling started in pixel units.
* @type {number}
*/
Blockly.WorkspaceSvg.prototype.startScrollY = 0;
@ -217,6 +188,13 @@ Blockly.WorkspaceSvg.prototype.trashcan = null;
*/
Blockly.WorkspaceSvg.prototype.scrollbar = null;
/**
* The current gesture in progress on this workspace, if any.
* @type {Blockly.Gesture}
* @private
*/
Blockly.WorkspaceSvg.prototype.currentGesture_ = null;
/**
* This workspace's surface for dragging blocks, if it exists.
* @type {Blockly.BlockDragSurfaceSvg}
@ -387,11 +365,16 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
*/
this.svgGroup_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyWorkspace'}, null);
// Note that a <g> alone does not receive mouse events--it must have a
// valid target inside it. If no background class is specified, as in the
// flyout, the workspace will not receive mouse events.
if (opt_backgroundClass) {
/** @type {SVGElement} */
this.svgBackground_ = Blockly.utils.createSvgElement('rect',
{'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
this.svgGroup_);
if (opt_backgroundClass == 'blocklyMainBackground') {
this.svgBackground_.style.fill =
'url(#' + this.options.gridPattern.id + ')';
@ -414,9 +397,6 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
if (!this.isFlyout) {
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
this.onMouseDown_);
var thisWorkspace = this;
Blockly.bindEvent_(this.svgGroup_, 'touchstart', null,
function(e) {Blockly.longStart_(e, thisWorkspace);});
if (this.options.zoomOptions && this.options.zoomOptions.wheel) {
// Mouse-wheel.
Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this,
@ -446,6 +426,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
Blockly.WorkspaceSvg.prototype.dispose = function() {
// Stop rerendering.
this.rendered = false;
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
Blockly.WorkspaceSvg.superClass_.dispose.call(this);
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
@ -921,10 +904,20 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
if (!this.rendered) {
return;
}
Blockly.terminateDrag_(); // Dragging while pasting? No.
if (this.currentGesture_) {
this.currentGesture_.cancel(); // Dragging while pasting? No.
}
Blockly.Events.disable();
try {
var block = Blockly.Xml.domToBlock(xmlBlock, this);
// Rerender to get around problem with IE and Edge not measuring text
// correctly when it is hidden.
if (goog.userAgent.IE || goog.userAgent.EDGE) {
var blocks = block.getDescendants();
for (var i = blocks.length - 1; i >= 0; i--) {
blocks[i].render(false);
}
}
// Move the duplicate to original position.
var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
@ -1000,7 +993,7 @@ Blockly.WorkspaceSvg.prototype.renameVariable = function(oldName, newName) {
Blockly.WorkspaceSvg.prototype.createVariable = function(name) {
Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
// Don't refresh the toolbox if there's a drag in progress.
if (this.toolbox_ && this.toolbox_.flyout_ && !Blockly.Flyout.startFlyout_) {
if (this.toolbox_ && this.toolbox_.flyout_ && !this.currentGesture_) {
this.toolbox_.refreshSelection();
}
};
@ -1025,7 +1018,6 @@ Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
/**
* Is the mouse event over a delete area (toolbox or non-closing flyout)?
* Opens or closes the trashcan and sets the cursor as a side effect.
* @param {!Event} e Mouse move event.
* @return {?number} Null if not over a delete area, or an enum representing
* which delete area the event is over.
@ -1038,7 +1030,7 @@ Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
if (this.deleteAreaToolbox_ && this.deleteAreaToolbox_.contains(xy)) {
return Blockly.DELETE_AREA_TOOLBOX;
}
return null;
return Blockly.DELETE_AREA_NONE;
};
/**
@ -1047,61 +1039,13 @@ Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
* @private
*/
Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
this.markFocused();
if (Blockly.utils.isTargetInput(e)) {
Blockly.Touch.clearTouchIdentifier();
return;
}
Blockly.terminateDrag_(); // In case mouse-up event was lost.
Blockly.hideChaff();
// TODO (fenichel): Move this to gesture.
Blockly.DropDownDiv.hide();
var isTargetWorkspace = e.target && e.target.nodeName &&
(e.target.nodeName.toLowerCase() == 'svg' ||
e.target == this.svgBackground_);
if (isTargetWorkspace && Blockly.selected && !this.options.readOnly) {
// Clicking on the document clears the selection.
Blockly.selected.unselect();
}
if (Blockly.utils.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
// This is to handle the case where the event is pretending to be a right
// click event but it was really a long press. In that case, we want to make
// sure any in progress drags are stopped.
Blockly.onMouseUp_(e);
// Since this was a click, not a drag, end the gesture immediately.
Blockly.Touch.clearTouchIdentifier();
} else if (this.scrollbar) {
this.dragMode_ = Blockly.DRAG_BEGIN;
// Record the current mouse position.
this.startDragMouseX = e.clientX;
this.startDragMouseY = e.clientY;
this.startDragMetrics = this.getMetrics();
this.startScrollX = this.scrollX;
this.startScrollY = this.scrollY;
this.setupDragSurface();
// If this is a touch event then bind to the mouseup so workspace drag mode
// is turned off and double move events are not performed on a block.
// See comment in inject.js Blockly.init_ as to why mouseup events are
// bound to the document instead of the SVG's surface.
if ('mouseup' in Blockly.Touch.TOUCH_MAP) {
Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_ || [];
Blockly.Touch.onTouchUpWrapper_ = Blockly.Touch.onTouchUpWrapper_.concat(
Blockly.bindEventWithChecks_(document, 'mouseup', null,
Blockly.onMouseUp_));
}
Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
Blockly.bindEventWithChecks_(document, 'mousemove', null,
Blockly.onMouseMove_));
} else {
// It was a click, but the workspace isn't draggable.
Blockly.Touch.clearTouchIdentifier();
var gesture = this.getGesture(e);
if (gesture) {
gesture.handleWsStart(e, this);
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
e.preventDefault();
};
/**
@ -1138,10 +1082,7 @@ Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
* @return {boolean} True if currently dragging or scrolling.
*/
Blockly.WorkspaceSvg.prototype.isDragging = function() {
return Blockly.dragMode_ == Blockly.DRAG_FREE ||
(Blockly.Flyout.startFlyout_ &&
Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) ||
this.dragMode_ == Blockly.DRAG_FREE;
return this.currentGesture_ && this.currentGesture_.isDragging();
};
/**
@ -1158,9 +1099,12 @@ Blockly.WorkspaceSvg.prototype.isDraggable = function() {
* @private
*/
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
// TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
// TODO: Remove gesture cancellation and compensate for coordinate skew during
// zoom.
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
if (e.ctrlKey) {
Blockly.terminateDrag_();
// The vertical scroll distance that corresponds to a click of a zoom button.
var PIXELS_PER_ZOOM_STEP = 50;
var delta = -e.deltaY / PIXELS_PER_ZOOM_STEP;
@ -1183,6 +1127,7 @@ Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
/**
* Calculate the bounding box for the blocks on the workspace.
* Coordinate system: workspace coordinates.
*
* @return {Object} Contains the position and size of the bounding box
* containing the blocks on the workspace.
@ -1252,6 +1197,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
var menuOptions = [];
var topBlocks = this.getTopBlocks(true);
var eventGroup = Blockly.utils.genUid();
var ws = this;
// Options to undo/redo previous action.
var undoOption = {};
@ -1367,6 +1313,9 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteCount)),
enabled: deleteCount > 0,
callback: function() {
if (ws.currentGesture_) {
ws.currentGesture_.cancel();
}
if (deleteList.length < 2 ) {
deleteNext();
} else {
@ -1757,6 +1706,7 @@ Blockly.WorkspaceSvg.prototype.updateStackGlowScale_ = function() {
/**
* Return an object with all the metrics required to size scrollbars for a
* top level workspace. The following properties are computed:
* Coordinate system: pixel coordinates.
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
@ -1793,6 +1743,7 @@ Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() {
var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1;
var viewWidth = svgSize.width - MARGIN;
var viewHeight = svgSize.height - MARGIN;
var blockBox = this.getBlocksBoundingBox();
// Fix scale.
@ -1976,6 +1927,57 @@ Blockly.WorkspaceSvg.prototype.removeToolboxCategoryCallback = function(key) {
this.toolboxCategoryCallbacks_[key] = null;
};
/**
* Look up the gesture that is tracking this touch stream on this workspace.
* May create a new gesture.
* @param {!Event} e Mouse event or touch event
* @return {Blockly.Gesture} The gesture that is tracking this touch stream,
* or null if no valid gesture exists.
* @package
*/
Blockly.WorkspaceSvg.prototype.getGesture = function(e) {
var isStart = (e.type == 'mousedown' || e.type == 'touchstart');
var gesture = this.currentGesture_;
if (gesture) {
if (isStart && gesture.hasStarted()) {
console.warn('tried to start the same gesture twice');
// That's funny. We must have missed a mouse up.
// Cancel it, rather than try to retrieve all of the state we need.
gesture.cancel();
return null;
}
return gesture;
}
// No gesture existed on this workspace, but this looks like the start of a
// new gesture.
if (isStart) {
this.currentGesture_ = new Blockly.Gesture(e, this);
return this.currentGesture_;
}
// No gesture existed and this event couldn't be the start of a new gesture.
return null;
};
/**
* Clear the reference to the current gesture.
* @package
*/
Blockly.WorkspaceSvg.prototype.clearGesture = function() {
this.currentGesture_ = null;
};
/**
* Cancel the current gesture, if one exists.
* @package
*/
Blockly.WorkspaceSvg.prototype.cancelCurrentGesture = function() {
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
};
// Export symbols that would otherwise be renamed by Closure compiler.
Blockly.WorkspaceSvg.prototype['setVisible'] =
Blockly.WorkspaceSvg.prototype.setVisible;

View file

@ -332,7 +332,7 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) {
}
Blockly.Field.stopCache();
workspace.updateVariableList(false);
workspace.updateVariableStore(false);
// Re-enable workspace resizing.
if (workspace.setResizesEnabled) {
workspace.setResizesEnabled(true);
@ -629,6 +629,9 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
goog.asserts.assert(child.isShadow(),
'Shadow block not allowed non-shadow child.');
}
// Ensure this block doesn't have any variable inputs.
goog.asserts.assert(block.getVars().length == 0,
'Shadow blocks cannot have variable fields.');
block.setShadow(true);
}
return block;

View file

@ -196,6 +196,14 @@ Blockly.ZoomControls.prototype.createDom = function() {
);
// Attach event listeners.
Blockly.bindEventWithChecks_(zoomresetSvg, 'mousedown', null, function(e) {
workspace.markFocused();
workspace.setScale(workspace.options.zoomOptions.startScale);
workspace.scrollCenter();
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
Blockly.bindEventWithChecks_(zoominSvg, 'mousedown', null, function(e) {
workspace.markFocused();
workspace.zoomCenter(1);
@ -210,14 +218,6 @@ Blockly.ZoomControls.prototype.createDom = function() {
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
Blockly.bindEventWithChecks_(zoomresetSvg, 'mousedown', null, function(e) {
workspace.markFocused();
workspace.setScale(workspace.options.zoomOptions.startScale);
workspace.scrollCenter();
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
e.stopPropagation(); // Don't start a workspace scroll.
e.preventDefault(); // Stop double-clicking from selecting text.
});
return this.svgGroup_;
};

View file

@ -103,7 +103,7 @@ Blockly.Dart.init = function(workspace) {
}
var defvars = [];
var variables = workspace.variableList;
var variables = workspace.getAllVariables();
if (variables.length) {
for (var i = 0; i < variables.length; i++) {
defvars[i] = Blockly.Dart.variableDB_.getName(variables[i],

View file

@ -153,7 +153,7 @@ Blockly.JavaScript.init = function(workspace) {
}
var defvars = [];
var variables = workspace.variableList;
var variables = workspace.getAllVariables();
if (variables.length) {
for (var i = 0; i < variables.length; i++) {
defvars[i] = Blockly.JavaScript.variableDB_.getName(variables[i],

View file

@ -161,7 +161,7 @@ Blockly.Python.init = function(workspace) {
}
var defvars = [];
var variables = workspace.variableList;
var variables = workspace.getAllVariables();
for (var i = 0; i < variables.length; i++) {
defvars[i] = Blockly.Python.variableDB_.getName(variables[i],
Blockly.Variables.NAME_TYPE) + ' = None';

View file

@ -1 +0,0 @@
{"MATH_HUE": "230", "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", "VARIABLES_HUE": "330", "TEXTS_HUE": "160", "PROCEDURES_HUE": "290", "COLOUR_HUE": "20"}

View file

@ -125,6 +125,8 @@ Blockly.Msg.NEW_VARIABLE = 'Create variable...';
Blockly.Msg.NEW_VARIABLE_TITLE = 'New variable name:';
/// alert - Tells the user that the name they entered is already in use.
Blockly.Msg.VARIABLE_ALREADY_EXISTS = 'A variable named "%1" already exists.'
/// alert - Tells the user that the name they entered is already in use for a procedure.
Blockly.Msg.PROCEDURE_ALREADY_EXISTS = 'A procedure named "%1" already exists.'
// Variable deletion.
/// confirm - Ask the user to confirm their deletion of multiple uses of a variable.

View file

@ -5,12 +5,11 @@ if [ ! -d $chromedriver_dir ]; then
mkdir $chromedriver_dir
fi
if [[ $os_name == 'Linux' ]]; then
if [[ $os_name == 'Linux' && ! -f $chromedriver_dir/chromedriver ]]; then
cd chromedriver && curl -L https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip > tmp.zip && unzip -o tmp.zip && rm tmp.zip
# wait until download finish
sleep 5
elif [[ $os_name == 'Darwin' ]]; then
elif [[ $os_name == 'Darwin' && ! -f $chromedriver_dir/chromedriver ]]; then
cd chromedriver && curl -L https://chromedriver.storage.googleapis.com/2.29/chromedriver_mac64.zip | tar xz
# wait until download finish
sleep 5
fi

View file

@ -1,5 +1,11 @@
#!/bin/bash
os_name=`uname`
if [ -f geckodriver ]; then
exit 0
fi
echo "downloading gechdriver"
if [[ $os_name == 'Linux' ]]; then
cd ../ && curl -L https://github.com/mozilla/geckodriver/releases/download/v0.11.1/geckodriver-v0.11.1-linux64.tar.gz | tar xz
sleep 5

View file

@ -6,10 +6,9 @@ if [ ! -d $DIR ]; then
mkdir $DIR
fi
echo "downloading selenium jar"
if [ ! -f $DIR/$FILE ]; then
cd $DIR && curl -O http://selenium-release.storage.googleapis.com/3.0/selenium-server-standalone-3.0.1.jar
sleep 5
fi

8
scripts/setup_linux_env.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
if [ "${TRAVIS_OS_NAME}" == "linux" ]
then
export CHROME_BIN="/usr/bin/google-chrome"
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start &
fi

8
scripts/setup_osx_env.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
if [ "${TRAVIS_OS_NAME}" == "osx" ]
then
brew cask install google-chrome
sudo Xvfb :99 -ac -screen 0 1024x768x8 &
export CHROME_BIN="/Applications/Google Chrome.app"
fi

View file

@ -13,10 +13,11 @@ function check_command {
check_command scripts/get_geckdriver.sh
sleep 5
check_command scripts/get_selenium.sh
check_command scripts/get_selenium.sh
sleep 5
check_command scripts/get_chromedriver.sh
sleep 5
check_command scripts/selenium_connect.sh
sleep 3
check_command scripts/get_chromedriver.sh
sleep 10
check_command scripts/selenium_connect.sh
sleep 10
exit $EXIT_STATUS

View file

@ -18,5 +18,6 @@
<script src="xml_test.js"></script>
<script src="json_test.js"></script>
<script src="variable_model_test.js"></script>
<script src="variable_map_test.js"></script>
</body>
</html>

View file

@ -10,7 +10,7 @@ var path = process.cwd();
var browser = webdriverio
.remote(options)
.init()
.url("file://" + path + "/tests/jsunit/index.html").pause(3000);
.url("file://" + path + "/tests/jsunit/index.html").pause(5000);
browser

View file

@ -0,0 +1,282 @@
/**
* @license
* Blockly Tests
*
* 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.
*/
'use strict';
goog.require('goog.testing');
goog.require('goog.testing.MockControl');
var variable_map;
var mockControl_;
function variableMapTest_setUp() {
variable_map = new Blockly.VariableMap();
mockControl_ = new goog.testing.MockControl();
}
function variableMapTest_tearDown() {
mockControl_.$tearDown();
variable_map = null;
}
/**
* Check if a variable with the given values exists.
* @param {!string} name The expected name of the variable.
* @param {!string} type The expected type of the variable.
* @param {!string} id The expected id of the variable.
*/
function variableMapTest_checkVariableValues(name, type, id) {
var variable = variable_map.getVariable(name);
assertNotUndefined(variable);
assertEquals(name, variable.name);
assertEquals(type, variable.type);
assertEquals(id, variable.getId());
}
function test_getVariable_Trivial() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', 'type1', 'id1');
var var_2 = variable_map.createVariable('name2', 'type1', 'id2');
var var_3 = variable_map.createVariable('name3', 'type2', 'id3');
var result_1 = variable_map.getVariable('name1');
var result_2 = variable_map.getVariable('name2');
var result_3 = variable_map.getVariable('name3');
assertEquals(var_1, result_1);
assertEquals(var_2, result_2);
assertEquals(var_3, result_3);
variableMapTest_tearDown();
}
function test_getVariable_NotFound() {
variableMapTest_setUp();
var result = variable_map.getVariable('name1');
assertNull(result);
variableMapTest_tearDown();
}
function test_getVariableById_Trivial() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', 'type1', 'id1');
var var_2 = variable_map.createVariable('name2', 'type1', 'id2');
var var_3 = variable_map.createVariable('name3', 'type2', 'id3');
var result_1 = variable_map.getVariableById('id1');
var result_2 = variable_map.getVariableById('id2');
var result_3 = variable_map.getVariableById('id3');
assertEquals(var_1, result_1);
assertEquals(var_2, result_2);
assertEquals(var_3, result_3);
variableMapTest_tearDown();
}
function test_getVariableById_NotFound() {
variableMapTest_setUp();
var result = variable_map.getVariableById('id1');
assertNull(result);
variableMapTest_tearDown();
}
function test_createVariableTrivial() {
variableMapTest_setUp();
variable_map.createVariable('name1', 'type1', 'id1');
variableMapTest_checkVariableValues('name1', 'type1', 'id1')
variableMapTest_tearDown();
}
function test_createVariableAlreadyExists() {
// Expect that when the variable already exists, the variableMap_ is unchanged.
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', 'type1', 'id1');
// Assert there is only one variable in the variable_map.
var keys = Object.keys(variable_map.variableMap_);
assertEquals(1, keys.length);
var varMapLength = variable_map.variableMap_[keys[0]].length;
assertEquals(1, varMapLength);
variable_map.createVariable('name1');
variableMapTest_checkVariableValues('name1', 'type1', 'id1');
// Check that the size of the variableMap_ did not change.
keys = Object.keys(variable_map.variableMap_);
assertEquals(1, keys.length);
varMapLength = variable_map.variableMap_[keys[0]].length;
assertEquals(1, varMapLength);
variableMapTest_tearDown();
}
function test_createVariableNullAndUndefinedType() {
variableMapTest_setUp();
variable_map.createVariable('name1', null, 'id1');
variable_map.createVariable('name2', undefined, 'id2');
variableMapTest_checkVariableValues('name1', '', 'id1');
variableMapTest_checkVariableValues('name2', '', 'id2');
variableMapTest_tearDown();
}
function test_createVariableNullId() {
variableMapTest_setUp();
var mockGenUid = setUpMockMethod(Blockly.utils, 'genUid', null, '1');
try {
variable_map.createVariable('name1', 'type1', null);
mockGenUid.$verify();
variableMapTest_checkVariableValues('name1', 'type1', '1');
}
finally {
variableMapTest_tearDown();
}
}
function test_createVariableUndefinedId() {
variableMapTest_setUp();
var mockGenUid = setUpMockMethod(Blockly.utils, 'genUid', null, '1');
try {
variable_map.createVariable('name1', 'type1', undefined);
mockGenUid.$verify();
variableMapTest_checkVariableValues('name1', 'type1', '1');
}
finally {
variableMapTest_tearDown();
}
}
function test_createVariableIdAlreadyExists() {
variableMapTest_setUp();
variable_map.createVariable('name1', 'type1', 'id1');
try {
variable_map.createVariable('name2', 'type2', 'id1');
fail();
} catch (e) {
// expected
}
variableMapTest_tearDown();
}
function test_createVariableMismatchedIdAndType() {
variableMapTest_setUp();
variable_map.createVariable('name1', 'type1', 'id1');
try {
variable_map.createVariable('name1', 'type2', 'id1');
fail();
} catch (e) {
// expected
}
try {
variable_map.createVariable('name1', 'type1', 'id2');
fail();
} catch (e) {
// expected
}
variableMapTest_tearDown();
}
function test_createVariableTwoSameTypes() {
variableMapTest_setUp();
variable_map.createVariable('name1', 'type1', 'id1');
variable_map.createVariable('name2', 'type1', 'id2');
variableMapTest_checkVariableValues('name1', 'type1', 'id1');
variableMapTest_checkVariableValues('name2', 'type1', 'id2');
variableMapTest_tearDown();
}
function test_getVariablesOfType_Trivial() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', 'type1', 'id1');
var var_2 = variable_map.createVariable('name2', 'type1', 'id2');
variable_map.createVariable('name3', 'type2', 'id3');
variable_map.createVariable('name4', 'type3', 'id4');
var result_array_1 = variable_map.getVariablesOfType('type1');
var result_array_2 = variable_map.getVariablesOfType('type5');
this.isEqualArrays([var_1, var_2], result_array_1);
this.isEqualArrays([], result_array_2);
variableMapTest_tearDown();
}
function test_getVariablesOfType_Null() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', '', 'id1');
var var_2 = variable_map.createVariable('name2', '', 'id2');
var var_3 = variable_map.createVariable('name3', '', 'id3');
variable_map.createVariable('name4', 'type1', 'id4');
var result_array = variable_map.getVariablesOfType(null);
this.isEqualArrays([var_1, var_2, var_3], result_array);
variableMapTest_tearDown();
}
function test_getVariablesOfType_EmptyString() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', null, 'id1');
var var_2 = variable_map.createVariable('name2', null, 'id2');
var result_array = variable_map.getVariablesOfType('');
this.isEqualArrays([var_1, var_2], result_array);
variableMapTest_tearDown();
}
function test_getVariablesOfType_Deleted() {
variableMapTest_setUp();
var variable = variable_map.createVariable('name1', null, 'id1');
variable_map.deleteVariable(variable);
var result_array = variable_map.getVariablesOfType('');
this.isEqualArrays([], result_array);
variableMapTest_tearDown();
}
function test_getVariablesOfType_DoesNotExist() {
variableMapTest_setUp();
var result_array = variable_map.getVariablesOfType('type1');
this.isEqualArrays([], result_array);
variableMapTest_tearDown();
}
function test_getVariableTypes_Trivial() {
variableMapTest_setUp();
variable_map.createVariable('name1', 'type1', 'id1');
variable_map.createVariable('name2', 'type1', 'id2');
variable_map.createVariable('name3', 'type2', 'id3');
variable_map.createVariable('name4', 'type3', 'id4');
var result_array = variable_map.getVariableTypes();
this.isEqualArrays(['type1', 'type2', 'type3'], result_array);
variableMapTest_tearDown();
}
function test_getVariableTypes_None() {
variableMapTest_setUp();
var result_array = variable_map.getVariableTypes();
this.isEqualArrays([], result_array);
variableMapTest_tearDown();
}
function test_getAllVariables_Trivial() {
variableMapTest_setUp();
var var_1 = variable_map.createVariable('name1', 'type1', 'id1');
var var_2 = variable_map.createVariable('name2', 'type1', 'id2');
var var_3 = variable_map.createVariable('name3', 'type2', 'id3');
var result_array = variable_map.getAllVariables();
this.isEqualArrays([var_1, var_2, var_3], result_array);
variableMapTest_tearDown();
}
function test_getAllVariables_None() {
variableMapTest_setUp();
var result_array = variable_map.getAllVariables();
this.isEqualArrays([], result_array);
variableMapTest_tearDown();
}

View file

@ -19,8 +19,107 @@
*/
'use strict';
goog.require('goog.testing');
goog.require('goog.testing.MockControl');
var workspace;
var mockControl_;
var saved_msg = Blockly.Msg.DELETE_VARIABLE;
Blockly.defineBlocksWithJsonArray([{
"type": "get_var_block",
"message0": "%1",
"args0": [
{
"type": "field_variable",
"name": "VAR",
}
]
}]);
function workspaceTest_setUp() {
workspace = new Blockly.Workspace();
mockControl_ = new goog.testing.MockControl();
}
function workspaceTest_setUpWithMockBlocks() {
workspaceTest_setUp();
// Need to define this because field_variable's dropdownCreate() calls replace
// on undefined value, Blockly.Msg.DELETE_VARIABLE. To fix this, define
// Blockly.Msg.DELETE_VARIABLE as %1 so the replace function finds the %1 it
// expects.
Blockly.Msg.DELETE_VARIABLE = '%1';
}
function workspaceTest_tearDown() {
mockControl_.$tearDown();
workspace.dispose();
}
function workspaceTest_tearDownWithMockBlocks() {
workspaceTest_tearDown();
Blockly.Msg.DELETE_VARIABLE = saved_msg;
}
/**
* Create a test get_var_block.
* @param {?string} variable The string to put into the variable field.
* @return {!Blockly.Block} The created block.
*/
function createMockBlock(variable) {
var block = new Blockly.Block(workspace, 'get_var_block');
block.inputList[0].fieldRow[0].setValue(variable);
return block;
}
/**
* Check that two arrays have the same content.
* @param {!Array.<string>} array1 The first array.
* @param {!Array.<string>} array2 The second array.
*/
function isEqualArrays(array1, array2) {
assertEquals(array1.length, array2.length);
for (var i = 0; i < array1.length; i++) {
assertEquals(array1[i], array2[i]);
}
}
/**
* Check if a variable with the given values exists.
* @param {!string} name The expected name of the variable.
* @param {!string} type The expected type of the variable.
* @param {!string} id The expected id of the variable.
*/
function workspaceTest_checkVariableValues(name, type, id) {
var variable = workspace.getVariable(name);
assertNotUndefined(variable);
assertEquals(name, variable.name);
assertEquals(type, variable.type);
assertEquals(id, variable.getId());
}
/**
* Creates a controlled MethodMock. Set the expected return values. Set the
* method to replay.
* @param {!Object} scope The scope of the method to be mocked out.
* @param {!string} funcName The name of the function we're going to mock.
* @param {Object} parameters The parameters to call the mock with.
* @param {!Object} return_value The value to return when called.
* @return {!goog.testing.MockInterface} The mocked method.
*/
function setUpMockMethod(scope, funcName, parameters, return_value) {
var mockMethod = mockControl_.createMethodMock(scope, funcName);
if (parameters) {
mockMethod(parameters).$returns(return_value);
}
else {
mockMethod().$returns(return_value);
}
mockMethod.$replay();
return mockMethod;
}
function test_emptyWorkspace() {
var workspace = new Blockly.Workspace();
workspaceTest_setUp();
try {
assertEquals('Empty workspace (1).', 0, workspace.getTopBlocks(true).length);
assertEquals('Empty workspace (2).', 0, workspace.getTopBlocks(false).length);
@ -29,20 +128,20 @@ function test_emptyWorkspace() {
assertEquals('Empty workspace (4).', 0, workspace.getTopBlocks(true).length);
assertEquals('Empty workspace (5).', 0, workspace.getTopBlocks(false).length);
assertEquals('Empty workspace (6).', 0, workspace.getAllBlocks().length);
} finally {
workspace.dispose();
}
finally {
workspaceTest_tearDown();
}
}
function test_flatWorkspace() {
var workspace = new Blockly.Workspace();
var blockA, blockB;
workspaceTest_setUp();
try {
blockA = workspace.newBlock('');
var blockA = workspace.newBlock('');
assertEquals('One block workspace (1).', 1, workspace.getTopBlocks(true).length);
assertEquals('One block workspace (2).', 1, workspace.getTopBlocks(false).length);
assertEquals('One block workspace (3).', 1, workspace.getAllBlocks().length);
blockB = workspace.newBlock('');
var blockB = workspace.newBlock('');
assertEquals('Two block workspace (1).', 2, workspace.getTopBlocks(true).length);
assertEquals('Two block workspace (2).', 2, workspace.getTopBlocks(false).length);
assertEquals('Two block workspace (3).', 2, workspace.getAllBlocks().length);
@ -55,17 +154,15 @@ function test_flatWorkspace() {
assertEquals('Cleared workspace (2).', 0, workspace.getTopBlocks(false).length);
assertEquals('Cleared workspace (3).', 0, workspace.getAllBlocks().length);
} finally {
blockB && blockB.dispose();
blockA && blockA.dispose();
workspace.dispose();
workspaceTest_tearDown();
}
}
function test_maxBlocksWorkspace() {
var workspace = new Blockly.Workspace();
var blockA = workspace.newBlock('');
var blockB = workspace.newBlock('');
workspaceTest_setUp();
try {
var blockA = workspace.newBlock('');
var blockB = workspace.newBlock('');
assertEquals('Infinite capacity.', Infinity, workspace.remainingCapacity());
workspace.options.maxBlocks = 3;
assertEquals('Three capacity.', 1, workspace.remainingCapacity());
@ -78,9 +175,7 @@ function test_maxBlocksWorkspace() {
workspace.clear();
assertEquals('Cleared capacity.', 0, workspace.remainingCapacity());
} finally {
blockB.dispose();
blockA.dispose();
workspace.dispose();
workspaceTest_tearDown();
}
}
@ -106,10 +201,10 @@ function test_getWorkspaceById() {
}
function test_getBlockById() {
var workspace = new Blockly.Workspace();
var blockA = workspace.newBlock('');
var blockB = workspace.newBlock('');
workspaceTest_setUp();
try {
var blockA = workspace.newBlock('');
var blockB = workspace.newBlock('');
assertEquals('Find blockA.', blockA, workspace.getBlockById(blockA.id));
assertEquals('Find blockB.', blockB, workspace.getBlockById(blockB.id));
assertEquals('No block found.', null,
@ -120,8 +215,351 @@ function test_getBlockById() {
workspace.clear();
assertEquals('Can\'t find blockB.', null, workspace.getBlockById(blockB.id));
} finally {
blockB.dispose();
blockA.dispose();
workspace.dispose();
workspaceTest_tearDown();
}
}
function test_deleteVariable_InternalTrivial() {
workspaceTest_setUpWithMockBlocks()
var var_1 = workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
createMockBlock('name1');
createMockBlock('name1');
createMockBlock('name2');
workspace.deleteVariableInternal_(var_1);
var variable = workspace.getVariable('name1');
var block_var_name = workspace.topBlocks_[0].getVars()[0];
assertNull(variable);
workspaceTest_checkVariableValues('name2', 'type2', 'id2');
assertEquals('name2', block_var_name);
workspaceTest_tearDownWithMockBlocks();
}
// TODO(marisaleung): Test the alert for deleting a variable that is a procedure.
function test_updateVariableStore_TrivialNoClear() {
workspaceTest_setUp();
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
var mockAllUsedVariables = setUpMockMethod(Blockly.Variables,
'allUsedVariables', workspace, ['name1', 'name2']);
try {
workspace.updateVariableStore();
mockAllUsedVariables.$verify();
workspaceTest_checkVariableValues('name1', 'type1', 'id1');
workspaceTest_checkVariableValues('name2', 'type2', 'id2');
}
finally {
workspaceTest_tearDown();
}
}
function test_updateVariableStore_NameNotInvariableMap_NoClear() {
workspaceTest_setUp();
setUpMockMethod(Blockly.Variables, 'allUsedVariables', workspace, ['name1']);
setUpMockMethod(Blockly.utils, 'genUid', null, '1');
try {
workspace.updateVariableStore();
mockControl_.$verifyAll();
workspaceTest_checkVariableValues('name1', '', '1');
}
finally {
workspaceTest_tearDown();
}
}
function test_updateVariableStore_ClearAndAllInUse() {
workspaceTest_setUp();
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
var mockAllUsedVariables = setUpMockMethod(Blockly.Variables,
'allUsedVariables', workspace, ['name1', 'name2']);
try {
workspace.updateVariableStore(true);
mockAllUsedVariables.$verify();
workspaceTest_checkVariableValues('name1', 'type1', 'id1');
workspaceTest_checkVariableValues('name2', 'type2', 'id2');
}
finally {
workspaceTest_tearDown();
}
}
function test_updateVariableStore_ClearAndOneInUse() {
workspaceTest_setUp();
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
var mockAllUsedVariables = setUpMockMethod(Blockly.Variables,
'allUsedVariables', workspace, ['name1']);
try {
workspace.updateVariableStore(true);
mockAllUsedVariables.$verify();
workspaceTest_checkVariableValues('name1', 'type1', 'id1');
var variabe = workspace.getVariable('name2');
assertNull(variable);
}
finally {
workspaceTest_tearDown();
}
}
function test_addTopBlock_TrivialFlyoutIsTrue() {
workspaceTest_setUpWithMockBlocks()
workspace.isFlyout = true;
var block = createMockBlock();
workspace.removeTopBlock(block);
setUpMockMethod(Blockly.Variables, 'allUsedVariables', block, ['name1']);
setUpMockMethod(Blockly.utils, 'genUid', null, '1');
try {
workspace.addTopBlock(block);
mockControl_.$verifyAll();
workspaceTest_checkVariableValues('name1', '', '1');
}
finally {
workspaceTest_tearDownWithMockBlocks();
}
}
function test_clear_Trivial() {
workspaceTest_setUp();
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type2', 'id2');
var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup');
mockSetGroup(true);
mockSetGroup(false);
mockSetGroup.$replay();
try {
workspace.clear();
mockControl_.$verifyAll();
var topBlocks_length = workspace.topBlocks_.length;
var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length;
assertEquals(0, topBlocks_length);
assertEquals(0, varMapLength);
}
finally {
workspaceTest_tearDown();
}
}
function test_clear_NoVariables() {
workspaceTest_setUp();
var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup');
mockSetGroup(true);
mockSetGroup(false);
mockSetGroup.$replay();
try {
workspace.clear();
mockSetGroup.$verify();
var topBlocks_length = workspace.topBlocks_.length;
var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length;
assertEquals(0, topBlocks_length);
assertEquals(0, varMapLength);
}
finally {
workspaceTest_tearDown();
}
}
function test_renameVariable_NoBlocks() {
// Expect 'renameVariable' to create new variable with newName.
workspaceTest_setUp();
var oldName = 'name1';
var newName = 'name2';
var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup');
var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid');
// Mocked setGroup to ensure only one call to the mocked genUid.
mockSetGroup(true);
mockSetGroup(false);
mockGenUid().$returns('1');
mockControl_.$replayAll();
try {
workspace.renameVariable(oldName, newName);
mockControl_.$verifyAll();
workspaceTest_checkVariableValues('name2', '', '1');
var variable = workspace.getVariable(oldName);
assertNull(variable);
}
finally {
workspaceTest_tearDown();
}
}
function test_renameVariable_SameNameNoBlocks() {
// Expect 'renameVariable' to create new variable with newName.
workspaceTest_setUpWithMockBlocks()
var name = 'name1';
workspace.createVariable(name, 'type1', 'id1');
workspace.renameVariable(name, name);
workspaceTest_checkVariableValues(name, 'type1', 'id1');
workspaceTest_tearDownWithMockBlocks();
}
function test_renameVariable_OnlyOldNameBlockExists() {
// Expect 'renameVariable' to change oldName variable name to newName.
workspaceTest_setUpWithMockBlocks()
var oldName = 'name1';
var newName = 'name2';
workspace.createVariable(oldName, 'type1', 'id1');
createMockBlock(oldName);
workspace.renameVariable(oldName, newName);
workspaceTest_checkVariableValues(newName, 'type1', 'id1');
var variable = workspace.getVariable(oldName);
var block_var_name = workspace.topBlocks_[0].getVars()[0];
assertNull(variable);
assertEquals(newName, block_var_name);
workspaceTest_tearDownWithMockBlocks();
}
function test_renameVariable_TwoVariablesSameType() {
// Expect 'renameVariable' to change oldName variable name to newName.
// Expect oldName block name to change to newName
workspaceTest_setUpWithMockBlocks()
var oldName = 'name1';
var newName = 'name2';
workspace.createVariable(oldName, 'type1', 'id1');
workspace.createVariable(newName, 'type1', 'id2');
createMockBlock(oldName);
createMockBlock(newName);
workspace.renameVariable(oldName, newName);
workspaceTest_checkVariableValues(newName, 'type1', 'id2');
var variable = workspace.getVariable(oldName);
var block_var_name_1 = workspace.topBlocks_[0].getVars()[0];
var block_var_name_2 = workspace.topBlocks_[1].getVars()[0];
assertNull(variable);
assertEquals(newName, block_var_name_1);
assertEquals(newName, block_var_name_2);
workspaceTest_tearDownWithMockBlocks();
}
function test_renameVariable_TwoVariablesDifferentType() {
// Expect triggered error because of different types
workspaceTest_setUpWithMockBlocks()
var oldName = 'name1';
var newName = 'name2';
workspace.createVariable(oldName, 'type1', 'id1');
workspace.createVariable(newName, 'type2', 'id2');
createMockBlock(oldName);
createMockBlock(newName);
try {
workspace.renameVariable(oldName, newName);
fail();
} catch (e) {
// expected
}
workspaceTest_checkVariableValues(oldName, 'type1', 'id1');
workspaceTest_checkVariableValues(newName, 'type2', 'id2');
var block_var_name_1 = workspace.topBlocks_[0].getVars()[0];
var block_var_name_2 = workspace.topBlocks_[1].getVars()[0];
assertEquals(oldName, block_var_name_1);
assertEquals(newName, block_var_name_2);
workspaceTest_tearDownWithMockBlocks();
}
function test_renameVariable_OldCase() {
// Expect triggered error because of different types
workspaceTest_setUpWithMockBlocks();
var oldCase = 'Name1';
var newName = 'name1';
workspace.createVariable(oldCase, 'type1', 'id1');
createMockBlock(oldCase);
workspace.renameVariable(oldCase, newName);
workspaceTest_checkVariableValues(newName, 'type1', 'id1');
var result_oldCase = workspace.getVariable(oldCase).name
assertNotEquals(oldCase, result_oldCase);
workspaceTest_tearDownWithMockBlocks();
}
function test_renameVariable_TwoVariablesAndOldCase() {
// Expect triggered error because of different types
workspaceTest_setUpWithMockBlocks()
var oldName = 'name1';
var oldCase = 'Name2';
var newName = 'name2';
workspace.createVariable(oldName, 'type1', 'id1');
workspace.createVariable(oldCase, 'type1', 'id2');
createMockBlock(oldName);
createMockBlock(oldCase);
workspace.renameVariable(oldName, newName);
workspaceTest_checkVariableValues(newName, 'type1', 'id2');
var variable = workspace.getVariable(oldName);
var result_oldCase = workspace.getVariable(oldCase).name;
var block_var_name_1 = workspace.topBlocks_[0].getVars()[0];
var block_var_name_2 = workspace.topBlocks_[1].getVars()[0];
assertNull(variable);
assertNotEquals(oldCase, result_oldCase);
assertEquals(newName, block_var_name_1);
assertEquals(newName, block_var_name_2);
workspaceTest_tearDownWithMockBlocks();
}
// Extra testing not required for renameVariableById. It calls renameVariable
// and that has extensive testing.
function test_renameVariableById_TwoVariablesSameType() {
// Expect 'renameVariableById' to change oldName variable name to newName.
// Expect oldName block name to change to newName
workspaceTest_setUpWithMockBlocks()
var oldName = 'name1';
var newName = 'name2';
workspace.createVariable(oldName, 'type1', 'id1');
workspace.createVariable(newName, 'type1', 'id2');
createMockBlock(oldName);
createMockBlock(newName);
workspace.renameVariableById('id1', newName);
workspaceTest_checkVariableValues(newName, 'type1', 'id2');
var variable = workspace.getVariable(oldName)
var block_var_name_1 = workspace.topBlocks_[0].getVars()[0];
var block_var_name_2 = workspace.topBlocks_[1].getVars()[0];
assertNull(variable);
assertEquals(newName, block_var_name_1);
assertEquals(newName, block_var_name_2);
workspaceTest_tearDownWithMockBlocks();
}
function test_deleteVariable_Trivial() {
workspaceTest_setUpWithMockBlocks()
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type1', 'id2');
createMockBlock('name1');
createMockBlock('name2');
workspace.deleteVariable('name1');
workspaceTest_checkVariableValues('name2', 'type1', 'id2');
var variable = workspace.getVariable('name1');
var block_var_name = workspace.topBlocks_[0].getVars()[0];
assertNull(variable);
assertEquals('name2', block_var_name);
workspaceTest_tearDownWithMockBlocks();
}
function test_deleteVariableById_Trivial() {
workspaceTest_setUpWithMockBlocks()
workspace.createVariable('name1', 'type1', 'id1');
workspace.createVariable('name2', 'type1', 'id2');
createMockBlock('name1');
createMockBlock('name2');
workspace.deleteVariableById('id1');
workspaceTest_checkVariableValues('name2', 'type1', 'id2');
var variable = workspace.getVariable('name1');
var block_var_name = workspace.topBlocks_[0].getVars()[0];
assertNull(variable);
assertEquals('name2', block_var_name);
workspaceTest_tearDownWithMockBlocks();
}