diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 33faf9b05..1404ccb16 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -93,6 +93,7 @@ goog.addDependency('../../core/interfaces/i_deletearea.js', ['Blockly.IDeleteAre goog.addDependency('../../core/interfaces/i_flyout.js', ['Blockly.IFlyout'], [], {}); goog.addDependency('../../core/interfaces/i_metrics_manager.js', ['Blockly.IMetricsManager'], [], {}); goog.addDependency('../../core/interfaces/i_movable.js', ['Blockly.IMovable'], [], {}); +goog.addDependency('../../core/interfaces/i_positionable.js', ['Blockly.IPositionable'], [], {}); goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistrable'], [], {}); goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], [], {}); goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], [], {}); @@ -174,7 +175,7 @@ goog.addDependency('../../core/toolbox/toolbox_item.js', ['Blockly.ToolboxItem'] goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.browserEvents', 'Blockly.utils.string'], {}); goog.addDependency('../../core/touch.js', ['Blockly.Touch'], ['Blockly.constants', 'Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.string'], {}); goog.addDependency('../../core/touch_gesture.js', ['Blockly.TouchGesture'], ['Blockly.Gesture', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.object'], {}); -goog.addDependency('../../core/trashcan.js', ['Blockly.Trashcan'], ['Blockly.Events', 'Blockly.Events.TrashcanOpen', 'Blockly.Scrollbar', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {}); +goog.addDependency('../../core/trashcan.js', ['Blockly.Trashcan'], ['Blockly.Events', 'Blockly.Events.TrashcanOpen', 'Blockly.IPositionable', 'Blockly.Scrollbar', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.toolbox'], {}); goog.addDependency('../../core/utils.js', ['Blockly.utils'], ['Blockly.Msg', 'Blockly.constants', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.colour', 'Blockly.utils.global', 'Blockly.utils.string', 'Blockly.utils.style', 'Blockly.utils.userAgent'], {}); goog.addDependency('../../core/utils/aria.js', ['Blockly.utils.aria'], [], {}); goog.addDependency('../../core/utils/colour.js', ['Blockly.utils.colour'], [], {}); @@ -209,9 +210,9 @@ goog.addDependency('../../core/workspace_comment_render_svg.js', ['Blockly.Works goog.addDependency('../../core/workspace_comment_svg.js', ['Blockly.WorkspaceCommentSvg'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.Events.Selected', 'Blockly.WorkspaceComment', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {}); goog.addDependency('../../core/workspace_drag_surface_svg.js', ['Blockly.WorkspaceDragSurfaceSvg'], ['Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {}); goog.addDependency('../../core/workspace_dragger.js', ['Blockly.WorkspaceDragger'], ['Blockly.utils.Coordinate'], {}); -goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {}); +goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.ContextMenu', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.ThemeChange', 'Blockly.Events.ViewportChange', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.MetricsManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {}); goog.addDependency('../../core/xml.js', ['Blockly.Xml'], ['Blockly.Events', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.dom', 'Blockly.utils.global', 'Blockly.utils.xml'], {}); -goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.Click', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es5'}); +goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.Click', 'Blockly.IPositionable', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.constants', 'Blockly.utils.Rect', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es5'}); goog.addDependency("base.js", [], []); // Load Blockly. diff --git a/core/interfaces/i_positionable.js b/core/interfaces/i_positionable.js new file mode 100644 index 000000000..a48ff5a33 --- /dev/null +++ b/core/interfaces/i_positionable.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for a positionable ui element. + * @author kozbial@google.com (Monica Kozbial) + */ + +'use strict'; + +goog.provide('Blockly.IPositionable'); + + +/** + * Interface for a component that is positioned on top of the workspace. + * @interface + */ +Blockly.IPositionable = function() {}; + +/** + * Positions the element. Called when the window is resized. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace + * viewMetrics. + * @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute + * metrics for the workspace. + * @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox + * metrics for the workspace. + * @param {!Array} savedPositions List of rectangles that + * are already on the workspace. + */ +Blockly.IPositionable.prototype.position; + +/** + * Returns the bounding rectangle of the UI element in pixel units relative to + * the Blockly injection div. + * @returns {!Blockly.utils.Rect} The plugin’s bounding box. + */ +Blockly.IPositionable.prototype.getBoundingRectangle; + + diff --git a/core/trashcan.js b/core/trashcan.js index 9eb0901b5..8c1a82d2e 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -16,8 +16,10 @@ goog.require('Blockly.browserEvents'); goog.require('Blockly.constants'); goog.require('Blockly.Events'); goog.require('Blockly.Events.TrashcanOpen'); +goog.require('Blockly.IPositionable'); goog.require('Blockly.Scrollbar'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.math'); goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.Svg'); goog.require('Blockly.utils.toolbox'); @@ -34,6 +36,7 @@ goog.requireType('Blockly.WorkspaceSvg'); * @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in. * @constructor * @implements {Blockly.IDeleteArea} + * @implements {Blockly.IPositionable} */ Blockly.Trashcan = function(workspace) { /** @@ -431,38 +434,74 @@ Blockly.Trashcan.prototype.emptyContents = function() { * Position the trashcan. * It is positioned in the opposite corner to the corner the * categories/toolbox starts at. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace + * viewMetrics. + * @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute + * metrics for the workspace. + * @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox + * metrics for the workspace. + * @param {!Array} savedPositions List of rectangles that + * are already on the workspace. */ -Blockly.Trashcan.prototype.position = function() { +Blockly.Trashcan.prototype.position = function( + viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions) { // Not yet initialized. if (!this.verticalSpacing_) { return; } - var metrics = this.workspace_.getMetrics(); - if (!metrics) { - // There are no metrics available (workspace is probably not visible). - return; - } - if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT || + + if (toolboxMetrics.position == Blockly.TOOLBOX_AT_LEFT || (this.workspace_.horizontalLayout && !this.workspace_.RTL)) { // Toolbox starts in the left corner. - this.left_ = metrics.viewWidth + metrics.absoluteLeft - + this.left_ = viewMetrics.width + absoluteMetrics.left - this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness; } else { // Toolbox starts in the right corner. this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness; } - if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { - this.top_ = this.verticalSpacing_; - } else { - this.top_ = metrics.viewHeight + metrics.absoluteTop - - (this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.verticalSpacing_; + var height = this.BODY_HEIGHT_ + this.LID_HEIGHT_; + // Upper corner placement + var minTop = this.top_ = this.verticalSpacing_; + // Bottom corner placement + var maxTop = viewMetrics.height + absoluteMetrics.top - height - + this.verticalSpacing_; + var placeBottom = toolboxMetrics.position !== Blockly.TOOLBOX_AT_BOTTOM; + this.top_ = placeBottom ? maxTop : minTop; + + // Check for collision and bump if needed. + var boundingRect = this.getBoundingRectangle(); + for (var i = 0, otherEl; (otherEl = savedPositions[i]); i++) { + if (boundingRect.intersects(otherEl)) { + if (placeBottom) { + // Bump up + this.top_ = otherEl.top - height - this.MARGIN_BOTTOM_; + } else { + this.top_ = otherEl.bottom + this.MARGIN_BOTTOM_; + } + // Recheck other savedPositions + boundingRect = this.getBoundingRectangle(); + i = -1; + } } + // Clamp top value within valid range. + this.top_ = Blockly.utils.math.clamp(minTop, this.top_, maxTop); this.svgGroup_.setAttribute('transform', 'translate(' + this.left_ + ',' + this.top_ + ')'); }; +/** + * Returns the bounding rectangle of the UI element in pixel units relative to + * the Blockly injection div. + * @returns {!Blockly.utils.Rect} The plugin’s bounding box. + */ +Blockly.Trashcan.prototype.getBoundingRectangle = function() { + var bottom = this.top_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_; + var right = this.left_ + this.WIDTH_; + return new Blockly.utils.Rect(this.top_, bottom, this.left_, right); +}; + /** * Return the deletion rectangle for this trash can. * @return {Blockly.utils.Rect} Rectangle in which to delete. diff --git a/core/utils/rect.js b/core/utils/rect.js index 296c0fcda..fcea05623 100644 --- a/core/utils/rect.js +++ b/core/utils/rect.js @@ -52,3 +52,15 @@ Blockly.utils.Rect = function(top, bottom, left, right) { Blockly.utils.Rect.prototype.contains = function(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 {Blockly.utils.Rect} other The other rectangle to check for + * intersection with. + * @return {boolean} Whether this rectangle intersects the provided rectangle. + */ +Blockly.utils.Rect.prototype.intersects = function(other) { + return !(this.left > other.right || this.right < other.left || + this.top > other.bottom || this.bottom < other.top); +}; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 4e7231bfa..2c5373da6 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -55,6 +55,7 @@ goog.requireType('Blockly.IASTNodeLocationSvg'); goog.requireType('Blockly.IBoundedElement'); goog.requireType('Blockly.IFlyout'); goog.requireType('Blockly.IMetricsManager'); +goog.requireType('Blockly.IPositionable'); goog.requireType('Blockly.IToolbox'); goog.requireType('Blockly.Marker'); goog.requireType('Blockly.ScrollbarPair'); @@ -1077,12 +1078,27 @@ Blockly.WorkspaceSvg.prototype.resize = function() { if (this.flyout_) { this.flyout_.position(); } + /** @type {Array} */ + var positionableEls = []; if (this.trashcan) { - this.trashcan.position(); + positionableEls.push(this.trashcan); } if (this.zoomControls_) { - this.zoomControls_.position(); + positionableEls.push(this.zoomControls_); } + if (positionableEls) { + var metricsManager = this.getMetricsManager(); + var viewMetrics = metricsManager.getViewMetrics(); + var absoluteMetrics = metricsManager.getAbsoluteMetrics(); + var toolboxMetrics = metricsManager.getToolboxMetrics(); + var savedPositions = []; + for (var i = 0, uiElement; (uiElement = positionableEls[i]); i++) { + uiElement.position( + viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions); + savedPositions.push(uiElement.getBoundingRectangle()); + } + } + if (this.scrollbar) { this.scrollbar.resize(); } diff --git a/core/zoom_controls.js b/core/zoom_controls.js index eeb8e023b..6479e24ec 100644 --- a/core/zoom_controls.js +++ b/core/zoom_controls.js @@ -20,7 +20,9 @@ goog.require('Blockly.Events.Click'); goog.require('Blockly.Scrollbar'); goog.require('Blockly.Touch'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.Svg'); +goog.require('Blockly.IPositionable'); goog.requireType('Blockly.WorkspaceSvg'); @@ -29,6 +31,7 @@ goog.requireType('Blockly.WorkspaceSvg'); * Class for a zoom controls. * @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in. * @constructor + * @implements {Blockly.IPositionable} */ Blockly.ZoomControls = function(workspace) { /** @@ -194,44 +197,82 @@ Blockly.ZoomControls.prototype.dispose = function() { } }; +/** + * Returns the bounding rectangle of the UI element in pixel units relative to + * the Blockly injection div. + * @returns {!Blockly.utils.Rect} The plugin’s bounding box. + */ +Blockly.ZoomControls.prototype.getBoundingRectangle = function() { + var bottom = this.top_ + this.HEIGHT_; + var right = this.left_ + this.WIDTH_; + return new Blockly.utils.Rect(this.top_, bottom, this.left_, right); +}; + + /** * Position the zoom controls. * It is positioned in the opposite corner to the corner the * categories/toolbox starts at. + * @param {!Blockly.MetricsManager.ContainerRegion} viewMetrics The workspace + * viewMetrics. + * @param {!Blockly.MetricsManager.AbsoluteMetrics} absoluteMetrics The absolute + * metrics for the workspace. + * @param {!Blockly.MetricsManager.ToolboxMetrics} toolboxMetrics The toolbox + * metrics for the workspace. + * @param {!Array} savedPositions List of rectangles that + * are already on the workspace. */ -Blockly.ZoomControls.prototype.position = function() { +Blockly.ZoomControls.prototype.position = function( + viewMetrics, absoluteMetrics, toolboxMetrics, savedPositions) { // Not yet initialized. if (!this.verticalSpacing_) { return; } - var metrics = this.workspace_.getMetrics(); - if (!metrics) { - // There are no metrics available (workspace is probably not visible). - return; - } - if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT || + if (toolboxMetrics.position == Blockly.TOOLBOX_AT_LEFT || (this.workspace_.horizontalLayout && !this.workspace_.RTL)) { // Toolbox starts in the left corner. - this.left_ = metrics.viewWidth + metrics.absoluteLeft - + this.left_ = viewMetrics.width + absoluteMetrics.left - this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness; } else { // Toolbox starts in the right corner. this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness; } - if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) { - this.top_ = this.verticalSpacing_; + // Upper corner placement + var minTop = this.top_ = this.verticalSpacing_; + // Bottom corner placement + var maxTop = viewMetrics.height + absoluteMetrics.top - + this.HEIGHT_ - this.verticalSpacing_; + var placeBottom = toolboxMetrics.position !== Blockly.TOOLBOX_AT_BOTTOM; + this.top_ = placeBottom ? maxTop : minTop; + if (placeBottom) { + this.zoomInGroup_.setAttribute('transform', 'translate(0, 43)'); + this.zoomOutGroup_.setAttribute('transform', 'translate(0, 77)'); + } else { this.zoomInGroup_.setAttribute('transform', 'translate(0, 34)'); if (this.zoomResetGroup_) { this.zoomResetGroup_.setAttribute('transform', 'translate(0, 77)'); } - } else { - this.top_ = metrics.viewHeight + metrics.absoluteTop - - this.HEIGHT_ - this.verticalSpacing_; - this.zoomInGroup_.setAttribute('transform', 'translate(0, 43)'); - this.zoomOutGroup_.setAttribute('transform', 'translate(0, 77)'); } + // Check for collision and bump if needed. + var boundingRect = this.getBoundingRectangle(); + for (var i = 0, otherEl; (otherEl = savedPositions[i]); i++) { + if (boundingRect.intersects(otherEl)) { + if (placeBottom) { + // Bump up + this.top_ = otherEl.top - this.HEIGHT_ - this.MARGIN_BOTTOM_; + } else { + this.top_ = otherEl.bottom + this.MARGIN_BOTTOM_; + } + // Recheck other savedPositions + boundingRect = this.getBoundingRectangle(); + i = -1; + } + } + // Clamp top value within valid range. + this.top_ = Blockly.utils.math.clamp(minTop, this.top_, maxTop); + this.svgGroup_.setAttribute('transform', 'translate(' + this.left_ + ',' + this.top_ + ')'); };