From 1671ec4452208f311c4c5eb7c71a8b36449fe171 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 10:31:56 -0800 Subject: [PATCH 01/10] refactor: convert utils/coordinate.js to ES6 class --- core/utils/coordinate.js | 194 ++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 96 deletions(-) diff --git a/core/utils/coordinate.js b/core/utils/coordinate.js index 1a8dece0585..8f3cffe1f3f 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; - } - if (!a || !b) { - return false; + clone() { + return new Coordinate(this.x, this.y); } - 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); -}; + /** + * 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; + } -/** - * 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); -}; + /** + * 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 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); -}; + /** + * 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 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 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); + } -/** - * 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 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); + } -/** - * 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; -}; + /** + * 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); + } -/** - * 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; From 4705994c48fe77b14a5759c60a02ad5e5f7005df Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:11:15 -0800 Subject: [PATCH 02/10] refactor: convert utils/rect.js to ES6 class --- core/utils/rect.js | 79 ++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/core/utils/rect.js b/core/utils/rect.js index 8ecf7d35814..c3ea1b9211a 100644 --- a/core/utils/rect.js +++ b/core/utils/rect.js @@ -22,50 +22,53 @@ 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; From 908cf03fda3aca5cc6a34e562aa364ccd4aaa15f Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:14:15 -0800 Subject: [PATCH 03/10] refactor: convert utils/size.js to ES6 class --- core/utils/size.js | 59 ++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/core/utils/size.js b/core/utils/size.js index 13c5e40f417..b59a66283a6 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; - } - if (!a || !b) { - return false; + static equals(a, b) { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.width === b.width && a.height === b.height; } - return a.width === b.width && a.height === b.height; }; exports.Size = Size; From 3656372fb7e22ee229110bd1426d443eb3c67b9e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:23:11 -0800 Subject: [PATCH 04/10] refactor: convert block_drag_surface.js to ES6 class --- core/block_drag_surface.js | 396 +++++++++++++++++++------------------ 1 file changed, 199 insertions(+), 197 deletions(-) diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js index 7c7a64f171a..aa0bf2a1e9b 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; + + /** + * 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 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(); + } /** - * Containing HTML element; parent of the workspace and the drag surface. - * @type {!Element} - * @private + * Create the drag surface and inject it into the container. */ - this.container_ = 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_); + } /** - * Cached value for the scale of the drag surface. - * Used to set/get the correct translation during and after a drag. - * @type {number} - * @private + * 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. */ - this.scale_ = 1; + 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); + } /** - * 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 + * 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. */ - this.surfaceXY_ = null; + 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 + ')'); + } /** - * 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} + * Translate the drag surface's SVG based on its internal state. * @private */ - this.childSurfaceXY_ = new Coordinate(0, 0); - - this.createDom(); -}; + 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'; - -/** - * Create the drag surface and inject it into the container. - */ -BlockDragSurfaceSvg.prototype.createDom = function() { - if (this.SVG_) { - return; // Already created. + dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)'); } - 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.'); + /** + * 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_(); } - // 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_(); -}; + /** + * 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. - */ -BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() { - const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_)); - return new Coordinate(xy.x / this.scale_, xy.y / this.scale_); -}; + /** + * 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. - */ -BlockDragSurfaceSvg.prototype.getGroup = function() { - return this.dragGroup_; -}; + /** + * 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. - */ -BlockDragSurfaceSvg.prototype.getSvgRoot = function() { - return this.SVG_; -}; + /** + * 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. - */ -BlockDragSurfaceSvg.prototype.getCurrentBlock = function() { - return /** @type {Element} */ (this.dragGroup_.firstChild); -}; + /** + * 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. - */ -BlockDragSurfaceSvg.prototype.getWsTranslation = function() { - // Returning a copy so the coordinate can not be changed outside this class. - return this.childSurfaceXY_.clone(); -}; + /** + * 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. - */ -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); + /** + * 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.SVG_.style.display = 'none'; - if (this.dragGroup_.childNodes.length) { - throw Error('Drag group was not cleared.'); - } - this.surfaceXY_ = null; }; exports.BlockDragSurfaceSvg = BlockDragSurfaceSvg; From 12fa79bce278c2807c1e80b350fc9bfd3562fdfa Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:28:23 -0800 Subject: [PATCH 05/10] refactor: convert block_dragger.js to ES6 class --- core/block_dragger.js | 750 +++++++++++++++++++++--------------------- 1 file changed, 376 insertions(+), 374 deletions(-) diff --git a/core/block_dragger.js b/core/block_dragger.js index 817fa10fe35..beb7efbda72 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -40,439 +40,441 @@ 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} - * @protected + * @param {!BlockSvg} block The block to drag. + * @param {!WorkspaceSvg} workspace The workspace to drag on. + * @alias Blockly.BlockDragger */ - this.draggingBlock_ = block; + 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); + } /** - * The workspace on which the block is being dragged. - * @type {!WorkspaceSvg} - * @protected + * Sever all links from this object. + * @package */ - this.workspace_ = workspace; + dispose() { + this.dragIconData_.length = 0; - /** - * Object that keeps track of connections on dragged blocks. - * @type {!InsertionMarkerManager} - * @protected - */ - this.draggedConnectionManager_ = - new InsertionMarkerManager(this.draggingBlock_); + if (this.draggedConnectionManager_) { + this.draggedConnectionManager_.dispose(); + } + } /** - * Which drag area the mouse pointer is over, if any. - * @type {?IDragTarget} - * @private + * 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 */ - this.dragTarget_ = null; + 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 the block would be deleted if dropped immediately. - * @type {boolean} + * 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.wouldDeleteBlock_ = false; + shouldDisconnect_(healStack) { + return !!( + this.draggingBlock_.getParent() || + (healStack && this.draggingBlock_.nextConnection && + this.draggingBlock_.nextConnection.targetBlock())); + } /** - * The location of the top left corner of the dragging block at the beginning - * of the drag in workspace coordinates. - * @type {!Coordinate} + * 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.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY(); + 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(); + } /** - * 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} + * Fire a UI event at the start of a block drag. * @protected */ - this.dragIconData_ = initIconData(block); -}; - -/** - * Sever all links from this object. - * @package - */ -BlockDragger.prototype.dispose = function() { - this.dragIconData_.length = 0; - - if (this.draggedConnectionManager_) { - this.draggedConnectionManager_.dispose(); + fireDragStartEvent_() { + const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))( + this.draggingBlock_, true, this.draggingBlock_.getDescendants(false)); + eventUtils.fire(event); } -}; -/** - * Make a list of all of the icons (comment, warning, and mutator) that are - * on this block and its descendants. Moving an icon moves the bubble that - * extends from it if that bubble is open. - * @param {!BlockSvg} block The root block that is being dragged. - * @return {!Array} The list of all icons and their locations. - */ -const initIconData = function(block) { - // Build a list of icons that need to be moved and where they started. - const dragIconData = []; - const descendants = block.getDescendants(false); - - for (let i = 0, descendant; (descendant = descendants[i]); i++) { - const icons = descendant.getIcons(); - for (let j = 0; j < icons.length; j++) { - const data = { - // Coordinate with x and y properties (workspace - // coordinates). - location: icons[j].getIconLocation(), - // Blockly.Icon - icon: icons[j], - }; - dragIconData.push(data); + /** + * 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 + */ + 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_(); } - } - 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); + // 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_); } - 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(); -}; + /** + * 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); -/** - * 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); -}; + if (this.dragTarget_) { + this.dragTarget_.onDrop(this.draggingBlock_); + } -/** - * 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_(); - } + 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); - // 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_); + eventUtils.setGroup(false); } - 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; + /** + * 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 + */ + getNewLocationAfterDrag_(currentDragDeltaXY) { + const newValues = {}; + newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); + newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta); + return newValues; } - this.draggingBlock_.moveOffDragSurface(newLoc); - if (this.dragTarget_) { - this.dragTarget_.onDrop(this.draggingBlock_); + /** + * 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 + */ + 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; } - 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); + /** + * 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 + */ + 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 { - // 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.draggingBlock_.render(); } + this.draggingBlock_.scheduleSnapAndBump(); } - 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; + /** + * 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); } - 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(); + /** + * 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); + } + } } - 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(); + /** + * 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); + } - if (toolbox) { - const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : - 'blocklyToolboxGrab'; + /** + * 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_); + } - if (isEnd && typeof toolbox.removeStyle === 'function') { - toolbox.removeStyle(style); - } else if (!isEnd && typeof toolbox.addStyle === 'function') { - toolbox.addStyle(style); + /** + * 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; } -}; - -/** - * 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); + /** + * 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)); + } } - 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 + */ + getInsertionMarkers() { + // No insertion markers with the old style of dragged connection managers. + if (this.draggedConnectionManager_ && + this.draggedConnectionManager_.getInsertionMarkers) { + return this.draggedConnectionManager_.getInsertionMarkers(); + } + return []; } }; /** - * 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 + * Make a list of all of the icons (comment, warning, and mutator) that are + * on this block and its descendants. Moving an icon moves the bubble that + * extends from it if that bubble is open. + * @param {!BlockSvg} block The root block that is being dragged. + * @return {!Array} The list of all icons and their locations. */ -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(); +const initIconData = function(block) { + // Build a list of icons that need to be moved and where they started. + const dragIconData = []; + const descendants = block.getDescendants(false); + + for (let i = 0, descendant; (descendant = descendants[i]); i++) { + const icons = descendant.getIcons(); + for (let j = 0; j < icons.length; j++) { + const data = { + // Coordinate with x and y properties (workspace + // coordinates). + location: icons[j].getIconLocation(), + // Blockly.Icon + icon: icons[j], + }; + dragIconData.push(data); + } } - return []; + return dragIconData; }; registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger); From ba8037f4ff257aeefa07124b90bd189c9de214b6 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:33:51 -0800 Subject: [PATCH 06/10] refactor: convert bubble_dragger.js to ES6 class --- core/bubble_dragger.js | 417 +++++++++++++++++++++-------------------- 1 file changed, 210 insertions(+), 207 deletions(-) diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js index e312ecdc216..df51e9222e1 100644 --- a/core/bubble_dragger.js +++ b/core/bubble_dragger.js @@ -43,249 +43,252 @@ 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 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; + } /** - * 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 + * Sever all links from this object. + * @package + * @suppress {checkTypes} */ - this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY(); + dispose() { + this.draggingBubble_ = null; + this.workspace_ = null; + this.dragSurface_ = null; + } /** - * 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 + * Start dragging a bubble. This includes moving it to the drag surface. + * @package */ - 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; -}; + startBubbleDrag() { + if (!eventUtils.getGroup()) { + eventUtils.setGroup(true); + } -/** - * Start dragging a bubble. This includes moving it to the drag surface. - * @package - */ -BubbleDragger.prototype.startBubbleDrag = function() { - if (!eventUtils.getGroup()) { - eventUtils.setGroup(true); - } + this.workspace_.setResizesEnabled(false); + this.draggingBubble_.setAutoLayout(false); + if (this.dragSurface_) { + this.moveToDragSurface_(); + } - this.workspace_.setResizesEnabled(false); - this.draggingBubble_.setAutoLayout(false); - if (this.dragSurface_) { - this.moveToDragSurface_(); + this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true); } - 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); -/** - * 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 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_(); + } - 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_); } - // 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_); + /** + * 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; } - 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); - } + /** + * 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 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 + */ + endBubbleDrag(e, currentDragDeltaXY) { + // Make sure internal state is fresh. + this.dragBubble(e, currentDragDeltaXY); -/** - * 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); - 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.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); } - 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); + /** + * 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); } - this.fireMoveEvent_(); + // TODO (fenichel): move events for comments. + return; } - 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); + /** + * 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; } - // 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); + /** + * 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()); } - 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()); }; exports.BubbleDragger = BubbleDragger; From 0178de011a7a4fbfa621535e290eb238a2c92112 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 11:46:57 -0800 Subject: [PATCH 07/10] chore: declare bubble property in the constructor --- core/bubble.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/bubble.js b/core/bubble.js index 78799c0ece0..b1cffaa3ab1 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -65,6 +65,13 @@ const Bubble = function( */ this.rendered_ = false; + /** + * The SVG group containing all parts of the bubble. + * @type {SVGGElement} + * @private + */ + this.bubbleGroup_ = null; + /** * Absolute coordinate of anchor point, in workspace coordinates. * @type {Coordinate} @@ -326,7 +333,7 @@ Bubble.prototype.createDom_ = function(content, hasResize) { * @return {!SVGElement} The root SVG node of the bubble's group. */ Bubble.prototype.getSvgRoot = function() { - return this.bubbleGroup_; + return /** @type {!SVGElement} */ (this.bubbleGroup_); }; /** From c0a1fa40d48ad19458af82098d57d8553617e68e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 12:00:12 -0800 Subject: [PATCH 08/10] refactor: convert bubble.js to ES6 class --- core/bubble.js | 1615 ++++++++++++++++++++++++------------------------ 1 file changed, 820 insertions(+), 795 deletions(-) diff --git a/core/bubble.js b/core/bubble.js index b1cffaa3ab1..60a76d3e601 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -40,915 +40,940 @@ 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 { /** - * Flag to stop incremental rendering during construction. - * @type {boolean} - * @private + * @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 */ - this.rendered_ = false; + 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; + } /** - * The SVG group containing all parts of the bubble. - * @type {SVGGElement} + * 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.bubbleGroup_ = null; + 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} - * @private + * Return the root node of the bubble's SVG group. + * @return {!SVGElement} The root SVG node of the bubble's group. */ - this.anchorXY_ = null; + getSvgRoot() { + return /** @type {!SVGElement} */ (this.bubbleGroup_); + } /** - * 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 + * Expose the block's ID on the bubble's top-level SVG group. + * @param {string} id ID of block. */ - this.relativeLeft_ = 0; + setSvgId(id) { + if (this.bubbleGroup_.dataset) { + this.bubbleGroup_.dataset['blockId'] = id; + } + } /** - * Relative Y coordinate of bubble with respect to the anchor's centre, in - * workspace units. - * @type {number} + * Handle a mouse-down on bubble's border. + * @param {!Event} e Mouse down event. * @private */ - this.relativeTop_ = 0; + bubbleMouseDown_(e) { + const gesture = this.workspace_.getGesture(e); + if (gesture) { + gesture.handleBubbleStart(e, this); + } + } /** - * Width of bubble, in workspace units. - * @type {number} - * @private + * Show the context menu for this bubble. + * @param {!Event} _e Mouse event. + * @package */ - this.width_ = 0; + showContextMenu(_e) { + // NOP on bubbles, but used by the bubble dragger to pass events to + // workspace comments. + } /** - * Height of bubble, in workspace units. - * @type {number} - * @private + * Get whether this bubble is deletable or not. + * @return {boolean} True if deletable. + * @package */ - this.height_ = 0; + isDeletable() { + return false; + } /** - * Automatically position and reposition the bubble. - * @type {boolean} - * @private + * 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. */ - this.autoLayout_ = true; + setDeleteStyle(_enable) { + // NOP if bubble is not deletable. + } /** - * Method to call on resize of bubble. - * @type {?function()} + * Handle a mouse-down on bubble's resize corner. + * @param {!Event} e Mouse down event. * @private */ - this.resizeCallback_ = null; + 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(); + } /** - * Method to call on move of bubble. - * @type {?function()} + * Resize this bubble to follow the mouse. + * @param {!Event} e Mouse move event. * @private */ - this.moveCallback_ = null; + 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_(); + } + } /** - * Mouse down on bubbleBack_ event data. - * @type {?browserEvents.Data} - * @private + * Register a function as a callback event for when the bubble is resized. + * @param {!Function} callback The function to call on resize. */ - this.onMouseDownBubbleWrapper_ = null; + registerResizeEvent(callback) { + this.resizeCallback_ = callback; + } /** - * Mouse down on resizeGroup_ event data. - * @type {?browserEvents.Data} - * @private + * Register a function as a callback event for when the bubble is moved. + * @param {!Function} callback The function to call on move. */ - this.onMouseDownResizeWrapper_ = null; + registerMoveEvent(callback) { + this.moveCallback_ = callback; + } /** - * Describes whether this bubble has been disposed of (nodes and event - * listeners removed from the page) or not. - * @type {boolean} + * Move this bubble to the top of the stack. + * @return {boolean} Whether or not the bubble has been moved. * @package */ - this.disposed = false; - - let angle = Bubble.ARROW_ANGLE; - if (this.workspace_.RTL) { - angle = -angle; + promote() { + const svgGroup = this.bubbleGroup_.parentNode; + if (svgGroup.lastChild !== this.bubbleGroup_) { + svgGroup.appendChild(this.bubbleGroup_); + return true; + } + return false; } - 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; + /** + * 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_(); + } } - this.setBubbleSize(bubbleWidth, bubbleHeight); - // Render the bubble. - this.positionBubble_(); - this.renderArrow_(); - this.rendered_ = true; -}; + /** + * 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; + } -/** - * Width of the border around the bubble. - */ -Bubble.BORDER_WIDTH = 6; + /** + * 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_))); + } -/** - * Determines the thickness of the base of the arrow in relation to the size - * of the bubble. Higher numbers result in thinner arrows. - */ -Bubble.ARROW_THICKNESS = 5; + /** + * 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; -/** - * The number of degrees that the arrow bends counter-clockwise. - */ -Bubble.ARROW_ANGLE = 20; + // No amount of sliding left or right will give us a better overlap. + if (this.width_ > viewMetrics.width) { + return relativeLeft; + } -/** - * The sharpness of the arrow's bend. Higher numbers result in smoother arrows. - */ -Bubble.ARROW_BEND = 4; + 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_; + } + } -/** - * Distance between arrow point and anchor point. - */ -Bubble.ANCHOR_RADIUS = 8; + return relativeLeft; + } -/** - * Mouse up event data. - * @type {?browserEvents.Data} - * @private - */ -Bubble.onMouseUpWrapper_ = null; + /** + * 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; -/** - * Mouse move event data. - * @type {?browserEvents.Data} - * @private - */ -Bubble.onMouseMoveWrapper_ = null; + // No amount of sliding up or down will give us a better overlap. + if (this.height_ > viewMetrics.height) { + return relativeTop; + } -/** - * 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; - } -}; + 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); -/** - * 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_(); -}; + 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_; + } -/** - * 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; + return relativeTop; } - 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_); + /** + * 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); } - 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 /** @type {!SVGElement} */ (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; + /** + * 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 + ')'); } -}; -/** - * 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); + /** + * 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_(); + } } -}; - -/** - * 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_(); + /** + * Get the dimensions of this bubble. + * @return {!Size} The height and width of the bubble. + */ + getBubbleSize() { + return new Size(this.width_, this.height_); } -}; - -/** - * 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; -}; + /** + * 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_(); -/** - * 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; + // Allow the contents to resize. + if (this.resizeCallback_) { + this.resizeCallback_(); + } } - 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_(); + /** + * 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(' ')); } -}; - -/** - * 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; + /** + * 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); } - 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); + /** + * Dispose of this bubble. + */ + dispose() { + if (this.onMouseDownBubbleWrapper_) { + browserEvents.unbind(this.onMouseDownBubbleWrapper_); } - } 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_; + if (this.onMouseDownResizeWrapper_) { + browserEvents.unbind(this.onMouseDownResizeWrapper_); } + Bubble.unbindDragEvents_(); + dom.removeNode(this.bubbleGroup_); + this.disposed = true; } - 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; + /** + * 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_(); } - 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 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_); } - 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_(); + /** + * 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; } -}; - -/** - * 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) + ')'); + /** + * 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; } } - if (this.autoLayout_) { - this.layoutBubble_(); + + /** + * 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_(); } - this.positionBubble_(); - this.renderArrow_(); - // Allow the contents to resize. - if (this.resizeCallback_) { - this.resizeCallback_(); + /** + * 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; } -}; -/** - * 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(' ')); + /** + * 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; + } }; /** - * Change the colour of a bubble. - * @param {string} hexColour Hex code of colour. + * Width of the border around the bubble. */ -Bubble.prototype.setColour = function(hexColour) { - this.bubbleBack_.setAttribute('fill', hexColour); - this.bubbleArrow_.setAttribute('fill', hexColour); -}; +Bubble.BORDER_WIDTH = 6; /** - * Dispose of this bubble. + * Determines the thickness of the base of the arrow in relation to the size + * of the bubble. Higher numbers result in thinner arrows. */ -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; -}; +Bubble.ARROW_THICKNESS = 5; /** - * 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 + * The number of degrees that the arrow bends counter-clockwise. */ -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_(); -}; +Bubble.ARROW_ANGLE = 20; /** - * 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. + * The sharpness of the arrow's bend. Higher numbers result in smoother arrows. */ -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_); -}; +Bubble.ARROW_BEND = 4; /** - * 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 + * Distance between arrow point and anchor point. */ -Bubble.prototype.setAutoLayout = function(enable) { - this.autoLayout_ = enable; -}; +Bubble.ANCHOR_RADIUS = 8; /** - * 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 + * Mouse up event data. + * @type {?browserEvents.Data} + * @private */ -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; -}; +Bubble.onMouseUpWrapper_ = null; /** - * 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 + * Mouse move event data. + * @type {?browserEvents.Data} + * @private */ -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; -}; +Bubble.onMouseMoveWrapper_ = null; exports.Bubble = Bubble; From b309e4044b0850def6873a1bca0134131fb53b17 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 12:02:17 -0800 Subject: [PATCH 09/10] chore: clang-format --- core/block_drag_surface.js | 16 +++++++------- core/block_dragger.js | 29 ++++++++++++------------ core/bubble.js | 45 +++++++++++++++++++++----------------- core/bubble_dragger.js | 11 +++++----- core/utils/rect.js | 3 ++- 5 files changed, 55 insertions(+), 49 deletions(-) diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js index aa0bf2a1e9b..6c03480f3c8 100644 --- a/core/block_drag_surface.js +++ b/core/block_drag_surface.js @@ -50,8 +50,8 @@ const BlockDragSurfaceSvg = class { this.SVG_ = null; /** - * This is where blocks live while they are being dragged if the drag surface - * is enabled. + * This is where blocks live while they are being dragged if the drag + * surface is enabled. * @type {?SVGElement} * @private */ @@ -82,9 +82,9 @@ const BlockDragSurfaceSvg = class { 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. + * 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 */ @@ -244,9 +244,9 @@ const BlockDragSurfaceSvg = class { * 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. + * @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(); diff --git a/core/block_dragger.js b/core/block_dragger.js index beb7efbda72..d0ee612d356 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -86,8 +86,8 @@ const BlockDragger = class { this.wouldDeleteBlock_ = false; /** - * The location of the top left corner of the dragging block at the beginning - * of the drag in workspace coordinates. + * The location of the top left corner of the dragging block at the + * beginning of the drag in workspace coordinates. * @type {!Coordinate} * @protected */ @@ -148,8 +148,8 @@ const BlockDragger = class { } 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. + // the block dragger, which would also let the block not track the block + // drag surface. this.draggingBlock_.moveToDragSurface(); } @@ -175,8 +175,7 @@ const BlockDragger = class { * moved from the position at mouse down, in pixel units. * @protected */ - disconnectBlock_( - healStack, currentDragDeltaXY) { + disconnectBlock_(healStack, currentDragDeltaXY) { this.draggingBlock_.unplug(healStack); const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); const newLoc = Coordinate.sum(this.startXY_, delta); @@ -292,8 +291,8 @@ const BlockDragger = class { * 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. + * workspace units. newLocation is the new coordinate where the block + * should end up. * @protected */ getNewLocationAfterDrag_(currentDragDeltaXY) { @@ -304,9 +303,9 @@ const BlockDragger = class { } /** - * 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. + * 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 */ @@ -353,7 +352,8 @@ const BlockDragger = class { * 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. + * @param {boolean} isEnd True if we are at the end of a drag, false + * otherwise. * @protected */ updateToolboxStyle_(isEnd) { @@ -409,9 +409,8 @@ const BlockDragger = class { 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. + // 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); } diff --git a/core/bubble.js b/core/bubble.js index 60a76d3e601..e81453f77dc 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -55,8 +55,7 @@ const Bubble = class { * @struct * @alias Blockly.Bubble */ - constructor( - workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) { + constructor(workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) { this.workspace_ = workspace; this.content_ = content; this.shape_ = shape; @@ -184,7 +183,8 @@ const Bubble = class { this.arrow_radians_ = math.toRadians(angle); const canvas = workspace.getBubbleCanvas(); - canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight))); + canvas.appendChild( + this.createDom_(content, !!(bubbleWidth && bubbleHeight))); this.setAnchorLocation(anchorXY); if (!bubbleWidth || !bubbleHeight) { @@ -245,8 +245,9 @@ const Bubble = class { bubbleEmboss); if (hasResize) { this.resizeGroup_ = dom.createSvgElement( - Svg.G, - {'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'}, + Svg.G, { + 'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE' + }, this.bubbleGroup_); const resizeSize = 2 * Bubble.BORDER_WIDTH; dom.createSvgElement( @@ -436,7 +437,8 @@ const Bubble = class { */ layoutBubble_() { // Get the metrics in workspace units. - const viewMetrics = this.workspace_.getMetricsManager().getViewMetrics(true); + const viewMetrics = + this.workspace_.getMetricsManager().getViewMetrics(true); const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics); const optimalTop = this.getOptimalRelativeTop_(viewMetrics); @@ -459,7 +461,8 @@ const Bubble = class { const topPositionOverlap = this.getOverlap_(topPosition, viewMetrics); const startPositionOverlap = this.getOverlap_(startPosition, viewMetrics); const closerPositionOverlap = this.getOverlap_(closerPosition, viewMetrics); - const fartherPositionOverlap = this.getOverlap_(fartherPosition, 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. @@ -501,8 +504,9 @@ const Bubble = class { 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), + 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. @@ -513,8 +517,8 @@ const Bubble = class { // 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. + // 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}; @@ -535,8 +539,8 @@ const Bubble = class { } /** - * 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 + * 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. @@ -649,7 +653,8 @@ const Bubble = class { * @package */ moveTo(x, y) { - this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')'); + this.bubbleGroup_.setAttribute( + 'transform', 'translate(' + x + ',' + y + ')'); } /** @@ -778,8 +783,8 @@ const Bubble = class { 'C' + (baseX1 + swirlRun) + ',' + (baseY1 + swirlRise) + ' ' + relAnchorX + ',' + relAnchorY + ' ' + relAnchorX + ',' + relAnchorY); steps.push( - 'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) + ',' + - (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2); + 'C' + relAnchorX + ',' + relAnchorY + ' ' + (baseX2 + swirlRun) + + ',' + (baseY2 + swirlRise) + ' ' + baseX2 + ',' + baseY2); } steps.push('z'); this.bubbleArrow_.setAttribute('d', steps.join(' ')); @@ -834,8 +839,8 @@ const Bubble = class { } /** - * 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 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() { @@ -926,8 +931,8 @@ const Bubble = class { // 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++) { + for (let i = 0, textElement; + (textElement = paragraphElement.childNodes[i]); i++) { textElement.setAttribute('text-anchor', 'end'); textElement.setAttribute('x', maxWidth + Bubble.BORDER_WIDTH); } diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js index df51e9222e1..7a716d3dbeb 100644 --- a/core/bubble_dragger.js +++ b/core/bubble_dragger.js @@ -151,7 +151,8 @@ const BubbleDragger = class { this.updateCursorDuringBubbleDrag_(); } - // Call drag enter/exit/over after wouldDeleteBlock is called in shouldDelete_ + // 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_); @@ -241,7 +242,8 @@ const BubbleDragger = class { */ fireMoveEvent_() { if (this.draggingBubble_.isComment) { - // TODO (adodson): Resolve build errors when requiring WorkspaceCommentSvg. + // TODO (adodson): Resolve build errors when requiring + // WorkspaceCommentSvg. const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))( /** @type {!WorkspaceCommentSvg} */ (this.draggingBubble_)); event.setOldCoordinate(this.startXY_); @@ -269,9 +271,8 @@ const BubbleDragger = class { 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. + // 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); } diff --git a/core/utils/rect.js b/core/utils/rect.js index c3ea1b9211a..1089ce1021a 100644 --- a/core/utils/rect.js +++ b/core/utils/rect.js @@ -54,7 +54,8 @@ const Rect = class { * @return {boolean} Whether this rectangle contains given coordinate. */ contains(x, y) { - return x >= this.left && x <= this.right && y >= this.top && y <= this.bottom; + return x >= this.left && x <= this.right && y >= this.top && + y <= this.bottom; } /** From 6087fd46d81b212ee04b4960a953f64509ee8bc2 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 7 Jan 2022 12:14:55 -0800 Subject: [PATCH 10/10] chore(lint): lint and format --- core/bubble.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/bubble.js b/core/bubble.js index e81453f77dc..1f045cf5e67 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -246,7 +246,8 @@ const Bubble = class { if (hasResize) { this.resizeGroup_ = dom.createSvgElement( Svg.G, { - 'class': this.workspace_.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE' + 'class': this.workspace_.RTL ? 'blocklyResizeSW' : + 'blocklyResizeSE', }, this.bubbleGroup_); const resizeSize = 2 * Bubble.BORDER_WIDTH;