diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js
index 7c7a64f17..6c03480f3 100644
--- a/core/block_drag_surface.js
+++ b/core/block_drag_surface.js
@@ -35,233 +35,235 @@ const {Svg} = goog.require('Blockly.utils.Svg');
/**
* Class for a drag surface for the currently dragged block. This is a separate
* SVG that contains only the currently moving block, or nothing.
- * @param {!Element} container Containing element.
- * @constructor
- * @alias Blockly.BlockDragSurfaceSvg
*/
-const BlockDragSurfaceSvg = function(container) {
+const BlockDragSurfaceSvg = class {
/**
- * The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom.
- * @type {?SVGElement}
- * @private
+ * @param {!Element} container Containing element.
+ * @alias Blockly.BlockDragSurfaceSvg
*/
- this.SVG_ = null;
+ constructor(container) {
+ /**
+ * The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom.
+ * @type {?SVGElement}
+ * @private
+ */
+ this.SVG_ = null;
- /**
- * This is where blocks live while they are being dragged if the drag surface
- * is enabled.
- * @type {?SVGElement}
- * @private
- */
- this.dragGroup_ = null;
+ /**
+ * This is where blocks live while they are being dragged if the drag
+ * surface is enabled.
+ * @type {?SVGElement}
+ * @private
+ */
+ this.dragGroup_ = null;
- /**
- * Containing HTML element; parent of the workspace and the drag surface.
- * @type {!Element}
- * @private
- */
- this.container_ = container;
+ /**
+ * Containing HTML element; parent of the workspace and the drag surface.
+ * @type {!Element}
+ * @private
+ */
+ this.container_ = container;
- /**
- * Cached value for the scale of the drag surface.
- * Used to set/get the correct translation during and after a drag.
- * @type {number}
- * @private
- */
- this.scale_ = 1;
+ /**
+ * Cached value for the scale of the drag surface.
+ * Used to set/get the correct translation during and after a drag.
+ * @type {number}
+ * @private
+ */
+ this.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 {?Coordinate}
- * @private
- */
- this.surfaceXY_ = null;
+ /**
+ * 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 {?Coordinate}
+ * @private
+ */
+ this.surfaceXY_ = null;
- /**
- * Cached value for the translation of the child drag surface in pixel units.
- * Since the child drag surface tracks the translation of the workspace this
- * is ultimately the translation of the workspace.
- * @type {!Coordinate}
- * @private
- */
- this.childSurfaceXY_ = new Coordinate(0, 0);
+ /**
+ * Cached value for the translation of the child drag surface in pixel
+ * units. Since the child drag surface tracks the translation of the
+ * workspace this is ultimately the translation of the workspace.
+ * @type {!Coordinate}
+ * @private
+ */
+ this.childSurfaceXY_ = new Coordinate(0, 0);
- this.createDom();
-};
-
-
-/**
- * Create the drag surface and inject it into the container.
- */
-BlockDragSurfaceSvg.prototype.createDom = function() {
- if (this.SVG_) {
- return; // Already created.
+ this.createDom();
}
- this.SVG_ = dom.createSvgElement(
- Svg.SVG, {
- 'xmlns': dom.SVG_NS,
- 'xmlns:html': dom.HTML_NS,
- 'xmlns:xlink': dom.XLINK_NS,
- 'version': '1.1',
- 'class': 'blocklyBlockDragSurface',
- },
- this.container_);
- this.dragGroup_ = dom.createSvgElement(Svg.G, {}, this.SVG_);
-};
-/**
- * Set the SVG blocks on the drag surface's group and show the surface.
- * Only one block group should be on the drag surface at a time.
- * @param {!SVGElement} blocks Block or group of blocks to place on the drag
- * surface.
- */
-BlockDragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
- if (this.dragGroup_.childNodes.length) {
- throw Error('Already dragging a block.');
- }
- // appendChild removes the blocks from the previous parent
- this.dragGroup_.appendChild(blocks);
- this.SVG_.style.display = 'block';
- this.surfaceXY_ = new Coordinate(0, 0);
-};
-
-/**
- * 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 pixel coordinates.
- * @param {number} y Y translation in pixel coordinates.
- * @param {number} scale Scale of the group.
- */
-BlockDragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
- this.scale_ = scale;
- // This is a work-around to prevent a the blocks from rendering
- // fuzzy while they are being dragged on the drag surface.
- const fixedX = x.toFixed(0);
- const fixedY = y.toFixed(0);
-
- this.childSurfaceXY_.x = parseInt(fixedX, 10);
- this.childSurfaceXY_.y = parseInt(fixedY, 10);
-
- this.dragGroup_.setAttribute(
- 'transform',
- 'translate(' + fixedX + ',' + fixedY + ') scale(' + scale + ')');
-};
-
-/**
- * Translate the drag surface's SVG based on its internal state.
- * @private
- */
-BlockDragSurfaceSvg.prototype.translateSurfaceInternal_ = function() {
- let x = this.surfaceXY_.x;
- let 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';
-
- dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
-};
-
-/**
- * Translates the entire surface by a relative offset.
- * @param {number} deltaX Horizontal offset in pixel units.
- * @param {number} deltaY Vertical offset in pixel units.
- */
-BlockDragSurfaceSvg.prototype.translateBy = function(deltaX, deltaY) {
- const x = this.surfaceXY_.x + deltaX;
- const y = this.surfaceXY_.y + deltaY;
- this.surfaceXY_ = new Coordinate(x, y);
- this.translateSurfaceInternal_();
-};
-
-/**
- * Translate the entire drag surface during a drag.
- * We translate the drag surface instead of the blocks inside the surface
- * so that the browser avoids repainting the SVG.
- * Because of this, the drag coordinates must be adjusted by scale.
- * @param {number} x X translation for the entire surface.
- * @param {number} y Y translation for the entire surface.
- */
-BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) {
- this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
- this.translateSurfaceInternal_();
-};
-
-/**
- * Reports the surface translation in scaled workspace coordinates.
- * Use this when finishing a drag to return blocks to the correct position.
- * @return {!Coordinate} Current translation of the surface.
- */
-BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() {
- const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_));
- return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
-};
-
-/**
- * Provide a reference to the drag group (primarily for
- * BlockSvg.getRelativeToSurfaceXY).
- * @return {?SVGElement} Drag surface group element.
- */
-BlockDragSurfaceSvg.prototype.getGroup = function() {
- return this.dragGroup_;
-};
-
-/**
- * Returns the SVG drag surface.
- * @returns {?SVGElement} The SVG drag surface.
- */
-BlockDragSurfaceSvg.prototype.getSvgRoot = function() {
- return this.SVG_;
-};
-
-/**
- * Get the current blocks on the drag surface, if any (primarily
- * for BlockSvg.getRelativeToSurfaceXY).
- * @return {?Element} Drag surface block DOM element, or null if no blocks
- * exist.
- */
-BlockDragSurfaceSvg.prototype.getCurrentBlock = function() {
- return /** @type {Element} */ (this.dragGroup_.firstChild);
-};
-
-/**
- * Gets the translation of the child block surface
- * This surface is in charge of keeping track of how much the workspace has
- * moved.
- * @return {!Coordinate} The amount the workspace has been moved.
- */
-BlockDragSurfaceSvg.prototype.getWsTranslation = function() {
- // Returning a copy so the coordinate can not be changed outside this class.
- return this.childSurfaceXY_.clone();
-};
-
-/**
- * Clear the group and hide the surface; move the blocks off onto the provided
- * element.
- * 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.
- */
-BlockDragSurfaceSvg.prototype.clearAndHide = function(opt_newSurface) {
- const currentBlockElement = this.getCurrentBlock();
- if (currentBlockElement) {
- if (opt_newSurface) {
- // appendChild removes the node from this.dragGroup_
- opt_newSurface.appendChild(currentBlockElement);
- } else {
- this.dragGroup_.removeChild(currentBlockElement);
+ /**
+ * Create the drag surface and inject it into the container.
+ */
+ createDom() {
+ if (this.SVG_) {
+ return; // Already created.
}
+ this.SVG_ = dom.createSvgElement(
+ Svg.SVG, {
+ 'xmlns': dom.SVG_NS,
+ 'xmlns:html': dom.HTML_NS,
+ 'xmlns:xlink': dom.XLINK_NS,
+ 'version': '1.1',
+ 'class': 'blocklyBlockDragSurface',
+ },
+ this.container_);
+ this.dragGroup_ = dom.createSvgElement(Svg.G, {}, this.SVG_);
}
- this.SVG_.style.display = 'none';
- if (this.dragGroup_.childNodes.length) {
- throw Error('Drag group was not cleared.');
+
+ /**
+ * Set the SVG blocks on the drag surface's group and show the surface.
+ * Only one block group should be on the drag surface at a time.
+ * @param {!SVGElement} blocks Block or group of blocks to place on the drag
+ * surface.
+ */
+ setBlocksAndShow(blocks) {
+ if (this.dragGroup_.childNodes.length) {
+ throw Error('Already dragging a block.');
+ }
+ // appendChild removes the blocks from the previous parent
+ this.dragGroup_.appendChild(blocks);
+ this.SVG_.style.display = 'block';
+ this.surfaceXY_ = new Coordinate(0, 0);
+ }
+
+ /**
+ * 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 pixel coordinates.
+ * @param {number} y Y translation in pixel coordinates.
+ * @param {number} scale Scale of the group.
+ */
+ translateAndScaleGroup(x, y, scale) {
+ this.scale_ = scale;
+ // This is a work-around to prevent a the blocks from rendering
+ // fuzzy while they are being dragged on the drag surface.
+ const fixedX = x.toFixed(0);
+ const fixedY = y.toFixed(0);
+
+ this.childSurfaceXY_.x = parseInt(fixedX, 10);
+ this.childSurfaceXY_.y = parseInt(fixedY, 10);
+
+ this.dragGroup_.setAttribute(
+ 'transform',
+ 'translate(' + fixedX + ',' + fixedY + ') scale(' + scale + ')');
+ }
+
+ /**
+ * Translate the drag surface's SVG based on its internal state.
+ * @private
+ */
+ translateSurfaceInternal_() {
+ let x = this.surfaceXY_.x;
+ let 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';
+
+ dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
+ }
+
+ /**
+ * Translates the entire surface by a relative offset.
+ * @param {number} deltaX Horizontal offset in pixel units.
+ * @param {number} deltaY Vertical offset in pixel units.
+ */
+ translateBy(deltaX, deltaY) {
+ const x = this.surfaceXY_.x + deltaX;
+ const y = this.surfaceXY_.y + deltaY;
+ this.surfaceXY_ = new Coordinate(x, y);
+ this.translateSurfaceInternal_();
+ }
+
+ /**
+ * Translate the entire drag surface during a drag.
+ * We translate the drag surface instead of the blocks inside the surface
+ * so that the browser avoids repainting the SVG.
+ * Because of this, the drag coordinates must be adjusted by scale.
+ * @param {number} x X translation for the entire surface.
+ * @param {number} y Y translation for the entire surface.
+ */
+ translateSurface(x, y) {
+ this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
+ this.translateSurfaceInternal_();
+ }
+
+ /**
+ * Reports the surface translation in scaled workspace coordinates.
+ * Use this when finishing a drag to return blocks to the correct position.
+ * @return {!Coordinate} Current translation of the surface.
+ */
+ getSurfaceTranslation() {
+ const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_));
+ return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
+ }
+
+ /**
+ * Provide a reference to the drag group (primarily for
+ * BlockSvg.getRelativeToSurfaceXY).
+ * @return {?SVGElement} Drag surface group element.
+ */
+ getGroup() {
+ return this.dragGroup_;
+ }
+
+ /**
+ * Returns the SVG drag surface.
+ * @returns {?SVGElement} The SVG drag surface.
+ */
+ getSvgRoot() {
+ return this.SVG_;
+ }
+
+ /**
+ * Get the current blocks on the drag surface, if any (primarily
+ * for BlockSvg.getRelativeToSurfaceXY).
+ * @return {?Element} Drag surface block DOM element, or null if no blocks
+ * exist.
+ */
+ getCurrentBlock() {
+ return /** @type {Element} */ (this.dragGroup_.firstChild);
+ }
+
+ /**
+ * Gets the translation of the child block surface
+ * This surface is in charge of keeping track of how much the workspace has
+ * moved.
+ * @return {!Coordinate} The amount the workspace has been moved.
+ */
+ getWsTranslation() {
+ // Returning a copy so the coordinate can not be changed outside this class.
+ return this.childSurfaceXY_.clone();
+ }
+
+ /**
+ * Clear the group and hide the surface; move the blocks off onto the provided
+ * element.
+ * 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.
+ */
+ clearAndHide(opt_newSurface) {
+ const currentBlockElement = this.getCurrentBlock();
+ if (currentBlockElement) {
+ if (opt_newSurface) {
+ // appendChild removes the node from this.dragGroup_
+ opt_newSurface.appendChild(currentBlockElement);
+ } else {
+ this.dragGroup_.removeChild(currentBlockElement);
+ }
+ }
+ this.SVG_.style.display = 'none';
+ if (this.dragGroup_.childNodes.length) {
+ throw Error('Drag group was not cleared.');
+ }
+ this.surfaceXY_ = null;
}
- this.surfaceXY_ = null;
};
exports.BlockDragSurfaceSvg = BlockDragSurfaceSvg;
diff --git a/core/block_dragger.js b/core/block_dragger.js
index 817fa10fe..d0ee612d3 100644
--- a/core/block_dragger.js
+++ b/core/block_dragger.js
@@ -40,76 +40,411 @@ goog.require('Blockly.Events.BlockMove');
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
- * @param {!BlockSvg} block The block to drag.
- * @param {!WorkspaceSvg} workspace The workspace to drag on.
- * @constructor
* @implements {IBlockDragger}
- * @alias Blockly.BlockDragger
*/
-const BlockDragger = function(block, workspace) {
+const BlockDragger = class {
/**
- * The top block in the stack that is being dragged.
- * @type {!BlockSvg}
+ * @param {!BlockSvg} block The block to drag.
+ * @param {!WorkspaceSvg} workspace The workspace to drag on.
+ * @alias Blockly.BlockDragger
+ */
+ constructor(block, workspace) {
+ /**
+ * The top block in the stack that is being dragged.
+ * @type {!BlockSvg}
+ * @protected
+ */
+ this.draggingBlock_ = block;
+
+ /**
+ * The workspace on which the block is being dragged.
+ * @type {!WorkspaceSvg}
+ * @protected
+ */
+ this.workspace_ = workspace;
+
+ /**
+ * Object that keeps track of connections on dragged blocks.
+ * @type {!InsertionMarkerManager}
+ * @protected
+ */
+ this.draggedConnectionManager_ =
+ new InsertionMarkerManager(this.draggingBlock_);
+
+ /**
+ * Which drag area the mouse pointer is over, if any.
+ * @type {?IDragTarget}
+ * @private
+ */
+ this.dragTarget_ = null;
+
+ /**
+ * Whether the block would be deleted if dropped immediately.
+ * @type {boolean}
+ * @protected
+ */
+ this.wouldDeleteBlock_ = false;
+
+ /**
+ * The location of the top left corner of the dragging block at the
+ * beginning of the drag in workspace coordinates.
+ * @type {!Coordinate}
+ * @protected
+ */
+ 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}
+ * @protected
+ */
+ this.dragIconData_ = initIconData(block);
+ }
+
+ /**
+ * Sever all links from this object.
+ * @package
+ */
+ dispose() {
+ this.dragIconData_.length = 0;
+
+ if (this.draggedConnectionManager_) {
+ this.draggedConnectionManager_.dispose();
+ }
+ }
+
+ /**
+ * Start dragging a block. This includes moving it to the drag surface.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at mouse down, in pixel units.
+ * @param {boolean} healStack Whether or not to heal the stack after
+ * disconnecting.
+ * @public
+ */
+ startDrag(currentDragDeltaXY, healStack) {
+ if (!eventUtils.getGroup()) {
+ eventUtils.setGroup(true);
+ }
+ this.fireDragStartEvent_();
+
+ // Mutators don't have the same type of z-ordering as the normal workspace
+ // during a drag. They have to rely on the order of the blocks in the SVG.
+ // For performance reasons that usually happens at the end of a drag,
+ // but do it at the beginning for mutators.
+ if (this.workspace_.isMutator) {
+ this.draggingBlock_.bringToFront();
+ }
+
+ // During a drag there may be a lot of rerenders, but not field changes.
+ // Turn the cache on so we don't do spurious remeasures during the drag.
+ dom.startTextWidthCache();
+ this.workspace_.setResizesEnabled(false);
+ blockAnimation.disconnectUiStop();
+
+ if (this.shouldDisconnect_(healStack)) {
+ this.disconnectBlock_(healStack, currentDragDeltaXY);
+ }
+ 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();
+ }
+
+ /**
+ * Whether or not we should disconnect the block when a drag is started.
+ * @param {boolean} healStack Whether or not to heal the stack after
+ * disconnecting.
+ * @return {boolean} True to disconnect the block, false otherwise.
* @protected
*/
- this.draggingBlock_ = block;
+ shouldDisconnect_(healStack) {
+ return !!(
+ this.draggingBlock_.getParent() ||
+ (healStack && this.draggingBlock_.nextConnection &&
+ this.draggingBlock_.nextConnection.targetBlock()));
+ }
/**
- * The workspace on which the block is being dragged.
- * @type {!WorkspaceSvg}
+ * Disconnects the block and moves it to a new location.
+ * @param {boolean} healStack Whether or not to heal the stack after
+ * disconnecting.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at mouse down, in pixel units.
* @protected
*/
- this.workspace_ = workspace;
+ disconnectBlock_(healStack, currentDragDeltaXY) {
+ this.draggingBlock_.unplug(healStack);
+ const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
+ const newLoc = Coordinate.sum(this.startXY_, delta);
+
+ this.draggingBlock_.translate(newLoc.x, newLoc.y);
+ blockAnimation.disconnectUiEffect(this.draggingBlock_);
+ this.draggedConnectionManager_.updateAvailableConnections();
+ }
/**
- * Object that keeps track of connections on dragged blocks.
- * @type {!InsertionMarkerManager}
+ * Fire a UI event at the start of a block drag.
* @protected
*/
- this.draggedConnectionManager_ =
- new InsertionMarkerManager(this.draggingBlock_);
+ fireDragStartEvent_() {
+ const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
+ this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
+ eventUtils.fire(event);
+ }
/**
- * Which drag area the mouse pointer is over, if any.
- * @type {?IDragTarget}
- * @private
+ * Execute a step of block dragging, based on the given event. Update the
+ * display accordingly.
+ * @param {!Event} e The most recent move event.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at the start of the drag, in pixel units.
+ * @public
*/
- this.dragTarget_ = null;
+ drag(e, currentDragDeltaXY) {
+ const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
+ const newLoc = Coordinate.sum(this.startXY_, delta);
+ this.draggingBlock_.moveDuringDrag(newLoc);
+ this.dragIcons_(delta);
+
+ const oldDragTarget = this.dragTarget_;
+ this.dragTarget_ = this.workspace_.getDragTarget(e);
+
+ this.draggedConnectionManager_.update(delta, this.dragTarget_);
+ const oldWouldDeleteBlock = this.wouldDeleteBlock_;
+ this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
+ if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
+ // Prevent unnecessary add/remove class calls.
+ this.updateCursorDuringBlockDrag_();
+ }
+
+ // Call drag enter/exit/over after wouldDeleteBlock is called in
+ // InsertionMarkerManager.update.
+ if (this.dragTarget_ !== oldDragTarget) {
+ oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
+ this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
+ }
+ this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);
+ }
/**
- * Whether the block would be deleted if dropped immediately.
- * @type {boolean}
+ * Finish a block drag and put the block back on the workspace.
+ * @param {!Event} e The mouseup/touchend event.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at the start of the drag, in pixel units.
+ * @public
+ */
+ endDrag(e, currentDragDeltaXY) {
+ // Make sure internal state is fresh.
+ this.drag(e, currentDragDeltaXY);
+ this.dragIconData_ = [];
+ this.fireDragEndEvent_();
+
+ dom.stopTextWidthCache();
+
+ blockAnimation.disconnectUiStop();
+
+ const preventMove = !!this.dragTarget_ &&
+ this.dragTarget_.shouldPreventMove(this.draggingBlock_);
+ /** @type {Coordinate} */
+ let newLoc;
+ /** @type {Coordinate} */
+ let delta;
+ if (preventMove) {
+ newLoc = this.startXY_;
+ } else {
+ const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
+ delta = newValues.delta;
+ newLoc = newValues.newLocation;
+ }
+ this.draggingBlock_.moveOffDragSurface(newLoc);
+
+ if (this.dragTarget_) {
+ this.dragTarget_.onDrop(this.draggingBlock_);
+ }
+
+ const deleted = this.maybeDeleteBlock_();
+ if (!deleted) {
+ // These are expensive and don't need to be done if we're deleting.
+ this.draggingBlock_.setDragging(false);
+ if (delta) { // !preventMove
+ this.updateBlockAfterMove_(delta);
+ } else {
+ // Blocks dragged directly from a flyout may need to be bumped into
+ // bounds.
+ bumpObjects.bumpIntoBounds(
+ this.draggingBlock_.workspace,
+ this.workspace_.getMetricsManager().getScrollMetrics(true),
+ this.draggingBlock_);
+ }
+ }
+ this.workspace_.setResizesEnabled(true);
+
+ eventUtils.setGroup(false);
+ }
+
+ /**
+ * Calculates the drag delta and new location values after a block is dragged.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the start of the drag, in pixel units.
+ * @return {{delta: !Coordinate, newLocation:
+ * !Coordinate}} New location after drag. delta is in
+ * workspace units. newLocation is the new coordinate where the block
+ * should end up.
* @protected
*/
- this.wouldDeleteBlock_ = false;
+ getNewLocationAfterDrag_(currentDragDeltaXY) {
+ const newValues = {};
+ newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
+ newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta);
+ return newValues;
+ }
/**
- * The location of the top left corner of the dragging block at the beginning
- * of the drag in workspace coordinates.
- * @type {!Coordinate}
+ * May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is
+ * not true, the block will not be deleted. This should be called at the end
+ * of a block drag.
+ * @return {boolean} True if the block was deleted.
* @protected
*/
- this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
+ maybeDeleteBlock_() {
+ if (this.wouldDeleteBlock_) {
+ // Fire a move event, so we know where to go back to for an undo.
+ this.fireMoveEvent_();
+ this.draggingBlock_.dispose(false, true);
+ common.draggingConnections.length = 0;
+ return true;
+ }
+ return false;
+ }
/**
- * 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}
+ * Updates the necessary information to place a block at a certain location.
+ * @param {!Coordinate} delta The change in location from where
+ * the block started the drag to where it ended the drag.
* @protected
*/
- this.dragIconData_ = initIconData(block);
-};
+ updateBlockAfterMove_(delta) {
+ this.draggingBlock_.moveConnections(delta.x, delta.y);
+ this.fireMoveEvent_();
+ if (this.draggedConnectionManager_.wouldConnectBlock()) {
+ // Applying connections also rerenders the relevant blocks.
+ this.draggedConnectionManager_.applyConnections();
+ } else {
+ this.draggingBlock_.render();
+ }
+ this.draggingBlock_.scheduleSnapAndBump();
+ }
-/**
- * Sever all links from this object.
- * @package
- */
-BlockDragger.prototype.dispose = function() {
- this.dragIconData_.length = 0;
+ /**
+ * Fire a UI event at the end of a block drag.
+ * @protected
+ */
+ fireDragEndEvent_() {
+ const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
+ this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
+ eventUtils.fire(event);
+ }
- if (this.draggedConnectionManager_) {
- this.draggedConnectionManager_.dispose();
+ /**
+ * Adds or removes the style of the cursor for the toolbox.
+ * This is what changes the cursor to display an x when a deletable block is
+ * held over the toolbox.
+ * @param {boolean} isEnd True if we are at the end of a drag, false
+ * otherwise.
+ * @protected
+ */
+ updateToolboxStyle_(isEnd) {
+ const toolbox = this.workspace_.getToolbox();
+
+ if (toolbox) {
+ const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
+ 'blocklyToolboxGrab';
+
+ if (isEnd && typeof toolbox.removeStyle === 'function') {
+ toolbox.removeStyle(style);
+ } else if (!isEnd && typeof toolbox.addStyle === 'function') {
+ toolbox.addStyle(style);
+ }
+ }
+ }
+
+ /**
+ * Fire a move event at the end of a block drag.
+ * @protected
+ */
+ fireMoveEvent_() {
+ const event =
+ new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_);
+ event.oldCoordinate = this.startXY_;
+ event.recordNew();
+ eventUtils.fire(event);
+ }
+
+ /**
+ * Update the cursor (and possibly the trash can lid) to reflect whether the
+ * dragging block would be deleted if released immediately.
+ * @protected
+ */
+ updateCursorDuringBlockDrag_() {
+ this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
+ }
+
+ /**
+ * 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 {!Coordinate} pixelCoord A coordinate with x and y
+ * values in CSS pixel units.
+ * @return {!Coordinate} The input coordinate divided by the
+ * workspace scale.
+ * @protected
+ */
+ pixelsToWorkspaceUnits_(pixelCoord) {
+ const result = new 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.
+ const mainScale = this.workspace_.options.parentWorkspace.scale;
+ result.scale(1 / mainScale);
+ }
+ return result;
+ }
+
+ /**
+ * Move all of the icons connected to this drag.
+ * @param {!Coordinate} dxy How far to move the icons from their
+ * original positions, in workspace units.
+ * @protected
+ */
+ dragIcons_(dxy) {
+ // Moving icons moves their associated bubbles.
+ for (let i = 0; i < this.dragIconData_.length; i++) {
+ const data = this.dragIconData_[i];
+ data.icon.setIconLocation(Coordinate.sum(data.location, dxy));
+ }
+ }
+
+ /**
+ * Get a list of the insertion markers that currently exist. Drags have 0, 1,
+ * or 2 insertion markers.
+ * @return {!Array} A possibly empty list of insertion
+ * marker blocks.
+ * @public
+ */
+ getInsertionMarkers() {
+ // No insertion markers with the old style of dragged connection managers.
+ if (this.draggedConnectionManager_ &&
+ this.draggedConnectionManager_.getInsertionMarkers) {
+ return this.draggedConnectionManager_.getInsertionMarkers();
+ }
+ return [];
}
};
@@ -141,340 +476,6 @@ const initIconData = function(block) {
return dragIconData;
};
-/**
- * Start dragging a block. This includes moving it to the drag surface.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at mouse down, in pixel units.
- * @param {boolean} healStack Whether or not to heal the stack after
- * disconnecting.
- * @public
- */
-BlockDragger.prototype.startDrag = function(currentDragDeltaXY, healStack) {
- if (!eventUtils.getGroup()) {
- eventUtils.setGroup(true);
- }
- this.fireDragStartEvent_();
-
- // Mutators don't have the same type of z-ordering as the normal workspace
- // during a drag. They have to rely on the order of the blocks in the SVG.
- // For performance reasons that usually happens at the end of a drag,
- // but do it at the beginning for mutators.
- if (this.workspace_.isMutator) {
- this.draggingBlock_.bringToFront();
- }
-
- // During a drag there may be a lot of rerenders, but not field changes.
- // Turn the cache on so we don't do spurious remeasures during the drag.
- dom.startTextWidthCache();
- this.workspace_.setResizesEnabled(false);
- blockAnimation.disconnectUiStop();
-
- if (this.shouldDisconnect_(healStack)) {
- this.disconnectBlock_(healStack, currentDragDeltaXY);
- }
- 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();
-};
-
-/**
- * Whether or not we should disconnect the block when a drag is started.
- * @param {boolean} healStack Whether or not to heal the stack after
- * disconnecting.
- * @return {boolean} True to disconnect the block, false otherwise.
- * @protected
- */
-BlockDragger.prototype.shouldDisconnect_ = function(healStack) {
- return !!(
- this.draggingBlock_.getParent() ||
- (healStack && this.draggingBlock_.nextConnection &&
- this.draggingBlock_.nextConnection.targetBlock()));
-};
-
-/**
- * Disconnects the block and moves it to a new location.
- * @param {boolean} healStack Whether or not to heal the stack after
- * disconnecting.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at mouse down, in pixel units.
- * @protected
- */
-BlockDragger.prototype.disconnectBlock_ = function(
- healStack, currentDragDeltaXY) {
- this.draggingBlock_.unplug(healStack);
- const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
- const newLoc = Coordinate.sum(this.startXY_, delta);
-
- this.draggingBlock_.translate(newLoc.x, newLoc.y);
- blockAnimation.disconnectUiEffect(this.draggingBlock_);
- this.draggedConnectionManager_.updateAvailableConnections();
-};
-
-/**
- * Fire a UI event at the start of a block drag.
- * @protected
- */
-BlockDragger.prototype.fireDragStartEvent_ = function() {
- const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
- this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
- eventUtils.fire(event);
-};
-
-/**
- * Execute a step of block dragging, based on the given event. Update the
- * display accordingly.
- * @param {!Event} e The most recent move event.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at the start of the drag, in pixel units.
- * @public
- */
-BlockDragger.prototype.drag = function(e, currentDragDeltaXY) {
- const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
- const newLoc = Coordinate.sum(this.startXY_, delta);
- this.draggingBlock_.moveDuringDrag(newLoc);
- this.dragIcons_(delta);
-
- const oldDragTarget = this.dragTarget_;
- this.dragTarget_ = this.workspace_.getDragTarget(e);
-
- this.draggedConnectionManager_.update(delta, this.dragTarget_);
- const oldWouldDeleteBlock = this.wouldDeleteBlock_;
- this.wouldDeleteBlock_ = this.draggedConnectionManager_.wouldDeleteBlock();
- if (oldWouldDeleteBlock !== this.wouldDeleteBlock_) {
- // Prevent unnecessary add/remove class calls.
- this.updateCursorDuringBlockDrag_();
- }
-
- // Call drag enter/exit/over after wouldDeleteBlock is called in
- // InsertionMarkerManager.update.
- if (this.dragTarget_ !== oldDragTarget) {
- oldDragTarget && oldDragTarget.onDragExit(this.draggingBlock_);
- this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBlock_);
- }
- this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBlock_);
-};
-
-/**
- * Finish a block drag and put the block back on the workspace.
- * @param {!Event} e The mouseup/touchend event.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at the start of the drag, in pixel units.
- * @public
- */
-BlockDragger.prototype.endDrag = function(e, currentDragDeltaXY) {
- // Make sure internal state is fresh.
- this.drag(e, currentDragDeltaXY);
- this.dragIconData_ = [];
- this.fireDragEndEvent_();
-
- dom.stopTextWidthCache();
-
- blockAnimation.disconnectUiStop();
-
- const preventMove = !!this.dragTarget_ &&
- this.dragTarget_.shouldPreventMove(this.draggingBlock_);
- /** @type {Coordinate} */
- let newLoc;
- /** @type {Coordinate} */
- let delta;
- if (preventMove) {
- newLoc = this.startXY_;
- } else {
- const newValues = this.getNewLocationAfterDrag_(currentDragDeltaXY);
- delta = newValues.delta;
- newLoc = newValues.newLocation;
- }
- this.draggingBlock_.moveOffDragSurface(newLoc);
-
- if (this.dragTarget_) {
- this.dragTarget_.onDrop(this.draggingBlock_);
- }
-
- const deleted = this.maybeDeleteBlock_();
- if (!deleted) {
- // These are expensive and don't need to be done if we're deleting.
- this.draggingBlock_.setDragging(false);
- if (delta) { // !preventMove
- this.updateBlockAfterMove_(delta);
- } else {
- // Blocks dragged directly from a flyout may need to be bumped into
- // bounds.
- bumpObjects.bumpIntoBounds(
- this.draggingBlock_.workspace,
- this.workspace_.getMetricsManager().getScrollMetrics(true),
- this.draggingBlock_);
- }
- }
- this.workspace_.setResizesEnabled(true);
-
- eventUtils.setGroup(false);
-};
-
-/**
- * Calculates the drag delta and new location values after a block is dragged.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the start of the drag, in pixel units.
- * @return {{delta: !Coordinate, newLocation:
- * !Coordinate}} New location after drag. delta is in
- * workspace units. newLocation is the new coordinate where the block should
- * end up.
- * @protected
- */
-BlockDragger.prototype.getNewLocationAfterDrag_ = function(currentDragDeltaXY) {
- const newValues = {};
- newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
- newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta);
- return newValues;
-};
-
-/**
- * May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is not
- * true, the block will not be deleted. This should be called at the end of a
- * block drag.
- * @return {boolean} True if the block was deleted.
- * @protected
- */
-BlockDragger.prototype.maybeDeleteBlock_ = function() {
- if (this.wouldDeleteBlock_) {
- // Fire a move event, so we know where to go back to for an undo.
- this.fireMoveEvent_();
- this.draggingBlock_.dispose(false, true);
- common.draggingConnections.length = 0;
- return true;
- }
- return false;
-};
-
-/**
- * Updates the necessary information to place a block at a certain location.
- * @param {!Coordinate} delta The change in location from where
- * the block started the drag to where it ended the drag.
- * @protected
- */
-BlockDragger.prototype.updateBlockAfterMove_ = function(delta) {
- this.draggingBlock_.moveConnections(delta.x, delta.y);
- this.fireMoveEvent_();
- if (this.draggedConnectionManager_.wouldConnectBlock()) {
- // Applying connections also rerenders the relevant blocks.
- this.draggedConnectionManager_.applyConnections();
- } else {
- this.draggingBlock_.render();
- }
- this.draggingBlock_.scheduleSnapAndBump();
-};
-
-/**
- * Fire a UI event at the end of a block drag.
- * @protected
- */
-BlockDragger.prototype.fireDragEndEvent_ = function() {
- const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
- this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
- eventUtils.fire(event);
-};
-
-/**
- * Adds or removes the style of the cursor for the toolbox.
- * This is what changes the cursor to display an x when a deletable block is
- * held over the toolbox.
- * @param {boolean} isEnd True if we are at the end of a drag, false otherwise.
- * @protected
- */
-BlockDragger.prototype.updateToolboxStyle_ = function(isEnd) {
- const toolbox = this.workspace_.getToolbox();
-
- if (toolbox) {
- const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
- 'blocklyToolboxGrab';
-
- if (isEnd && typeof toolbox.removeStyle === 'function') {
- toolbox.removeStyle(style);
- } else if (!isEnd && typeof toolbox.addStyle === 'function') {
- toolbox.addStyle(style);
- }
- }
-};
-
-
-/**
- * Fire a move event at the end of a block drag.
- * @protected
- */
-BlockDragger.prototype.fireMoveEvent_ = function() {
- const event =
- new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_);
- event.oldCoordinate = this.startXY_;
- event.recordNew();
- eventUtils.fire(event);
-};
-
-/**
- * Update the cursor (and possibly the trash can lid) to reflect whether the
- * dragging block would be deleted if released immediately.
- * @protected
- */
-BlockDragger.prototype.updateCursorDuringBlockDrag_ = function() {
- this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
-};
-
-/**
- * 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 {!Coordinate} pixelCoord A coordinate with x and y
- * values in CSS pixel units.
- * @return {!Coordinate} The input coordinate divided by the
- * workspace scale.
- * @protected
- */
-BlockDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
- const result = new 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.
- const mainScale = this.workspace_.options.parentWorkspace.scale;
- result.scale(1 / mainScale);
- }
- return result;
-};
-
-/**
- * Move all of the icons connected to this drag.
- * @param {!Coordinate} dxy How far to move the icons from their
- * original positions, in workspace units.
- * @protected
- */
-BlockDragger.prototype.dragIcons_ = function(dxy) {
- // Moving icons moves their associated bubbles.
- for (let i = 0; i < this.dragIconData_.length; i++) {
- const data = this.dragIconData_[i];
- data.icon.setIconLocation(Coordinate.sum(data.location, dxy));
- }
-};
-
-/**
- * Get a list of the insertion markers that currently exist. Drags have 0, 1,
- * or 2 insertion markers.
- * @return {!Array} A possibly empty list of insertion
- * marker blocks.
- * @public
- */
-BlockDragger.prototype.getInsertionMarkers = function() {
- // No insertion markers with the old style of dragged connection managers.
- if (this.draggedConnectionManager_ &&
- this.draggedConnectionManager_.getInsertionMarkers) {
- return this.draggedConnectionManager_.getInsertionMarkers();
- }
- return [];
-};
-
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger);
exports.BlockDragger = BlockDragger;
diff --git a/core/bubble.js b/core/bubble.js
index 78799c0ec..1f045cf5e 100644
--- a/core/bubble.js
+++ b/core/bubble.js
@@ -40,133 +40,906 @@ goog.require('Blockly.Workspace');
/**
* Class for UI bubble.
- * @param {!WorkspaceSvg} workspace The workspace on which to draw the
- * bubble.
- * @param {!Element} content SVG content for the bubble.
- * @param {!Element} shape SVG element to avoid eclipsing.
- * @param {!Coordinate} anchorXY Absolute position of bubble's
- * anchor point.
- * @param {?number} bubbleWidth Width of bubble, or null if not resizable.
- * @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* @implements {IBubble}
- * @constructor
- * @alias Blockly.Bubble
*/
-const Bubble = function(
- workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) {
- this.workspace_ = workspace;
- this.content_ = content;
- this.shape_ = shape;
+const Bubble = class {
+ /**
+ * @param {!WorkspaceSvg} workspace The workspace on which to draw the
+ * bubble.
+ * @param {!Element} content SVG content for the bubble.
+ * @param {!Element} shape SVG element to avoid eclipsing.
+ * @param {!Coordinate} anchorXY Absolute position of bubble's
+ * anchor point.
+ * @param {?number} bubbleWidth Width of bubble, or null if not resizable.
+ * @param {?number} bubbleHeight Height of bubble, or null if not resizable.
+ * @struct
+ * @alias Blockly.Bubble
+ */
+ constructor(workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) {
+ this.workspace_ = workspace;
+ this.content_ = content;
+ this.shape_ = shape;
+
+ /**
+ * Flag to stop incremental rendering during construction.
+ * @type {boolean}
+ * @private
+ */
+ this.rendered_ = false;
+
+ /**
+ * The SVG group containing all parts of the bubble.
+ * @type {SVGGElement}
+ * @private
+ */
+ this.bubbleGroup_ = null;
+
+ /**
+ * The SVG path for the arrow from the bubble to the icon on the block.
+ * @type {SVGPathElement}
+ * @private
+ */
+ this.bubbleArrow_ = null;
+
+ /**
+ * The SVG rect for the main body of the bubble.
+ * @type {SVGRectElement}
+ * @private
+ */
+ this.bubbleBack_ = null;
+
+ /**
+ * The SVG group for the resize hash marks on some bubbles.
+ * @type {SVGGElement}
+ * @private
+ */
+ this.resizeGroup_ = null;
+
+ /**
+ * Absolute coordinate of anchor point, in workspace coordinates.
+ * @type {Coordinate}
+ * @private
+ */
+ this.anchorXY_ = null;
+
+ /**
+ * Relative X coordinate of bubble with respect to the anchor's centre,
+ * in workspace units.
+ * In RTL mode the initial value is negated.
+ * @type {number}
+ * @private
+ */
+ this.relativeLeft_ = 0;
+
+ /**
+ * Relative Y coordinate of bubble with respect to the anchor's centre, in
+ * workspace units.
+ * @type {number}
+ * @private
+ */
+ this.relativeTop_ = 0;
+
+ /**
+ * Width of bubble, in workspace units.
+ * @type {number}
+ * @private
+ */
+ this.width_ = 0;
+
+ /**
+ * Height of bubble, in workspace units.
+ * @type {number}
+ * @private
+ */
+ this.height_ = 0;
+
+ /**
+ * Automatically position and reposition the bubble.
+ * @type {boolean}
+ * @private
+ */
+ this.autoLayout_ = true;
+
+ /**
+ * Method to call on resize of bubble.
+ * @type {?function()}
+ * @private
+ */
+ this.resizeCallback_ = null;
+
+ /**
+ * Method to call on move of bubble.
+ * @type {?function()}
+ * @private
+ */
+ this.moveCallback_ = null;
+
+ /**
+ * Mouse down on bubbleBack_ event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
+ this.onMouseDownBubbleWrapper_ = null;
+
+ /**
+ * Mouse down on resizeGroup_ event data.
+ * @type {?browserEvents.Data}
+ * @private
+ */
+ this.onMouseDownResizeWrapper_ = null;
+
+ /**
+ * Describes whether this bubble has been disposed of (nodes and event
+ * listeners removed from the page) or not.
+ * @type {boolean}
+ * @package
+ */
+ this.disposed = false;
+
+ let angle = Bubble.ARROW_ANGLE;
+ if (this.workspace_.RTL) {
+ angle = -angle;
+ }
+ this.arrow_radians_ = math.toRadians(angle);
+
+ const canvas = workspace.getBubbleCanvas();
+ canvas.appendChild(
+ this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
+
+ this.setAnchorLocation(anchorXY);
+ if (!bubbleWidth || !bubbleHeight) {
+ const bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
+ bubbleWidth = bBox.width + 2 * Bubble.BORDER_WIDTH;
+ bubbleHeight = bBox.height + 2 * Bubble.BORDER_WIDTH;
+ }
+ this.setBubbleSize(bubbleWidth, bubbleHeight);
+
+ // Render the bubble.
+ this.positionBubble_();
+ this.renderArrow_();
+ this.rendered_ = true;
+ }
/**
- * Flag to stop incremental rendering during construction.
- * @type {boolean}
+ * Create the bubble's DOM.
+ * @param {!Element} content SVG content for the bubble.
+ * @param {boolean} hasResize Add diagonal resize gripper if true.
+ * @return {!SVGElement} The bubble's SVG group.
* @private
*/
- this.rendered_ = false;
+ createDom_(content, hasResize) {
+ /* Create the bubble. Here's the markup that will be generated:
+
+
+
+
+
+
+
+
+
+
+ [...content goes here...]
+
+ */
+ this.bubbleGroup_ = dom.createSvgElement(Svg.G, {}, null);
+ let filter = {
+ 'filter': 'url(#' +
+ this.workspace_.getRenderer().getConstants().embossFilterId + ')',
+ };
+ if (userAgent.JAVA_FX) {
+ // Multiple reports that JavaFX can't handle filters.
+ // https://github.com/google/blockly/issues/99
+ filter = {};
+ }
+ const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup_);
+ this.bubbleArrow_ = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
+ this.bubbleBack_ = dom.createSvgElement(
+ Svg.RECT, {
+ 'class': 'blocklyDraggable',
+ 'x': 0,
+ 'y': 0,
+ 'rx': Bubble.BORDER_WIDTH,
+ 'ry': Bubble.BORDER_WIDTH,
+ },
+ bubbleEmboss);
+ if (hasResize) {
+ this.resizeGroup_ = dom.createSvgElement(
+ Svg.G, {
+ 'class': this.workspace_.RTL ? 'blocklyResizeSW' :
+ 'blocklyResizeSE',
+ },
+ this.bubbleGroup_);
+ const resizeSize = 2 * Bubble.BORDER_WIDTH;
+ dom.createSvgElement(
+ Svg.POLYGON,
+ {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
+ this.resizeGroup_);
+ dom.createSvgElement(
+ Svg.LINE, {
+ 'class': 'blocklyResizeLine',
+ 'x1': resizeSize / 3,
+ 'y1': resizeSize - 1,
+ 'x2': resizeSize - 1,
+ 'y2': resizeSize / 3,
+ },
+ this.resizeGroup_);
+ dom.createSvgElement(
+ Svg.LINE, {
+ 'class': 'blocklyResizeLine',
+ 'x1': resizeSize * 2 / 3,
+ 'y1': resizeSize - 1,
+ 'x2': resizeSize - 1,
+ 'y2': resizeSize * 2 / 3,
+ },
+ this.resizeGroup_);
+ } else {
+ this.resizeGroup_ = null;
+ }
+
+ if (!this.workspace_.options.readOnly) {
+ this.onMouseDownBubbleWrapper_ = browserEvents.conditionalBind(
+ this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
+ if (this.resizeGroup_) {
+ this.onMouseDownResizeWrapper_ = browserEvents.conditionalBind(
+ this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
+ }
+ }
+ this.bubbleGroup_.appendChild(content);
+ return this.bubbleGroup_;
+ }
/**
- * Absolute coordinate of anchor point, in workspace coordinates.
- * @type {Coordinate}
+ * Return the root node of the bubble's SVG group.
+ * @return {!SVGElement} The root SVG node of the bubble's group.
+ */
+ getSvgRoot() {
+ return /** @type {!SVGElement} */ (this.bubbleGroup_);
+ }
+
+ /**
+ * Expose the block's ID on the bubble's top-level SVG group.
+ * @param {string} id ID of block.
+ */
+ setSvgId(id) {
+ if (this.bubbleGroup_.dataset) {
+ this.bubbleGroup_.dataset['blockId'] = id;
+ }
+ }
+
+ /**
+ * Handle a mouse-down on bubble's border.
+ * @param {!Event} e Mouse down event.
* @private
*/
- this.anchorXY_ = null;
+ bubbleMouseDown_(e) {
+ const gesture = this.workspace_.getGesture(e);
+ if (gesture) {
+ gesture.handleBubbleStart(e, this);
+ }
+ }
/**
- * Relative X coordinate of bubble with respect to the anchor's centre,
- * in workspace units.
- * In RTL mode the initial value is negated.
- * @type {number}
- * @private
- */
- this.relativeLeft_ = 0;
-
- /**
- * Relative Y coordinate of bubble with respect to the anchor's centre, in
- * workspace units.
- * @type {number}
- * @private
- */
- this.relativeTop_ = 0;
-
- /**
- * Width of bubble, in workspace units.
- * @type {number}
- * @private
- */
- this.width_ = 0;
-
- /**
- * Height of bubble, in workspace units.
- * @type {number}
- * @private
- */
- this.height_ = 0;
-
- /**
- * Automatically position and reposition the bubble.
- * @type {boolean}
- * @private
- */
- this.autoLayout_ = true;
-
- /**
- * Method to call on resize of bubble.
- * @type {?function()}
- * @private
- */
- this.resizeCallback_ = null;
-
- /**
- * Method to call on move of bubble.
- * @type {?function()}
- * @private
- */
- this.moveCallback_ = null;
-
- /**
- * Mouse down on bubbleBack_ event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onMouseDownBubbleWrapper_ = null;
-
- /**
- * Mouse down on resizeGroup_ event data.
- * @type {?browserEvents.Data}
- * @private
- */
- this.onMouseDownResizeWrapper_ = null;
-
- /**
- * Describes whether this bubble has been disposed of (nodes and event
- * listeners removed from the page) or not.
- * @type {boolean}
+ * Show the context menu for this bubble.
+ * @param {!Event} _e Mouse event.
* @package
*/
- this.disposed = false;
-
- let angle = Bubble.ARROW_ANGLE;
- if (this.workspace_.RTL) {
- angle = -angle;
+ showContextMenu(_e) {
+ // NOP on bubbles, but used by the bubble dragger to pass events to
+ // workspace comments.
}
- this.arrow_radians_ = math.toRadians(angle);
- const canvas = workspace.getBubbleCanvas();
- canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
-
- this.setAnchorLocation(anchorXY);
- if (!bubbleWidth || !bubbleHeight) {
- const bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
- bubbleWidth = bBox.width + 2 * Bubble.BORDER_WIDTH;
- bubbleHeight = bBox.height + 2 * Bubble.BORDER_WIDTH;
+ /**
+ * Get whether this bubble is deletable or not.
+ * @return {boolean} True if deletable.
+ * @package
+ */
+ isDeletable() {
+ return false;
}
- this.setBubbleSize(bubbleWidth, bubbleHeight);
- // Render the bubble.
- this.positionBubble_();
- this.renderArrow_();
- this.rendered_ = true;
+ /**
+ * Update the style of this bubble when it is dragged over a delete area.
+ * @param {boolean} _enable True if the bubble is about to be deleted, false
+ * otherwise.
+ */
+ setDeleteStyle(_enable) {
+ // NOP if bubble is not deletable.
+ }
+
+ /**
+ * Handle a mouse-down on bubble's resize corner.
+ * @param {!Event} e Mouse down event.
+ * @private
+ */
+ resizeMouseDown_(e) {
+ this.promote();
+ Bubble.unbindDragEvents_();
+ if (browserEvents.isRightButton(e)) {
+ // No right-click.
+ e.stopPropagation();
+ return;
+ }
+ // Left-click (or middle click)
+ this.workspace_.startDrag(
+ e,
+ new Coordinate(
+ this.workspace_.RTL ? -this.width_ : this.width_, this.height_));
+
+ Bubble.onMouseUpWrapper_ = browserEvents.conditionalBind(
+ document, 'mouseup', this, Bubble.bubbleMouseUp_);
+ Bubble.onMouseMoveWrapper_ = browserEvents.conditionalBind(
+ document, 'mousemove', this, this.resizeMouseMove_);
+ this.workspace_.hideChaff();
+ // This event has been handled. No need to bubble up to the document.
+ e.stopPropagation();
+ }
+
+ /**
+ * Resize this bubble to follow the mouse.
+ * @param {!Event} e Mouse move event.
+ * @private
+ */
+ resizeMouseMove_(e) {
+ this.autoLayout_ = false;
+ const newXY = this.workspace_.moveDrag(e);
+ this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
+ if (this.workspace_.RTL) {
+ // RTL requires the bubble to move its left edge.
+ this.positionBubble_();
+ }
+ }
+
+ /**
+ * Register a function as a callback event for when the bubble is resized.
+ * @param {!Function} callback The function to call on resize.
+ */
+ registerResizeEvent(callback) {
+ this.resizeCallback_ = callback;
+ }
+
+ /**
+ * Register a function as a callback event for when the bubble is moved.
+ * @param {!Function} callback The function to call on move.
+ */
+ registerMoveEvent(callback) {
+ this.moveCallback_ = callback;
+ }
+
+ /**
+ * Move this bubble to the top of the stack.
+ * @return {boolean} Whether or not the bubble has been moved.
+ * @package
+ */
+ promote() {
+ const svgGroup = this.bubbleGroup_.parentNode;
+ if (svgGroup.lastChild !== this.bubbleGroup_) {
+ svgGroup.appendChild(this.bubbleGroup_);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Notification that the anchor has moved.
+ * Update the arrow and bubble accordingly.
+ * @param {!Coordinate} xy Absolute location.
+ */
+ setAnchorLocation(xy) {
+ this.anchorXY_ = xy;
+ if (this.rendered_) {
+ this.positionBubble_();
+ }
+ }
+
+ /**
+ * Position the bubble so that it does not fall off-screen.
+ * @private
+ */
+ layoutBubble_() {
+ // Get the metrics in workspace units.
+ const viewMetrics =
+ this.workspace_.getMetricsManager().getViewMetrics(true);
+
+ const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics);
+ const optimalTop = this.getOptimalRelativeTop_(viewMetrics);
+ const bbox = this.shape_.getBBox();
+
+ const topPosition = {
+ x: optimalLeft,
+ y: -this.height_ -
+ this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT,
+ };
+ const startPosition = {x: -this.width_ - 30, y: optimalTop};
+ const endPosition = {x: bbox.width, y: optimalTop};
+ const bottomPosition = {x: optimalLeft, y: bbox.height};
+
+ const closerPosition =
+ bbox.width < bbox.height ? endPosition : bottomPosition;
+ const fartherPosition =
+ bbox.width < bbox.height ? bottomPosition : endPosition;
+
+ const topPositionOverlap = this.getOverlap_(topPosition, viewMetrics);
+ const startPositionOverlap = this.getOverlap_(startPosition, viewMetrics);
+ const closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics);
+ const fartherPositionOverlap =
+ this.getOverlap_(fartherPosition, viewMetrics);
+
+ // Set the position to whichever position shows the most of the bubble,
+ // with tiebreaks going in the order: top > start > close > far.
+ const mostOverlap = Math.max(
+ topPositionOverlap, startPositionOverlap, closerPositionOverlap,
+ fartherPositionOverlap);
+ if (topPositionOverlap === mostOverlap) {
+ this.relativeLeft_ = topPosition.x;
+ this.relativeTop_ = topPosition.y;
+ return;
+ }
+ if (startPositionOverlap === mostOverlap) {
+ this.relativeLeft_ = startPosition.x;
+ this.relativeTop_ = startPosition.y;
+ return;
+ }
+ if (closerPositionOverlap === mostOverlap) {
+ this.relativeLeft_ = closerPosition.x;
+ this.relativeTop_ = closerPosition.y;
+ return;
+ }
+ // TODO: I believe relativeLeft_ should actually be called relativeStart_
+ // and then the math should be fixed to reflect this. (hopefully it'll
+ // make it look simpler)
+ this.relativeLeft_ = fartherPosition.x;
+ this.relativeTop_ = fartherPosition.y;
+ }
+
+ /**
+ * Calculate the what percentage of the bubble overlaps with the visible
+ * workspace (what percentage of the bubble is visible).
+ * @param {!{x: number, y: number}} relativeMin The position of the top-left
+ * corner of the bubble relative to the anchor point.
+ * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
+ * of the workspace the bubble will appear in.
+ * @return {number} The percentage of the bubble that is visible.
+ * @private
+ */
+ getOverlap_(relativeMin, viewMetrics) {
+ // The position of the top-left corner of the bubble in workspace units.
+ const bubbleMin = {
+ x: this.workspace_.RTL ?
+ (this.anchorXY_.x - relativeMin.x - this.width_) :
+ (relativeMin.x + this.anchorXY_.x),
+ y: relativeMin.y + this.anchorXY_.y,
+ };
+ // The position of the bottom-right corner of the bubble in workspace units.
+ const bubbleMax = {
+ x: bubbleMin.x + this.width_,
+ y: bubbleMin.y + this.height_,
+ };
+
+ // We could adjust these values to account for the scrollbars, but the
+ // bubbles should have been adjusted to not collide with them anyway, so
+ // giving the workspace a slightly larger "bounding box" shouldn't affect
+ // the calculation.
+
+ // The position of the top-left corner of the workspace.
+ const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top};
+ // The position of the bottom-right corner of the workspace.
+ const workspaceMax = {
+ x: viewMetrics.left + viewMetrics.width,
+ y: viewMetrics.top + viewMetrics.height,
+ };
+
+ const overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) -
+ Math.max(bubbleMin.x, workspaceMin.x);
+ const overlapHeight = Math.min(bubbleMax.y, workspaceMax.y) -
+ Math.max(bubbleMin.y, workspaceMin.y);
+ return Math.max(
+ 0,
+ Math.min(
+ 1, (overlapWidth * overlapHeight) / (this.width_ * this.height_)));
+ }
+
+ /**
+ * Calculate what the optimal horizontal position of the top-left corner of
+ * the bubble is (relative to the anchor point) so that the most area of the
+ * bubble is shown.
+ * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
+ * of the workspace the bubble will appear in.
+ * @return {number} The optimal horizontal position of the top-left corner
+ * of the bubble.
+ * @private
+ */
+ getOptimalRelativeLeft_(viewMetrics) {
+ let relativeLeft = -this.width_ / 4;
+
+ // No amount of sliding left or right will give us a better overlap.
+ if (this.width_ > viewMetrics.width) {
+ return relativeLeft;
+ }
+
+ if (this.workspace_.RTL) {
+ // Bubble coordinates are flipped in RTL.
+ const bubbleRight = this.anchorXY_.x - relativeLeft;
+ const bubbleLeft = bubbleRight - this.width_;
+
+ const workspaceRight = viewMetrics.left + viewMetrics.width;
+ const workspaceLeft = viewMetrics.left +
+ // Thickness in workspace units.
+ (Scrollbar.scrollbarThickness / this.workspace_.scale);
+
+ if (bubbleLeft < workspaceLeft) {
+ // Slide the bubble right until it is onscreen.
+ relativeLeft = -(workspaceLeft - this.anchorXY_.x + this.width_);
+ } else if (bubbleRight > workspaceRight) {
+ // Slide the bubble left until it is onscreen.
+ relativeLeft = -(workspaceRight - this.anchorXY_.x);
+ }
+ } else {
+ const bubbleLeft = relativeLeft + this.anchorXY_.x;
+ const bubbleRight = bubbleLeft + this.width_;
+
+ const workspaceLeft = viewMetrics.left;
+ const workspaceRight = viewMetrics.left + viewMetrics.width -
+ // Thickness in workspace units.
+ (Scrollbar.scrollbarThickness / this.workspace_.scale);
+
+ if (bubbleLeft < workspaceLeft) {
+ // Slide the bubble right until it is onscreen.
+ relativeLeft = workspaceLeft - this.anchorXY_.x;
+ } else if (bubbleRight > workspaceRight) {
+ // Slide the bubble left until it is onscreen.
+ relativeLeft = workspaceRight - this.anchorXY_.x - this.width_;
+ }
+ }
+
+ return relativeLeft;
+ }
+
+ /**
+ * Calculate what the optimal vertical position of the top-left corner of
+ * the bubble is (relative to the anchor point) so that the most area of the
+ * bubble is shown.
+ * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
+ * of the workspace the bubble will appear in.
+ * @return {number} The optimal vertical position of the top-left corner
+ * of the bubble.
+ * @private
+ */
+ getOptimalRelativeTop_(viewMetrics) {
+ let relativeTop = -this.height_ / 4;
+
+ // No amount of sliding up or down will give us a better overlap.
+ if (this.height_ > viewMetrics.height) {
+ return relativeTop;
+ }
+
+ const bubbleTop = this.anchorXY_.y + relativeTop;
+ const bubbleBottom = bubbleTop + this.height_;
+ const workspaceTop = viewMetrics.top;
+ const workspaceBottom = viewMetrics.top + viewMetrics.height -
+ // Thickness in workspace units.
+ (Scrollbar.scrollbarThickness / this.workspace_.scale);
+
+ const anchorY = this.anchorXY_.y;
+ if (bubbleTop < workspaceTop) {
+ // Slide the bubble down until it is onscreen.
+ relativeTop = workspaceTop - anchorY;
+ } else if (bubbleBottom > workspaceBottom) {
+ // Slide the bubble up until it is onscreen.
+ relativeTop = workspaceBottom - anchorY - this.height_;
+ }
+
+ return relativeTop;
+ }
+
+ /**
+ * Move the bubble to a location relative to the anchor's centre.
+ * @private
+ */
+ positionBubble_() {
+ let left = this.anchorXY_.x;
+ if (this.workspace_.RTL) {
+ left -= this.relativeLeft_ + this.width_;
+ } else {
+ left += this.relativeLeft_;
+ }
+ const top = this.relativeTop_ + this.anchorXY_.y;
+ this.moveTo(left, top);
+ }
+
+ /**
+ * Move the bubble group to the specified location in workspace coordinates.
+ * @param {number} x The x position to move to.
+ * @param {number} y The y position to move to.
+ * @package
+ */
+ moveTo(x, y) {
+ this.bubbleGroup_.setAttribute(
+ 'transform', 'translate(' + x + ',' + y + ')');
+ }
+
+ /**
+ * Triggers a move callback if one exists at the end of a drag.
+ * @param {boolean} adding True if adding, false if removing.
+ * @package
+ */
+ setDragging(adding) {
+ if (!adding && this.moveCallback_) {
+ this.moveCallback_();
+ }
+ }
+
+ /**
+ * Get the dimensions of this bubble.
+ * @return {!Size} The height and width of the bubble.
+ */
+ getBubbleSize() {
+ return new Size(this.width_, this.height_);
+ }
+
+ /**
+ * Size this bubble.
+ * @param {number} width Width of the bubble.
+ * @param {number} height Height of the bubble.
+ */
+ setBubbleSize(width, height) {
+ const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
+ // Minimum size of a bubble.
+ width = Math.max(width, doubleBorderWidth + 45);
+ height = Math.max(height, doubleBorderWidth + 20);
+ this.width_ = width;
+ this.height_ = height;
+ this.bubbleBack_.setAttribute('width', width);
+ this.bubbleBack_.setAttribute('height', height);
+ if (this.resizeGroup_) {
+ if (this.workspace_.RTL) {
+ // Mirror the resize group.
+ const resizeSize = 2 * Bubble.BORDER_WIDTH;
+ this.resizeGroup_.setAttribute(
+ 'transform',
+ 'translate(' + resizeSize + ',' + (height - doubleBorderWidth) +
+ ') scale(-1 1)');
+ } else {
+ this.resizeGroup_.setAttribute(
+ 'transform',
+ 'translate(' + (width - doubleBorderWidth) + ',' +
+ (height - doubleBorderWidth) + ')');
+ }
+ }
+ if (this.autoLayout_) {
+ this.layoutBubble_();
+ }
+ this.positionBubble_();
+ this.renderArrow_();
+
+ // Allow the contents to resize.
+ if (this.resizeCallback_) {
+ this.resizeCallback_();
+ }
+ }
+
+ /**
+ * Draw the arrow between the bubble and the origin.
+ * @private
+ */
+ renderArrow_() {
+ const steps = [];
+ // Find the relative coordinates of the center of the bubble.
+ const relBubbleX = this.width_ / 2;
+ const relBubbleY = this.height_ / 2;
+ // Find the relative coordinates of the center of the anchor.
+ let relAnchorX = -this.relativeLeft_;
+ let relAnchorY = -this.relativeTop_;
+ if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
+ // Null case. Bubble is directly on top of the anchor.
+ // Short circuit this rather than wade through divide by zeros.
+ steps.push('M ' + relBubbleX + ',' + relBubbleY);
+ } else {
+ // Compute the angle of the arrow's line.
+ const rise = relAnchorY - relBubbleY;
+ let run = relAnchorX - relBubbleX;
+ if (this.workspace_.RTL) {
+ run *= -1;
+ }
+ const hypotenuse = Math.sqrt(rise * rise + run * run);
+ let angle = Math.acos(run / hypotenuse);
+ if (rise < 0) {
+ angle = 2 * Math.PI - angle;
+ }
+ // Compute a line perpendicular to the arrow.
+ let rightAngle = angle + Math.PI / 2;
+ if (rightAngle > Math.PI * 2) {
+ rightAngle -= Math.PI * 2;
+ }
+ const rightRise = Math.sin(rightAngle);
+ const rightRun = Math.cos(rightAngle);
+
+ // Calculate the thickness of the base of the arrow.
+ const bubbleSize = this.getBubbleSize();
+ let thickness =
+ (bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS;
+ thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
+
+ // Back the tip of the arrow off of the anchor.
+ const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse;
+ relAnchorX = relBubbleX + backoffRatio * run;
+ relAnchorY = relBubbleY + backoffRatio * rise;
+
+ // Coordinates for the base of the arrow.
+ const baseX1 = relBubbleX + thickness * rightRun;
+ const baseY1 = relBubbleY + thickness * rightRise;
+ const baseX2 = relBubbleX - thickness * rightRun;
+ const baseY2 = relBubbleY - thickness * rightRise;
+
+ // Distortion to curve the arrow.
+ let swirlAngle = angle + this.arrow_radians_;
+ if (swirlAngle > Math.PI * 2) {
+ swirlAngle -= Math.PI * 2;
+ }
+ const swirlRise = Math.sin(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
+ const swirlRun = Math.cos(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
+
+ steps.push('M' + baseX1 + ',' + baseY1);
+ steps.push(
+ 'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' +
+ relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY);
+ steps.push(
+ 'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) +
+ ',' + (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2);
+ }
+ steps.push('z');
+ this.bubbleArrow_.setAttribute('d', steps.join(' '));
+ }
+
+ /**
+ * Change the colour of a bubble.
+ * @param {string} hexColour Hex code of colour.
+ */
+ setColour(hexColour) {
+ this.bubbleBack_.setAttribute('fill', hexColour);
+ this.bubbleArrow_.setAttribute('fill', hexColour);
+ }
+
+ /**
+ * Dispose of this bubble.
+ */
+ dispose() {
+ if (this.onMouseDownBubbleWrapper_) {
+ browserEvents.unbind(this.onMouseDownBubbleWrapper_);
+ }
+ if (this.onMouseDownResizeWrapper_) {
+ browserEvents.unbind(this.onMouseDownResizeWrapper_);
+ }
+ Bubble.unbindDragEvents_();
+ dom.removeNode(this.bubbleGroup_);
+ this.disposed = true;
+ }
+
+ /**
+ * Move this bubble during a drag, taking into account whether or not there is
+ * a drag surface.
+ * @param {BlockDragSurfaceSvg} dragSurface The surface that carries
+ * rendered items during a drag, or null if no drag surface is in use.
+ * @param {!Coordinate} newLoc The location to translate to, in
+ * workspace coordinates.
+ * @package
+ */
+ moveDuringDrag(dragSurface, newLoc) {
+ if (dragSurface) {
+ dragSurface.translateSurface(newLoc.x, newLoc.y);
+ } else {
+ this.moveTo(newLoc.x, newLoc.y);
+ }
+ if (this.workspace_.RTL) {
+ this.relativeLeft_ = this.anchorXY_.x - newLoc.x - this.width_;
+ } else {
+ this.relativeLeft_ = newLoc.x - this.anchorXY_.x;
+ }
+ this.relativeTop_ = newLoc.y - this.anchorXY_.y;
+ this.renderArrow_();
+ }
+
+ /**
+ * Return the coordinates of the top-left corner of this bubble's body
+ * relative to the drawing surface's origin (0,0), in workspace units.
+ * @return {!Coordinate} Object with .x and .y properties.
+ */
+ getRelativeToSurfaceXY() {
+ return new Coordinate(
+ this.workspace_.RTL ?
+ -this.relativeLeft_ + this.anchorXY_.x - this.width_ :
+ this.anchorXY_.x + this.relativeLeft_,
+ this.anchorXY_.y + this.relativeTop_);
+ }
+
+ /**
+ * Set whether auto-layout of this bubble is enabled. The first time a bubble
+ * is shown it positions itself to not cover any blocks. Once a user has
+ * dragged it to reposition, it renders where the user put it.
+ * @param {boolean} enable True if auto-layout should be enabled, false
+ * otherwise.
+ * @package
+ */
+ setAutoLayout(enable) {
+ this.autoLayout_ = enable;
+ }
+
+ /**
+ * Stop binding to the global mouseup and mousemove events.
+ * @private
+ */
+ static unbindDragEvents_() {
+ if (Bubble.onMouseUpWrapper_) {
+ browserEvents.unbind(Bubble.onMouseUpWrapper_);
+ Bubble.onMouseUpWrapper_ = null;
+ }
+ if (Bubble.onMouseMoveWrapper_) {
+ browserEvents.unbind(Bubble.onMouseMoveWrapper_);
+ Bubble.onMouseMoveWrapper_ = null;
+ }
+ }
+
+ /**
+ * Handle a mouse-up event while dragging a bubble's border or resize handle.
+ * @param {!Event} _e Mouse up event.
+ * @private
+ */
+ static bubbleMouseUp_(_e) {
+ Touch.clearTouchIdentifier();
+ Bubble.unbindDragEvents_();
+ }
+
+ /**
+ * Create the text for a non editable bubble.
+ * @param {string} text The text to display.
+ * @return {!SVGTextElement} The top-level node of the text.
+ * @package
+ */
+ static textToDom(text) {
+ const paragraph = dom.createSvgElement(
+ Svg.TEXT, {
+ 'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
+ 'y': Bubble.BORDER_WIDTH,
+ },
+ null);
+ const lines = text.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ const tspanElement = dom.createSvgElement(
+ Svg.TSPAN, {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, paragraph);
+ const textNode = document.createTextNode(lines[i]);
+ tspanElement.appendChild(textNode);
+ }
+ return paragraph;
+ }
+
+ /**
+ * Creates a bubble that can not be edited.
+ * @param {!SVGTextElement} paragraphElement The text element for the non
+ * editable bubble.
+ * @param {!BlockSvg} block The block that the bubble is attached to.
+ * @param {!Coordinate} iconXY The coordinate of the icon.
+ * @return {!Bubble} The non editable bubble.
+ * @package
+ */
+ static createNonEditableBubble(paragraphElement, block, iconXY) {
+ const bubble = new Bubble(
+ /** @type {!WorkspaceSvg} */ (block.workspace), paragraphElement,
+ block.pathObject.svgPath,
+ /** @type {!Coordinate} */ (iconXY), null, null);
+ // Expose this bubble's block's ID on its top-level SVG group.
+ bubble.setSvgId(block.id);
+ if (block.RTL) {
+ // Right-align the paragraph.
+ // This cannot be done until the bubble is rendered on screen.
+ const maxWidth = paragraphElement.getBBox().width;
+ for (let i = 0, textElement;
+ (textElement = paragraphElement.childNodes[i]); i++) {
+ textElement.setAttribute('text-anchor', 'end');
+ textElement.setAttribute('x', maxWidth + Bubble.BORDER_WIDTH);
+ }
+ }
+ return bubble;
+ }
};
/**
@@ -209,739 +982,4 @@ Bubble.onMouseUpWrapper_ = null;
*/
Bubble.onMouseMoveWrapper_ = null;
-/**
- * Stop binding to the global mouseup and mousemove events.
- * @private
- */
-Bubble.unbindDragEvents_ = function() {
- if (Bubble.onMouseUpWrapper_) {
- browserEvents.unbind(Bubble.onMouseUpWrapper_);
- Bubble.onMouseUpWrapper_ = null;
- }
- if (Bubble.onMouseMoveWrapper_) {
- browserEvents.unbind(Bubble.onMouseMoveWrapper_);
- Bubble.onMouseMoveWrapper_ = null;
- }
-};
-
-/**
- * Handle a mouse-up event while dragging a bubble's border or resize handle.
- * @param {!Event} _e Mouse up event.
- * @private
- */
-Bubble.bubbleMouseUp_ = function(_e) {
- Touch.clearTouchIdentifier();
- Bubble.unbindDragEvents_();
-};
-
-/**
- * Create the bubble's DOM.
- * @param {!Element} content SVG content for the bubble.
- * @param {boolean} hasResize Add diagonal resize gripper if true.
- * @return {!SVGElement} The bubble's SVG group.
- * @private
- */
-Bubble.prototype.createDom_ = function(content, hasResize) {
- /* Create the bubble. Here's the markup that will be generated:
-
-
-
-
-
-
-
-
-
-
- [...content goes here...]
-
- */
- this.bubbleGroup_ = dom.createSvgElement(Svg.G, {}, null);
- let filter = {
- 'filter': 'url(#' +
- this.workspace_.getRenderer().getConstants().embossFilterId + ')',
- };
- if (userAgent.JAVA_FX) {
- // Multiple reports that JavaFX can't handle filters.
- // https://github.com/google/blockly/issues/99
- filter = {};
- }
- const bubbleEmboss = dom.createSvgElement(Svg.G, filter, this.bubbleGroup_);
- this.bubbleArrow_ = dom.createSvgElement(Svg.PATH, {}, bubbleEmboss);
- this.bubbleBack_ = dom.createSvgElement(
- Svg.RECT, {
- 'class': 'blocklyDraggable',
- 'x': 0,
- 'y': 0,
- 'rx': Bubble.BORDER_WIDTH,
- 'ry': Bubble.BORDER_WIDTH,
- },
- bubbleEmboss);
- if (hasResize) {
- this.resizeGroup_ = dom.createSvgElement(
- Svg.G,
- {'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'},
- this.bubbleGroup_);
- const resizeSize = 2 * Bubble.BORDER_WIDTH;
- dom.createSvgElement(
- Svg.POLYGON,
- {'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
- this.resizeGroup_);
- dom.createSvgElement(
- Svg.LINE, {
- 'class': 'blocklyResizeLine',
- 'x1': resizeSize / 3,
- 'y1': resizeSize - 1,
- 'x2': resizeSize - 1,
- 'y2': resizeSize / 3,
- },
- this.resizeGroup_);
- dom.createSvgElement(
- Svg.LINE, {
- 'class': 'blocklyResizeLine',
- 'x1': resizeSize * 2 / 3,
- 'y1': resizeSize - 1,
- 'x2': resizeSize - 1,
- 'y2': resizeSize * 2 / 3,
- },
- this.resizeGroup_);
- } else {
- this.resizeGroup_ = null;
- }
-
- if (!this.workspace_.options.readOnly) {
- this.onMouseDownBubbleWrapper_ = browserEvents.conditionalBind(
- this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_);
- if (this.resizeGroup_) {
- this.onMouseDownResizeWrapper_ = browserEvents.conditionalBind(
- this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
- }
- }
- this.bubbleGroup_.appendChild(content);
- return this.bubbleGroup_;
-};
-
-/**
- * Return the root node of the bubble's SVG group.
- * @return {!SVGElement} The root SVG node of the bubble's group.
- */
-Bubble.prototype.getSvgRoot = function() {
- return this.bubbleGroup_;
-};
-
-/**
- * Expose the block's ID on the bubble's top-level SVG group.
- * @param {string} id ID of block.
- */
-Bubble.prototype.setSvgId = function(id) {
- if (this.bubbleGroup_.dataset) {
- this.bubbleGroup_.dataset['blockId'] = id;
- }
-};
-
-/**
- * Handle a mouse-down on bubble's border.
- * @param {!Event} e Mouse down event.
- * @private
- */
-Bubble.prototype.bubbleMouseDown_ = function(e) {
- const gesture = this.workspace_.getGesture(e);
- if (gesture) {
- gesture.handleBubbleStart(e, this);
- }
-};
-
-/**
- * Show the context menu for this bubble.
- * @param {!Event} _e Mouse event.
- * @package
- */
-Bubble.prototype.showContextMenu = function(_e) {
- // NOP on bubbles, but used by the bubble dragger to pass events to
- // workspace comments.
-};
-
-/**
- * Get whether this bubble is deletable or not.
- * @return {boolean} True if deletable.
- * @package
- */
-Bubble.prototype.isDeletable = function() {
- return false;
-};
-
-/**
- * Update the style of this bubble when it is dragged over a delete area.
- * @param {boolean} _enable True if the bubble is about to be deleted, false
- * otherwise.
- */
-Bubble.prototype.setDeleteStyle = function(_enable) {
- // NOP if bubble is not deletable.
-};
-
-/**
- * Handle a mouse-down on bubble's resize corner.
- * @param {!Event} e Mouse down event.
- * @private
- */
-Bubble.prototype.resizeMouseDown_ = function(e) {
- this.promote();
- Bubble.unbindDragEvents_();
- if (browserEvents.isRightButton(e)) {
- // No right-click.
- e.stopPropagation();
- return;
- }
- // Left-click (or middle click)
- this.workspace_.startDrag(
- e,
- new Coordinate(
- this.workspace_.RTL ? -this.width_ : this.width_, this.height_));
-
- Bubble.onMouseUpWrapper_ = browserEvents.conditionalBind(
- document, 'mouseup', this, Bubble.bubbleMouseUp_);
- Bubble.onMouseMoveWrapper_ = browserEvents.conditionalBind(
- document, 'mousemove', this, this.resizeMouseMove_);
- this.workspace_.hideChaff();
- // This event has been handled. No need to bubble up to the document.
- e.stopPropagation();
-};
-
-/**
- * Resize this bubble to follow the mouse.
- * @param {!Event} e Mouse move event.
- * @private
- */
-Bubble.prototype.resizeMouseMove_ = function(e) {
- this.autoLayout_ = false;
- const newXY = this.workspace_.moveDrag(e);
- this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
- if (this.workspace_.RTL) {
- // RTL requires the bubble to move its left edge.
- this.positionBubble_();
- }
-};
-
-/**
- * Register a function as a callback event for when the bubble is resized.
- * @param {!Function} callback The function to call on resize.
- */
-Bubble.prototype.registerResizeEvent = function(callback) {
- this.resizeCallback_ = callback;
-};
-
-/**
- * Register a function as a callback event for when the bubble is moved.
- * @param {!Function} callback The function to call on move.
- */
-Bubble.prototype.registerMoveEvent = function(callback) {
- this.moveCallback_ = callback;
-};
-
-/**
- * Move this bubble to the top of the stack.
- * @return {boolean} Whether or not the bubble has been moved.
- * @package
- */
-Bubble.prototype.promote = function() {
- const svgGroup = this.bubbleGroup_.parentNode;
- if (svgGroup.lastChild !== this.bubbleGroup_) {
- svgGroup.appendChild(this.bubbleGroup_);
- return true;
- }
- return false;
-};
-
-/**
- * Notification that the anchor has moved.
- * Update the arrow and bubble accordingly.
- * @param {!Coordinate} xy Absolute location.
- */
-Bubble.prototype.setAnchorLocation = function(xy) {
- this.anchorXY_ = xy;
- if (this.rendered_) {
- this.positionBubble_();
- }
-};
-
-/**
- * Position the bubble so that it does not fall off-screen.
- * @private
- */
-Bubble.prototype.layoutBubble_ = function() {
- // Get the metrics in workspace units.
- const viewMetrics = this.workspace_.getMetricsManager().getViewMetrics(true);
-
- const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics);
- const optimalTop = this.getOptimalRelativeTop_(viewMetrics);
- const bbox = this.shape_.getBBox();
-
- const topPosition = {
- x: optimalLeft,
- y: -this.height_ -
- this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT,
- };
- const startPosition = {x: -this.width_ - 30, y: optimalTop};
- const endPosition = {x: bbox.width, y: optimalTop};
- const bottomPosition = {x: optimalLeft, y: bbox.height};
-
- const closerPosition =
- bbox.width < bbox.height ? endPosition : bottomPosition;
- const fartherPosition =
- bbox.width < bbox.height ? bottomPosition : endPosition;
-
- const topPositionOverlap = this.getOverlap_(topPosition, viewMetrics);
- const startPositionOverlap = this.getOverlap_(startPosition, viewMetrics);
- const closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics);
- const fartherPositionOverlap = this.getOverlap_(fartherPosition, viewMetrics);
-
- // Set the position to whichever position shows the most of the bubble,
- // with tiebreaks going in the order: top > start > close > far.
- const mostOverlap = Math.max(
- topPositionOverlap, startPositionOverlap, closerPositionOverlap,
- fartherPositionOverlap);
- if (topPositionOverlap === mostOverlap) {
- this.relativeLeft_ = topPosition.x;
- this.relativeTop_ = topPosition.y;
- return;
- }
- if (startPositionOverlap === mostOverlap) {
- this.relativeLeft_ = startPosition.x;
- this.relativeTop_ = startPosition.y;
- return;
- }
- if (closerPositionOverlap === mostOverlap) {
- this.relativeLeft_ = closerPosition.x;
- this.relativeTop_ = closerPosition.y;
- return;
- }
- // TODO: I believe relativeLeft_ should actually be called relativeStart_
- // and then the math should be fixed to reflect this. (hopefully it'll
- // make it look simpler)
- this.relativeLeft_ = fartherPosition.x;
- this.relativeTop_ = fartherPosition.y;
-};
-
-/**
- * Calculate the what percentage of the bubble overlaps with the visible
- * workspace (what percentage of the bubble is visible).
- * @param {!{x: number, y: number}} relativeMin The position of the top-left
- * corner of the bubble relative to the anchor point.
- * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
- * of the workspace the bubble will appear in.
- * @return {number} The percentage of the bubble that is visible.
- * @private
- */
-Bubble.prototype.getOverlap_ = function(relativeMin, viewMetrics) {
- // The position of the top-left corner of the bubble in workspace units.
- const bubbleMin = {
- x: this.workspace_.RTL ? (this.anchorXY_.x - relativeMin.x - this.width_) :
- (relativeMin.x + this.anchorXY_.x),
- y: relativeMin.y + this.anchorXY_.y,
- };
- // The position of the bottom-right corner of the bubble in workspace units.
- const bubbleMax = {
- x: bubbleMin.x + this.width_,
- y: bubbleMin.y + this.height_,
- };
-
- // We could adjust these values to account for the scrollbars, but the
- // bubbles should have been adjusted to not collide with them anyway, so
- // giving the workspace a slightly larger "bounding box" shouldn't affect the
- // calculation.
-
- // The position of the top-left corner of the workspace.
- const workspaceMin = {x: viewMetrics.left, y: viewMetrics.top};
- // The position of the bottom-right corner of the workspace.
- const workspaceMax = {
- x: viewMetrics.left + viewMetrics.width,
- y: viewMetrics.top + viewMetrics.height,
- };
-
- const overlapWidth = Math.min(bubbleMax.x, workspaceMax.x) -
- Math.max(bubbleMin.x, workspaceMin.x);
- const overlapHeight = Math.min(bubbleMax.y, workspaceMax.y) -
- Math.max(bubbleMin.y, workspaceMin.y);
- return Math.max(
- 0,
- Math.min(
- 1, (overlapWidth * overlapHeight) / (this.width_ * this.height_)));
-};
-
-/**
- * Calculate what the optimal horizontal position of the top-left corner of the
- * bubble is (relative to the anchor point) so that the most area of the
- * bubble is shown.
- * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
- * of the workspace the bubble will appear in.
- * @return {number} The optimal horizontal position of the top-left corner
- * of the bubble.
- * @private
- */
-Bubble.prototype.getOptimalRelativeLeft_ = function(viewMetrics) {
- let relativeLeft = -this.width_ / 4;
-
- // No amount of sliding left or right will give us a better overlap.
- if (this.width_ > viewMetrics.width) {
- return relativeLeft;
- }
-
- if (this.workspace_.RTL) {
- // Bubble coordinates are flipped in RTL.
- const bubbleRight = this.anchorXY_.x - relativeLeft;
- const bubbleLeft = bubbleRight - this.width_;
-
- const workspaceRight = viewMetrics.left + viewMetrics.width;
- const workspaceLeft = viewMetrics.left +
- // Thickness in workspace units.
- (Scrollbar.scrollbarThickness / this.workspace_.scale);
-
- if (bubbleLeft < workspaceLeft) {
- // Slide the bubble right until it is onscreen.
- relativeLeft = -(workspaceLeft - this.anchorXY_.x + this.width_);
- } else if (bubbleRight > workspaceRight) {
- // Slide the bubble left until it is onscreen.
- relativeLeft = -(workspaceRight - this.anchorXY_.x);
- }
- } else {
- const bubbleLeft = relativeLeft + this.anchorXY_.x;
- const bubbleRight = bubbleLeft + this.width_;
-
- const workspaceLeft = viewMetrics.left;
- const workspaceRight = viewMetrics.left + viewMetrics.width -
- // Thickness in workspace units.
- (Scrollbar.scrollbarThickness / this.workspace_.scale);
-
- if (bubbleLeft < workspaceLeft) {
- // Slide the bubble right until it is onscreen.
- relativeLeft = workspaceLeft - this.anchorXY_.x;
- } else if (bubbleRight > workspaceRight) {
- // Slide the bubble left until it is onscreen.
- relativeLeft = workspaceRight - this.anchorXY_.x - this.width_;
- }
- }
-
- return relativeLeft;
-};
-
-/**
- * Calculate what the optimal vertical position of the top-left corner of
- * the bubble is (relative to the anchor point) so that the most area of the
- * bubble is shown.
- * @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
- * of the workspace the bubble will appear in.
- * @return {number} The optimal vertical position of the top-left corner
- * of the bubble.
- * @private
- */
-Bubble.prototype.getOptimalRelativeTop_ = function(viewMetrics) {
- let relativeTop = -this.height_ / 4;
-
- // No amount of sliding up or down will give us a better overlap.
- if (this.height_ > viewMetrics.height) {
- return relativeTop;
- }
-
- const bubbleTop = this.anchorXY_.y + relativeTop;
- const bubbleBottom = bubbleTop + this.height_;
- const workspaceTop = viewMetrics.top;
- const workspaceBottom = viewMetrics.top + viewMetrics.height -
- // Thickness in workspace units.
- (Scrollbar.scrollbarThickness / this.workspace_.scale);
-
- const anchorY = this.anchorXY_.y;
- if (bubbleTop < workspaceTop) {
- // Slide the bubble down until it is onscreen.
- relativeTop = workspaceTop - anchorY;
- } else if (bubbleBottom > workspaceBottom) {
- // Slide the bubble up until it is onscreen.
- relativeTop = workspaceBottom - anchorY - this.height_;
- }
-
- return relativeTop;
-};
-
-/**
- * Move the bubble to a location relative to the anchor's centre.
- * @private
- */
-Bubble.prototype.positionBubble_ = function() {
- let left = this.anchorXY_.x;
- if (this.workspace_.RTL) {
- left -= this.relativeLeft_ + this.width_;
- } else {
- left += this.relativeLeft_;
- }
- const top = this.relativeTop_ + this.anchorXY_.y;
- this.moveTo(left, top);
-};
-
-/**
- * Move the bubble group to the specified location in workspace coordinates.
- * @param {number} x The x position to move to.
- * @param {number} y The y position to move to.
- * @package
- */
-Bubble.prototype.moveTo = function(x, y) {
- this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
-};
-
-/**
- * Triggers a move callback if one exists at the end of a drag.
- * @param {boolean} adding True if adding, false if removing.
- * @package
- */
-Bubble.prototype.setDragging = function(adding) {
- if (!adding && this.moveCallback_) {
- this.moveCallback_();
- }
-};
-
-/**
- * Get the dimensions of this bubble.
- * @return {!Size} The height and width of the bubble.
- */
-Bubble.prototype.getBubbleSize = function() {
- return new Size(this.width_, this.height_);
-};
-
-/**
- * Size this bubble.
- * @param {number} width Width of the bubble.
- * @param {number} height Height of the bubble.
- */
-Bubble.prototype.setBubbleSize = function(width, height) {
- const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
- // Minimum size of a bubble.
- width = Math.max(width, doubleBorderWidth + 45);
- height = Math.max(height, doubleBorderWidth + 20);
- this.width_ = width;
- this.height_ = height;
- this.bubbleBack_.setAttribute('width', width);
- this.bubbleBack_.setAttribute('height', height);
- if (this.resizeGroup_) {
- if (this.workspace_.RTL) {
- // Mirror the resize group.
- const resizeSize = 2 * Bubble.BORDER_WIDTH;
- this.resizeGroup_.setAttribute(
- 'transform',
- 'translate(' + resizeSize + ',' + (height - doubleBorderWidth) +
- ') scale(-1 1)');
- } else {
- this.resizeGroup_.setAttribute(
- 'transform',
- 'translate(' + (width - doubleBorderWidth) + ',' +
- (height - doubleBorderWidth) + ')');
- }
- }
- if (this.autoLayout_) {
- this.layoutBubble_();
- }
- this.positionBubble_();
- this.renderArrow_();
-
- // Allow the contents to resize.
- if (this.resizeCallback_) {
- this.resizeCallback_();
- }
-};
-
-/**
- * Draw the arrow between the bubble and the origin.
- * @private
- */
-Bubble.prototype.renderArrow_ = function() {
- const steps = [];
- // Find the relative coordinates of the center of the bubble.
- const relBubbleX = this.width_ / 2;
- const relBubbleY = this.height_ / 2;
- // Find the relative coordinates of the center of the anchor.
- let relAnchorX = -this.relativeLeft_;
- let relAnchorY = -this.relativeTop_;
- if (relBubbleX === relAnchorX && relBubbleY === relAnchorY) {
- // Null case. Bubble is directly on top of the anchor.
- // Short circuit this rather than wade through divide by zeros.
- steps.push('M ' + relBubbleX + ',' + relBubbleY);
- } else {
- // Compute the angle of the arrow's line.
- const rise = relAnchorY - relBubbleY;
- let run = relAnchorX - relBubbleX;
- if (this.workspace_.RTL) {
- run *= -1;
- }
- const hypotenuse = Math.sqrt(rise * rise + run * run);
- let angle = Math.acos(run / hypotenuse);
- if (rise < 0) {
- angle = 2 * Math.PI - angle;
- }
- // Compute a line perpendicular to the arrow.
- let rightAngle = angle + Math.PI / 2;
- if (rightAngle > Math.PI * 2) {
- rightAngle -= Math.PI * 2;
- }
- const rightRise = Math.sin(rightAngle);
- const rightRun = Math.cos(rightAngle);
-
- // Calculate the thickness of the base of the arrow.
- const bubbleSize = this.getBubbleSize();
- let thickness =
- (bubbleSize.width + bubbleSize.height) / Bubble.ARROW_THICKNESS;
- thickness = Math.min(thickness, bubbleSize.width, bubbleSize.height) / 4;
-
- // Back the tip of the arrow off of the anchor.
- const backoffRatio = 1 - Bubble.ANCHOR_RADIUS / hypotenuse;
- relAnchorX = relBubbleX + backoffRatio * run;
- relAnchorY = relBubbleY + backoffRatio * rise;
-
- // Coordinates for the base of the arrow.
- const baseX1 = relBubbleX + thickness * rightRun;
- const baseY1 = relBubbleY + thickness * rightRise;
- const baseX2 = relBubbleX - thickness * rightRun;
- const baseY2 = relBubbleY - thickness * rightRise;
-
- // Distortion to curve the arrow.
- let swirlAngle = angle + this.arrow_radians_;
- if (swirlAngle > Math.PI * 2) {
- swirlAngle -= Math.PI * 2;
- }
- const swirlRise = Math.sin(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
- const swirlRun = Math.cos(swirlAngle) * hypotenuse / Bubble.ARROW_BEND;
-
- steps.push('M' + baseX1 + ',' + baseY1);
- steps.push(
- 'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' +
- relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY);
- steps.push(
- 'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) + ',' +
- (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2);
- }
- steps.push('z');
- this.bubbleArrow_.setAttribute('d', steps.join(' '));
-};
-
-/**
- * Change the colour of a bubble.
- * @param {string} hexColour Hex code of colour.
- */
-Bubble.prototype.setColour = function(hexColour) {
- this.bubbleBack_.setAttribute('fill', hexColour);
- this.bubbleArrow_.setAttribute('fill', hexColour);
-};
-
-/**
- * Dispose of this bubble.
- */
-Bubble.prototype.dispose = function() {
- if (this.onMouseDownBubbleWrapper_) {
- browserEvents.unbind(this.onMouseDownBubbleWrapper_);
- }
- if (this.onMouseDownResizeWrapper_) {
- browserEvents.unbind(this.onMouseDownResizeWrapper_);
- }
- Bubble.unbindDragEvents_();
- dom.removeNode(this.bubbleGroup_);
- this.disposed = true;
-};
-
-/**
- * Move this bubble during a drag, taking into account whether or not there is
- * a drag surface.
- * @param {BlockDragSurfaceSvg} dragSurface The surface that carries
- * rendered items during a drag, or null if no drag surface is in use.
- * @param {!Coordinate} newLoc The location to translate to, in
- * workspace coordinates.
- * @package
- */
-Bubble.prototype.moveDuringDrag = function(dragSurface, newLoc) {
- if (dragSurface) {
- dragSurface.translateSurface(newLoc.x, newLoc.y);
- } else {
- this.moveTo(newLoc.x, newLoc.y);
- }
- if (this.workspace_.RTL) {
- this.relativeLeft_ = this.anchorXY_.x - newLoc.x - this.width_;
- } else {
- this.relativeLeft_ = newLoc.x - this.anchorXY_.x;
- }
- this.relativeTop_ = newLoc.y - this.anchorXY_.y;
- this.renderArrow_();
-};
-
-/**
- * Return the coordinates of the top-left corner of this bubble's body relative
- * to the drawing surface's origin (0,0), in workspace units.
- * @return {!Coordinate} Object with .x and .y properties.
- */
-Bubble.prototype.getRelativeToSurfaceXY = function() {
- return new Coordinate(
- this.workspace_.RTL ?
- -this.relativeLeft_ + this.anchorXY_.x - this.width_ :
- this.anchorXY_.x + this.relativeLeft_,
- this.anchorXY_.y + this.relativeTop_);
-};
-
-/**
- * Set whether auto-layout of this bubble is enabled. The first time a bubble
- * is shown it positions itself to not cover any blocks. Once a user has
- * dragged it to reposition, it renders where the user put it.
- * @param {boolean} enable True if auto-layout should be enabled, false
- * otherwise.
- * @package
- */
-Bubble.prototype.setAutoLayout = function(enable) {
- this.autoLayout_ = enable;
-};
-
-/**
- * Create the text for a non editable bubble.
- * @param {string} text The text to display.
- * @return {!SVGTextElement} The top-level node of the text.
- * @package
- */
-Bubble.textToDom = function(text) {
- const paragraph = dom.createSvgElement(
- Svg.TEXT, {
- 'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
- 'y': Bubble.BORDER_WIDTH,
- },
- null);
- const lines = text.split('\n');
- for (let i = 0; i < lines.length; i++) {
- const tspanElement = dom.createSvgElement(
- Svg.TSPAN, {'dy': '1em', 'x': Bubble.BORDER_WIDTH}, paragraph);
- const textNode = document.createTextNode(lines[i]);
- tspanElement.appendChild(textNode);
- }
- return paragraph;
-};
-
-/**
- * Creates a bubble that can not be edited.
- * @param {!SVGTextElement} paragraphElement The text element for the non
- * editable bubble.
- * @param {!BlockSvg} block The block that the bubble is attached to.
- * @param {!Coordinate} iconXY The coordinate of the icon.
- * @return {!Bubble} The non editable bubble.
- * @package
- */
-Bubble.createNonEditableBubble = function(paragraphElement, block, iconXY) {
- const bubble = new Bubble(
- /** @type {!WorkspaceSvg} */ (block.workspace), paragraphElement,
- block.pathObject.svgPath,
- /** @type {!Coordinate} */ (iconXY), null, null);
- // Expose this bubble's block's ID on its top-level SVG group.
- bubble.setSvgId(block.id);
- if (block.RTL) {
- // Right-align the paragraph.
- // This cannot be done until the bubble is rendered on screen.
- const maxWidth = paragraphElement.getBBox().width;
- for (let i = 0, textElement; (textElement = paragraphElement.childNodes[i]);
- i++) {
- textElement.setAttribute('text-anchor', 'end');
- textElement.setAttribute('x', maxWidth + Bubble.BORDER_WIDTH);
- }
- }
- return bubble;
-};
-
exports.Bubble = Bubble;
diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js
index e312ecdc2..7a716d3db 100644
--- a/core/bubble_dragger.js
+++ b/core/bubble_dragger.js
@@ -43,249 +43,253 @@ goog.require('Blockly.constants');
* Class for a bubble dragger. It moves things on the bubble canvas around the
* workspace when they are being dragged by a mouse or touch. These can be
* block comments, mutators, warnings, or workspace comments.
- * @param {!IBubble} bubble The item on the bubble canvas to drag.
- * @param {!WorkspaceSvg} workspace The workspace to drag on.
- * @constructor
- * @alias Blockly.BubbleDragger
*/
-const BubbleDragger = function(bubble, workspace) {
+const BubbleDragger = class {
/**
- * The item on the bubble canvas that is being dragged.
- * @type {!IBubble}
- * @private
+ * @param {!IBubble} bubble The item on the bubble canvas to drag.
+ * @param {!WorkspaceSvg} workspace The workspace to drag on.
+ * @alias Blockly.BubbleDragger
*/
- this.draggingBubble_ = bubble;
+ constructor(bubble, workspace) {
+ /**
+ * The item on the bubble canvas that is being dragged.
+ * @type {!IBubble}
+ * @private
+ */
+ this.draggingBubble_ = bubble;
- /**
- * The workspace on which the bubble is being dragged.
- * @type {!WorkspaceSvg}
- * @private
- */
- this.workspace_ = workspace;
+ /**
+ * The workspace on which the bubble is being dragged.
+ * @type {!WorkspaceSvg}
+ * @private
+ */
+ this.workspace_ = workspace;
- /**
- * Which drag target the mouse pointer is over, if any.
- * @type {?IDragTarget}
- * @private
- */
- this.dragTarget_ = null;
+ /**
+ * Which drag target the mouse pointer is over, if any.
+ * @type {?IDragTarget}
+ * @private
+ */
+ this.dragTarget_ = null;
- /**
- * Whether the bubble would be deleted if dropped immediately.
- * @type {boolean}
- * @private
- */
- this.wouldDeleteBubble_ = false;
+ /**
+ * Whether the bubble would be deleted if dropped immediately.
+ * @type {boolean}
+ * @private
+ */
+ this.wouldDeleteBubble_ = false;
- /**
- * The location of the top left corner of the dragging bubble's body at the
- * beginning of the drag, in workspace coordinates.
- * @type {!Coordinate}
- * @private
- */
- this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY();
+ /**
+ * The location of the top left corner of the dragging bubble's body at the
+ * beginning of the drag, in workspace coordinates.
+ * @type {!Coordinate}
+ * @private
+ */
+ this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY();
- /**
- * The drag surface to move bubbles to during a drag, or null if none should
- * be used. Block dragging and bubble dragging use the same surface.
- * @type {BlockDragSurfaceSvg}
- * @private
- */
- this.dragSurface_ =
- svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
- workspace.getBlockDragSurface() :
- null;
-};
-
-/**
- * Sever all links from this object.
- * @package
- * @suppress {checkTypes}
- */
-BubbleDragger.prototype.dispose = function() {
- this.draggingBubble_ = null;
- this.workspace_ = null;
- this.dragSurface_ = null;
-};
-
-/**
- * Start dragging a bubble. This includes moving it to the drag surface.
- * @package
- */
-BubbleDragger.prototype.startBubbleDrag = function() {
- if (!eventUtils.getGroup()) {
- eventUtils.setGroup(true);
+ /**
+ * The drag surface to move bubbles to during a drag, or null if none should
+ * be used. Block dragging and bubble dragging use the same surface.
+ * @type {BlockDragSurfaceSvg}
+ * @private
+ */
+ this.dragSurface_ =
+ svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
+ workspace.getBlockDragSurface() :
+ null;
}
- this.workspace_.setResizesEnabled(false);
- this.draggingBubble_.setAutoLayout(false);
- if (this.dragSurface_) {
- this.moveToDragSurface_();
+ /**
+ * Sever all links from this object.
+ * @package
+ * @suppress {checkTypes}
+ */
+ dispose() {
+ this.draggingBubble_ = null;
+ this.workspace_ = null;
+ this.dragSurface_ = null;
}
- this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
-};
-
-/**
- * Execute a step of bubble dragging, based on the given event. Update the
- * display accordingly.
- * @param {!Event} e The most recent move event.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at the start of the drag, in pixel units.
- * @package
- */
-BubbleDragger.prototype.dragBubble = function(e, currentDragDeltaXY) {
- const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
- const newLoc = Coordinate.sum(this.startXY_, delta);
- this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
-
- const oldDragTarget = this.dragTarget_;
- this.dragTarget_ = this.workspace_.getDragTarget(e);
-
- const oldWouldDeleteBubble = this.wouldDeleteBubble_;
- this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
- if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) {
- // Prevent unnecessary add/remove class calls.
- this.updateCursorDuringBubbleDrag_();
- }
-
- // Call drag enter/exit/over after wouldDeleteBlock is called in shouldDelete_
- if (this.dragTarget_ !== oldDragTarget) {
- oldDragTarget && oldDragTarget.onDragExit(this.draggingBubble_);
- this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBubble_);
- }
- this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBubble_);
-};
-
-/**
- * Whether ending the drag would delete the bubble.
- * @param {?IDragTarget} dragTarget The drag target that the bubblee is
- * currently over.
- * @return {boolean} Whether dropping the bubble immediately would delete the
- * block.
- * @private
- */
-BubbleDragger.prototype.shouldDelete_ = function(dragTarget) {
- if (dragTarget) {
- const componentManager = this.workspace_.getComponentManager();
- const isDeleteArea = componentManager.hasCapability(
- dragTarget.id, ComponentManager.Capability.DELETE_AREA);
- if (isDeleteArea) {
- return (/** @type {!IDeleteArea} */ (dragTarget))
- .wouldDelete(this.draggingBubble_, false);
+ /**
+ * Start dragging a bubble. This includes moving it to the drag surface.
+ * @package
+ */
+ startBubbleDrag() {
+ if (!eventUtils.getGroup()) {
+ eventUtils.setGroup(true);
}
- }
- return false;
-};
-/**
- * Update the cursor (and possibly the trash can lid) to reflect whether the
- * dragging bubble would be deleted if released immediately.
- * @private
- */
-BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() {
- this.draggingBubble_.setDeleteStyle(this.wouldDeleteBubble_);
-};
-
-/**
- * Finish a bubble drag and put the bubble back on the workspace.
- * @param {!Event} e The mouseup/touchend event.
- * @param {!Coordinate} currentDragDeltaXY How far the pointer has
- * moved from the position at the start of the drag, in pixel units.
- * @package
- */
-BubbleDragger.prototype.endBubbleDrag = function(e, currentDragDeltaXY) {
- // Make sure internal state is fresh.
- this.dragBubble(e, currentDragDeltaXY);
-
- const preventMove = this.dragTarget_ &&
- this.dragTarget_.shouldPreventMove(this.draggingBubble_);
- let newLoc;
- if (preventMove) {
- newLoc = this.startXY_;
- } else {
- const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
- newLoc = Coordinate.sum(this.startXY_, delta);
- }
- // Move the bubble to its final location.
- this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
-
- if (this.dragTarget_) {
- this.dragTarget_.onDrop(this.draggingBubble_);
- }
-
- if (this.wouldDeleteBubble_) {
- // Fire a move event, so we know where to go back to for an undo.
- this.fireMoveEvent_();
- this.draggingBubble_.dispose(false, true);
- } else {
- // Put everything back onto the bubble canvas.
+ this.workspace_.setResizesEnabled(false);
+ this.draggingBubble_.setAutoLayout(false);
if (this.dragSurface_) {
- this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
+ this.moveToDragSurface_();
}
- if (this.draggingBubble_.setDragging) {
- this.draggingBubble_.setDragging(false);
+
+ this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
+ }
+
+ /**
+ * Execute a step of bubble dragging, based on the given event. Update the
+ * display accordingly.
+ * @param {!Event} e The most recent move event.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at the start of the drag, in pixel units.
+ * @package
+ */
+ dragBubble(e, currentDragDeltaXY) {
+ const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
+ const newLoc = Coordinate.sum(this.startXY_, delta);
+ this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
+
+ const oldDragTarget = this.dragTarget_;
+ this.dragTarget_ = this.workspace_.getDragTarget(e);
+
+ const oldWouldDeleteBubble = this.wouldDeleteBubble_;
+ this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
+ if (oldWouldDeleteBubble !== this.wouldDeleteBubble_) {
+ // Prevent unnecessary add/remove class calls.
+ this.updateCursorDuringBubbleDrag_();
}
- this.fireMoveEvent_();
+
+ // Call drag enter/exit/over after wouldDeleteBlock is called in
+ // shouldDelete_
+ if (this.dragTarget_ !== oldDragTarget) {
+ oldDragTarget && oldDragTarget.onDragExit(this.draggingBubble_);
+ this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBubble_);
+ }
+ this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBubble_);
}
- this.workspace_.setResizesEnabled(true);
- eventUtils.setGroup(false);
-};
-
-/**
- * Fire a move event at the end of a bubble drag.
- * @private
- */
-BubbleDragger.prototype.fireMoveEvent_ = function() {
- if (this.draggingBubble_.isComment) {
- // TODO (adodson): Resolve build errors when requiring WorkspaceCommentSvg.
- const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
- /** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_));
- event.setOldCoordinate(this.startXY_);
- event.recordNew();
- eventUtils.fire(event);
+ /**
+ * Whether ending the drag would delete the bubble.
+ * @param {?IDragTarget} dragTarget The drag target that the bubblee is
+ * currently over.
+ * @return {boolean} Whether dropping the bubble immediately would delete the
+ * block.
+ * @private
+ */
+ shouldDelete_(dragTarget) {
+ if (dragTarget) {
+ const componentManager = this.workspace_.getComponentManager();
+ const isDeleteArea = componentManager.hasCapability(
+ dragTarget.id, ComponentManager.Capability.DELETE_AREA);
+ if (isDeleteArea) {
+ return (/** @type {!IDeleteArea} */ (dragTarget))
+ .wouldDelete(this.draggingBubble_, false);
+ }
+ }
+ return false;
}
- // TODO (fenichel): move events for comments.
- return;
-};
-/**
- * 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 {!Coordinate} pixelCoord A coordinate with x and y
- * values in CSS pixel units.
- * @return {!Coordinate} The input coordinate divided by the
- * workspace scale.
- * @private
- */
-BubbleDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) {
- const result = new 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.
- const mainScale = this.workspace_.options.parentWorkspace.scale;
- result.scale(1 / mainScale);
+ /**
+ * Update the cursor (and possibly the trash can lid) to reflect whether the
+ * dragging bubble would be deleted if released immediately.
+ * @private
+ */
+ updateCursorDuringBubbleDrag_() {
+ this.draggingBubble_.setDeleteStyle(this.wouldDeleteBubble_);
}
- return result;
-};
-/**
- * Move the bubble onto the drag surface at the beginning of a drag. Move the
- * drag surface to preserve the apparent location of the bubble.
- * @private
- */
-BubbleDragger.prototype.moveToDragSurface_ = function() {
- this.draggingBubble_.moveTo(0, 0);
- this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
- // Execute the move on the top-level SVG component.
- this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot());
+ /**
+ * Finish a bubble drag and put the bubble back on the workspace.
+ * @param {!Event} e The mouseup/touchend event.
+ * @param {!Coordinate} currentDragDeltaXY How far the pointer has
+ * moved from the position at the start of the drag, in pixel units.
+ * @package
+ */
+ endBubbleDrag(e, currentDragDeltaXY) {
+ // Make sure internal state is fresh.
+ this.dragBubble(e, currentDragDeltaXY);
+
+ const preventMove = this.dragTarget_ &&
+ this.dragTarget_.shouldPreventMove(this.draggingBubble_);
+ let newLoc;
+ if (preventMove) {
+ newLoc = this.startXY_;
+ } else {
+ const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
+ newLoc = Coordinate.sum(this.startXY_, delta);
+ }
+ // Move the bubble to its final location.
+ this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
+
+ if (this.dragTarget_) {
+ this.dragTarget_.onDrop(this.draggingBubble_);
+ }
+
+ if (this.wouldDeleteBubble_) {
+ // Fire a move event, so we know where to go back to for an undo.
+ this.fireMoveEvent_();
+ this.draggingBubble_.dispose(false, true);
+ } else {
+ // Put everything back onto the bubble canvas.
+ if (this.dragSurface_) {
+ this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
+ }
+ if (this.draggingBubble_.setDragging) {
+ this.draggingBubble_.setDragging(false);
+ }
+ this.fireMoveEvent_();
+ }
+ this.workspace_.setResizesEnabled(true);
+
+ eventUtils.setGroup(false);
+ }
+
+ /**
+ * Fire a move event at the end of a bubble drag.
+ * @private
+ */
+ fireMoveEvent_() {
+ if (this.draggingBubble_.isComment) {
+ // TODO (adodson): Resolve build errors when requiring
+ // WorkspaceCommentSvg.
+ const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(
+ /** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_));
+ event.setOldCoordinate(this.startXY_);
+ event.recordNew();
+ eventUtils.fire(event);
+ }
+ // TODO (fenichel): move events for comments.
+ return;
+ }
+
+ /**
+ * 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 {!Coordinate} pixelCoord A coordinate with x and y
+ * values in CSS pixel units.
+ * @return {!Coordinate} The input coordinate divided by the
+ * workspace scale.
+ * @private
+ */
+ pixelsToWorkspaceUnits_(pixelCoord) {
+ const result = new 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.
+ const mainScale = this.workspace_.options.parentWorkspace.scale;
+ result.scale(1 / mainScale);
+ }
+ return result;
+ }
+
+ /**
+ * Move the bubble onto the drag surface at the beginning of a drag. Move the
+ * drag surface to preserve the apparent location of the bubble.
+ * @private
+ */
+ moveToDragSurface_() {
+ this.draggingBubble_.moveTo(0, 0);
+ this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
+ // Execute the move on the top-level SVG component.
+ this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot());
+ }
};
exports.BubbleDragger = BubbleDragger;
diff --git a/core/utils/coordinate.js b/core/utils/coordinate.js
index 1a8dece05..8f3cffe1f 100644
--- a/core/utils/coordinate.js
+++ b/core/utils/coordinate.js
@@ -21,116 +21,118 @@ goog.module('Blockly.utils.Coordinate');
/**
* Class for representing coordinates and positions.
- * @param {number} x Left.
- * @param {number} y Top.
- * @struct
- * @constructor
- * @alias Blockly.utils.Coordinate
*/
-const Coordinate = function(x, y) {
+const Coordinate = class {
/**
- * X-value
- * @type {number}
+ * @param {number} x Left.
+ * @param {number} y Top.
+ * @alias Blockly.utils.Coordinate
*/
- this.x = x;
+ constructor(x, y) {
+ /**
+ * X-value
+ * @type {number}
+ */
+ this.x = x;
+
+ /**
+ * Y-value
+ * @type {number}
+ */
+ this.y = y;
+ }
/**
- * Y-value
- * @type {number}
+ * Creates a new copy of this coordinate.
+ * @return {!Coordinate} A copy of this coordinate.
*/
- this.y = y;
-};
-
-/**
- * Compares coordinates for equality.
- * @param {?Coordinate} a A Coordinate.
- * @param {?Coordinate} b A Coordinate.
- * @return {boolean} True iff the coordinates are equal, or if both are null.
- */
-Coordinate.equals = function(a, b) {
- if (a === b) {
- return true;
+ clone() {
+ return new Coordinate(this.x, this.y);
}
- if (!a || !b) {
- return false;
+
+ /**
+ * Scales this coordinate by the given scale factor.
+ * @param {number} s The scale factor to use for both x and y dimensions.
+ * @return {!Coordinate} This coordinate after scaling.
+ */
+ scale(s) {
+ this.x *= s;
+ this.y *= s;
+ return this;
}
- return a.x === b.x && a.y === b.y;
-};
-/**
- * Returns the distance between two coordinates.
- * @param {!Coordinate} a A Coordinate.
- * @param {!Coordinate} b A Coordinate.
- * @return {number} The distance between `a` and `b`.
- */
-Coordinate.distance = function(a, b) {
- const dx = a.x - b.x;
- const dy = a.y - b.y;
- return Math.sqrt(dx * dx + dy * dy);
-};
+ /**
+ * Translates this coordinate by the given offsets.
+ * respectively.
+ * @param {number} tx The value to translate x by.
+ * @param {number} ty The value to translate y by.
+ * @return {!Coordinate} This coordinate after translating.
+ */
+ translate(tx, ty) {
+ this.x += tx;
+ this.y += ty;
+ return this;
+ }
-/**
- * Returns the magnitude of a coordinate.
- * @param {!Coordinate} a A Coordinate.
- * @return {number} The distance between the origin and `a`.
- */
-Coordinate.magnitude = function(a) {
- return Math.sqrt(a.x * a.x + a.y * a.y);
-};
+ /**
+ * Compares coordinates for equality.
+ * @param {?Coordinate} a A Coordinate.
+ * @param {?Coordinate} b A Coordinate.
+ * @return {boolean} True iff the coordinates are equal, or if both are null.
+ */
+ static equals(a, b) {
+ if (a === b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ return a.x === b.x && a.y === b.y;
+ }
-/**
- * Returns the difference between two coordinates as a new
- * Coordinate.
- * @param {!Coordinate|!SVGPoint} a An x/y coordinate.
- * @param {!Coordinate|!SVGPoint} b An x/y coordinate.
- * @return {!Coordinate} A Coordinate representing the difference
- * between `a` and `b`.
- */
-Coordinate.difference = function(a, b) {
- return new Coordinate(a.x - b.x, a.y - b.y);
-};
+ /**
+ * Returns the distance between two coordinates.
+ * @param {!Coordinate} a A Coordinate.
+ * @param {!Coordinate} b A Coordinate.
+ * @return {number} The distance between `a` and `b`.
+ */
+ static distance(a, b) {
+ const dx = a.x - b.x;
+ const dy = a.y - b.y;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
-/**
- * Returns the sum of two coordinates as a new Coordinate.
- * @param {!Coordinate|!SVGPoint} a An x/y coordinate.
- * @param {!Coordinate|!SVGPoint} b An x/y coordinate.
- * @return {!Coordinate} A Coordinate representing the sum of
- * the two coordinates.
- */
-Coordinate.sum = function(a, b) {
- return new Coordinate(a.x + b.x, a.y + b.y);
-};
+ /**
+ * Returns the magnitude of a coordinate.
+ * @param {!Coordinate} a A Coordinate.
+ * @return {number} The distance between the origin and `a`.
+ */
+ static magnitude(a) {
+ return Math.sqrt(a.x * a.x + a.y * a.y);
+ }
-/**
- * Creates a new copy of this coordinate.
- * @return {!Coordinate} A copy of this coordinate.
- */
-Coordinate.prototype.clone = function() {
- return new Coordinate(this.x, this.y);
-};
+ /**
+ * Returns the difference between two coordinates as a new
+ * Coordinate.
+ * @param {!Coordinate|!SVGPoint} a An x/y coordinate.
+ * @param {!Coordinate|!SVGPoint} b An x/y coordinate.
+ * @return {!Coordinate} A Coordinate representing the difference
+ * between `a` and `b`.
+ */
+ static difference(a, b) {
+ return new Coordinate(a.x - b.x, a.y - b.y);
+ }
-/**
- * Scales this coordinate by the given scale factor.
- * @param {number} s The scale factor to use for both x and y dimensions.
- * @return {!Coordinate} This coordinate after scaling.
- */
-Coordinate.prototype.scale = function(s) {
- this.x *= s;
- this.y *= s;
- return this;
-};
-
-/**
- * Translates this coordinate by the given offsets.
- * respectively.
- * @param {number} tx The value to translate x by.
- * @param {number} ty The value to translate y by.
- * @return {!Coordinate} This coordinate after translating.
- */
-Coordinate.prototype.translate = function(tx, ty) {
- this.x += tx;
- this.y += ty;
- return this;
+ /**
+ * Returns the sum of two coordinates as a new Coordinate.
+ * @param {!Coordinate|!SVGPoint} a An x/y coordinate.
+ * @param {!Coordinate|!SVGPoint} b An x/y coordinate.
+ * @return {!Coordinate} A Coordinate representing the sum of
+ * the two coordinates.
+ */
+ static sum(a, b) {
+ return new Coordinate(a.x + b.x, a.y + b.y);
+ }
};
exports.Coordinate = Coordinate;
diff --git a/core/utils/rect.js b/core/utils/rect.js
index 8ecf7d358..1089ce102 100644
--- a/core/utils/rect.js
+++ b/core/utils/rect.js
@@ -22,50 +22,54 @@ goog.module('Blockly.utils.Rect');
/**
* Class for representing rectangular regions.
- * @param {number} top Top.
- * @param {number} bottom Bottom.
- * @param {number} left Left.
- * @param {number} right Right.
- * @struct
- * @constructor
- * @alias Blockly.utils.Rect
*/
-const Rect = function(top, bottom, left, right) {
- /** @type {number} */
- this.top = top;
+const Rect = class {
+ /**
+ * @param {number} top Top.
+ * @param {number} bottom Bottom.
+ * @param {number} left Left.
+ * @param {number} right Right.
+ * @struct
+ * @alias Blockly.utils.Rect
+ */
+ constructor(top, bottom, left, right) {
+ /** @type {number} */
+ this.top = top;
- /** @type {number} */
- this.bottom = bottom;
+ /** @type {number} */
+ this.bottom = bottom;
- /** @type {number} */
- this.left = left;
+ /** @type {number} */
+ this.left = left;
- /** @type {number} */
- this.right = right;
-};
+ /** @type {number} */
+ this.right = right;
+ }
-/**
- * Tests whether this rectangle contains a x/y coordinate.
- *
- * @param {number} x The x coordinate to test for containment.
- * @param {number} y The y coordinate to test for containment.
- * @return {boolean} Whether this rectangle contains given coordinate.
- */
-Rect.prototype.contains = function(x, y) {
- return x >= this.left && x <= this.right && y >= this.top && y <= this.bottom;
-};
+ /**
+ * Tests whether this rectangle contains a x/y coordinate.
+ *
+ * @param {number} x The x coordinate to test for containment.
+ * @param {number} y The y coordinate to test for containment.
+ * @return {boolean} Whether this rectangle contains given coordinate.
+ */
+ contains(x, y) {
+ return x >= this.left && x <= this.right && y >= this.top &&
+ y <= this.bottom;
+ }
-/**
- * Tests whether this rectangle intersects the provided rectangle.
- * Assumes that the coordinate system increases going down and left.
- * @param {!Rect} other The other rectangle to check for
- * intersection with.
- * @return {boolean} Whether this rectangle intersects the provided rectangle.
- */
-Rect.prototype.intersects = function(other) {
- return !(
- this.left > other.right || this.right < other.left ||
- this.top > other.bottom || this.bottom < other.top);
+ /**
+ * Tests whether this rectangle intersects the provided rectangle.
+ * Assumes that the coordinate system increases going down and left.
+ * @param {!Rect} other The other rectangle to check for
+ * intersection with.
+ * @return {boolean} Whether this rectangle intersects the provided rectangle.
+ */
+ intersects(other) {
+ return !(
+ this.left > other.right || this.right < other.left ||
+ this.top > other.bottom || this.bottom < other.top);
+ }
};
exports.Rect = Rect;
diff --git a/core/utils/size.js b/core/utils/size.js
index 13c5e40f4..b59a66283 100644
--- a/core/utils/size.js
+++ b/core/utils/size.js
@@ -22,41 +22,44 @@ goog.module('Blockly.utils.Size');
/**
* Class for representing sizes consisting of a width and height.
- * @param {number} width Width.
- * @param {number} height Height.
- * @struct
- * @constructor
- * @alias Blockly.utils.Size
*/
-const Size = function(width, height) {
+const Size = class {
/**
- * Width
- * @type {number}
+ * @param {number} width Width.
+ * @param {number} height Height.
+ * @struct
+ * @alias Blockly.utils.Size
*/
- this.width = width;
+ constructor(width, height) {
+ /**
+ * Width
+ * @type {number}
+ */
+ this.width = width;
+
+ /**
+ * Height
+ * @type {number}
+ */
+ this.height = height;
+ }
/**
- * Height
- * @type {number}
+ * Compares sizes for equality.
+ * @param {?Size} a A Size.
+ * @param {?Size} b A Size.
+ * @return {boolean} True iff the sizes have equal widths and equal
+ * heights, or if both are null.
*/
- this.height = height;
-};
-
-/**
- * Compares sizes for equality.
- * @param {?Size} a A Size.
- * @param {?Size} b A Size.
- * @return {boolean} True iff the sizes have equal widths and equal
- * heights, or if both are null.
- */
-Size.equals = function(a, b) {
- if (a === b) {
- return true;
+ static equals(a, b) {
+ if (a === b) {
+ return true;
+ }
+ if (!a || !b) {
+ return false;
+ }
+ return a.width === b.width && a.height === b.height;
}
- if (!a || !b) {
- return false;
- }
- return a.width === b.width && a.height === b.height;
};
exports.Size = Size;