blockly
Version:
Blockly is a library for building visual programming editors.
1,599 lines (1,452 loc) • 87.2 kB
JavaScript
/**
* @license
* Copyright 2014 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Object representing a workspace rendered as SVG.
*/
'use strict';
/**
* Object representing a workspace rendered as SVG.
* @class
*/
goog.module('Blockly.WorkspaceSvg');
const ContextMenu = goog.require('Blockly.ContextMenu');
/* eslint-disable-next-line no-unused-vars */
const Procedures = goog.requireType('Blockly.Procedures');
const Tooltip = goog.require('Blockly.Tooltip');
/* eslint-disable-next-line no-unused-vars */
const Variables = goog.requireType('Blockly.Variables');
/* eslint-disable-next-line no-unused-vars */
const VariablesDynamic = goog.requireType('Blockly.VariablesDynamic');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const Xml = goog.require('Blockly.Xml');
const arrayUtils = goog.require('Blockly.utils.array');
const blockRendering = goog.require('Blockly.blockRendering');
const blocks = goog.require('Blockly.serialization.blocks');
const browserEvents = goog.require('Blockly.browserEvents');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const internalConstants = goog.require('Blockly.internalConstants');
const object = goog.require('Blockly.utils.object');
const registry = goog.require('Blockly.registry');
const svgMath = goog.require('Blockly.utils.svgMath');
const toolbox = goog.require('Blockly.utils.toolbox');
const userAgent = goog.require('Blockly.utils.userAgent');
const utils = goog.require('Blockly.utils');
/* eslint-disable-next-line no-unused-vars */
const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {BlocklyOptions} = goog.requireType('Blockly.BlocklyOptions');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Classic} = goog.require('Blockly.Themes.Classic');
const {ComponentManager} = goog.require('Blockly.ComponentManager');
const {ConnectionDB} = goog.require('Blockly.ConnectionDB');
const {ContextMenuRegistry} = goog.require('Blockly.ContextMenuRegistry');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {Cursor} = goog.requireType('Blockly.Cursor');
const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
/* eslint-disable-next-line no-unused-vars */
const {FlyoutButton} = goog.requireType('Blockly.FlyoutButton');
const {Gesture} = goog.require('Blockly.Gesture');
const {Grid} = goog.require('Blockly.Grid');
/* eslint-disable-next-line no-unused-vars */
const {IASTNodeLocationSvg} = goog.require('Blockly.IASTNodeLocationSvg');
/* eslint-disable-next-line no-unused-vars */
const {IBoundedElement} = goog.requireType('Blockly.IBoundedElement');
/* eslint-disable-next-line no-unused-vars */
const {IDragTarget} = goog.requireType('Blockly.IDragTarget');
/* eslint-disable-next-line no-unused-vars */
const {IFlyout} = goog.requireType('Blockly.IFlyout');
/* eslint-disable-next-line no-unused-vars */
const {IMetricsManager} = goog.requireType('Blockly.IMetricsManager');
/* eslint-disable-next-line no-unused-vars */
const {IToolbox} = goog.requireType('Blockly.IToolbox');
const {MarkerManager} = goog.require('Blockly.MarkerManager');
/* eslint-disable-next-line no-unused-vars */
const {Marker} = goog.requireType('Blockly.Marker');
/* eslint-disable-next-line no-unused-vars */
const {Metrics} = goog.requireType('Blockly.utils.Metrics');
const {Options} = goog.require('Blockly.Options');
const {Rect} = goog.require('Blockly.utils.Rect');
/* eslint-disable-next-line no-unused-vars */
const {Renderer} = goog.requireType('Blockly.blockRendering.Renderer');
/* eslint-disable-next-line no-unused-vars */
const {ScrollbarPair} = goog.requireType('Blockly.ScrollbarPair');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
const {ThemeManager} = goog.require('Blockly.ThemeManager');
/* eslint-disable-next-line no-unused-vars */
const {Theme} = goog.requireType('Blockly.Theme');
const {TouchGesture} = goog.require('Blockly.TouchGesture');
/* eslint-disable-next-line no-unused-vars */
const {Trashcan} = goog.requireType('Blockly.Trashcan');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
const {WorkspaceAudio} = goog.require('Blockly.WorkspaceAudio');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceCommentSvg} = goog.requireType('Blockly.WorkspaceCommentSvg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceDragSurfaceSvg} = goog.requireType('Blockly.WorkspaceDragSurfaceSvg');
const {Workspace} = goog.require('Blockly.Workspace');
/* eslint-disable-next-line no-unused-vars */
const {ZoomControls} = goog.requireType('Blockly.ZoomControls');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockCreate');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.ThemeChange');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.ViewportChange');
/** @suppress {extraRequire} */
goog.require('Blockly.MetricsManager');
/** @suppress {extraRequire} */
goog.require('Blockly.Msg');
/**
* Class for a workspace. This is an onscreen area with optional trashcan,
* scrollbars, bubbles, and dragging.
* @param {!Options} options Dictionary of options.
* @param {BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
* blocks.
* @param {WorkspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
* the workspace.
* @extends {Workspace}
* @implements {IASTNodeLocationSvg}
* @constructor
* @alias Blockly.WorkspaceSvg
*/
const WorkspaceSvg = function(
options, opt_blockDragSurface, opt_wsDragSurface) {
WorkspaceSvg.superClass_.constructor.call(this, options);
const MetricsManagerClass = registry.getClassFromOptions(
registry.Type.METRICS_MANAGER, options, true);
/**
* Object in charge of calculating metrics for the workspace.
* @type {!IMetricsManager}
* @private
*/
this.metricsManager_ = new MetricsManagerClass(this);
/**
* Method to get all the metrics that have to do with a workspace.
* @type {function():!Metrics}
* @package
*/
this.getMetrics = options.getMetrics ||
this.metricsManager_.getMetrics.bind(this.metricsManager_);
/**
* Translates the workspace.
* @type {function(!{x:number, y:number}):void}
* @package
*/
this.setMetrics =
options.setMetrics || WorkspaceSvg.setTopLevelWorkspaceMetrics_;
/**
* @type {!ComponentManager}
* @private
*/
this.componentManager_ = new ComponentManager();
this.connectionDBList = ConnectionDB.init(this.connectionChecker);
if (opt_blockDragSurface) {
this.blockDragSurface_ = opt_blockDragSurface;
}
if (opt_wsDragSurface) {
this.workspaceDragSurface_ = opt_wsDragSurface;
}
this.useWorkspaceDragSurface_ =
!!this.workspaceDragSurface_ && svgMath.is3dSupported();
/**
* List of currently highlighted blocks. Block highlighting is often used to
* visually mark blocks currently being executed.
* @type {!Array<!BlockSvg>}
* @private
*/
this.highlightedBlocks_ = [];
/**
* Object in charge of loading, storing, and playing audio for a workspace.
* @type {!WorkspaceAudio}
* @private
*/
this.audioManager_ = new WorkspaceAudio(
/** @type {WorkspaceSvg} */ (options.parentWorkspace));
/**
* This workspace's grid object or null.
* @type {Grid}
* @private
*/
this.grid_ = this.options.gridPattern ?
new Grid(this.options.gridPattern, options.gridOptions) :
null;
/**
* Manager in charge of markers and cursors.
* @type {!MarkerManager}
* @private
*/
this.markerManager_ = new MarkerManager(this);
/**
* Map from function names to callbacks, for deciding what to do when a custom
* toolbox category is opened.
* @type {!Object<string, ?function(!Workspace):
* !toolbox.FlyoutDefinition>}
* @private
*/
this.toolboxCategoryCallbacks_ = Object.create(null);
/**
* Map from function names to callbacks, for deciding what to do when a button
* is clicked.
* @type {!Object<string, ?function(!FlyoutButton)>}
* @private
*/
this.flyoutButtonCallbacks_ = Object.create(null);
const Variables = goog.module.get('Blockly.Variables');
if (Variables && Variables.flyoutCategory) {
this.registerToolboxCategoryCallback(
Variables.CATEGORY_NAME, Variables.flyoutCategory);
}
const VariablesDynamic = goog.module.get('Blockly.VariablesDynamic');
if (VariablesDynamic && VariablesDynamic.flyoutCategory) {
this.registerToolboxCategoryCallback(
VariablesDynamic.CATEGORY_NAME, VariablesDynamic.flyoutCategory);
}
const Procedures = goog.module.get('Blockly.Procedures');
if (Procedures && Procedures.flyoutCategory) {
this.registerToolboxCategoryCallback(
Procedures.CATEGORY_NAME, Procedures.flyoutCategory);
this.addChangeListener(Procedures.mutatorOpenListener);
}
/**
* Object in charge of storing and updating the workspace theme.
* @type {!ThemeManager}
* @protected
*/
this.themeManager_ = this.options.parentWorkspace ?
this.options.parentWorkspace.getThemeManager() :
new ThemeManager(this, this.options.theme || Classic);
this.themeManager_.subscribeWorkspace(this);
/**
* The block renderer used for rendering blocks on this workspace.
* @type {!Renderer}
* @private
*/
this.renderer_ = blockRendering.init(
this.options.renderer || 'geras', this.getTheme(),
this.options.rendererOverrides);
/**
* Cached parent SVG.
* @type {SVGElement}
* @private
*/
this.cachedParentSvg_ = null;
/**
* True if keyboard accessibility mode is on, false otherwise.
* @type {boolean}
*/
this.keyboardAccessibilityMode = false;
/**
* The list of top-level bounded elements on the workspace.
* @type {!Array<!IBoundedElement>}
* @private
*/
this.topBoundedElements_ = [];
/**
* The recorded drag targets.
* @type {!Array<
* {
* component: !IDragTarget,
* clientRect: !Rect
* }>}
* @private
*/
this.dragTargetAreas_ = [];
/**
* The cached size of the parent svg element.
* Used to compute svg metrics.
* @type {!Size}
* @private
*/
this.cachedParentSvgSize_ = new Size(0, 0);
};
object.inherits(WorkspaceSvg, Workspace);
/**
* A wrapper function called when a resize event occurs.
* You can pass the result to `eventHandling.unbind`.
* @type {?browserEvents.Data}
* @private
*/
WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
/**
* The render status of an SVG workspace.
* Returns `false` for headless workspaces and true for instances of
* `WorkspaceSvg`.
* @type {boolean}
*/
WorkspaceSvg.prototype.rendered = true;
/**
* Whether the workspace is visible. False if the workspace has been hidden
* by calling `setVisible(false)`.
* @type {boolean}
* @private
*/
WorkspaceSvg.prototype.isVisible_ = true;
/**
* Is this workspace the surface for a flyout?
* @type {boolean}
*/
WorkspaceSvg.prototype.isFlyout = false;
/**
* Is this workspace the surface for a mutator?
* @type {boolean}
* @package
*/
WorkspaceSvg.prototype.isMutator = false;
/**
* Whether this workspace has resizes enabled.
* Disable during batch operations for a performance improvement.
* @type {boolean}
* @private
*/
WorkspaceSvg.prototype.resizesEnabled_ = true;
/**
* Current horizontal scrolling offset in pixel units, relative to the
* workspace origin.
*
* It is useful to think about a view, and a canvas moving beneath that
* view. As the canvas moves right, this value becomes more positive, and
* the view is now "seeing" the left side of the canvas. As the canvas moves
* left, this value becomes more negative, and the view is now "seeing" the
* right side of the canvas.
*
* The confusing thing about this value is that it does not, and must not
* include the absoluteLeft offset. This is because it is used to calculate
* the viewLeft value.
*
* The viewLeft is relative to the workspace origin (although in pixel
* units). The workspace origin is the top-left corner of the workspace (at
* least when it is enabled). It is shifted from the top-left of the blocklyDiv
* so as not to be beneath the toolbox.
*
* When the workspace is enabled the viewLeft and workspace origin are at
* the same X location. As the canvas slides towards the right beneath the view
* this value (scrollX) becomes more positive, and the viewLeft becomes more
* negative relative to the workspace origin (imagine the workspace origin
* as a dot on the canvas sliding to the right as the canvas moves).
*
* So if the scrollX were to include the absoluteLeft this would in a way
* "unshift" the workspace origin. This means that the viewLeft would be
* representing the left edge of the blocklyDiv, rather than the left edge
* of the workspace.
*
* @type {number}
*/
WorkspaceSvg.prototype.scrollX = 0;
/**
* Current vertical scrolling offset in pixel units, relative to the
* workspace origin.
*
* It is useful to think about a view, and a canvas moving beneath that
* view. As the canvas moves down, this value becomes more positive, and the
* view is now "seeing" the upper part of the canvas. As the canvas moves
* up, this value becomes more negative, and the view is "seeing" the lower
* part of the canvas.
*
* This confusing thing about this value is that it does not, and must not
* include the absoluteTop offset. This is because it is used to calculate
* the viewTop value.
*
* The viewTop is relative to the workspace origin (although in pixel
* units). The workspace origin is the top-left corner of the workspace (at
* least when it is enabled). It is shifted from the top-left of the
* blocklyDiv so as not to be beneath the toolbox.
*
* When the workspace is enabled the viewTop and workspace origin are at the
* same Y location. As the canvas slides towards the bottom this value
* (scrollY) becomes more positive, and the viewTop becomes more negative
* relative to the workspace origin (image in the workspace origin as a dot
* on the canvas sliding downwards as the canvas moves).
*
* So if the scrollY were to include the absoluteTop this would in a way
* "unshift" the workspace origin. This means that the viewTop would be
* representing the top edge of the blocklyDiv, rather than the top edge of
* the workspace.
*
* @type {number}
*/
WorkspaceSvg.prototype.scrollY = 0;
/**
* Horizontal scroll value when scrolling started in pixel units.
* @type {number}
*/
WorkspaceSvg.prototype.startScrollX = 0;
/**
* Vertical scroll value when scrolling started in pixel units.
* @type {number}
*/
WorkspaceSvg.prototype.startScrollY = 0;
/**
* Distance from mouse to object being dragged.
* @type {Coordinate}
* @private
*/
WorkspaceSvg.prototype.dragDeltaXY_ = null;
/**
* Current scale.
* @type {number}
*/
WorkspaceSvg.prototype.scale = 1;
/**
* Cached scale value. Used to detect changes in viewport.
* @type {number}
* @private
*/
WorkspaceSvg.prototype.oldScale_ = 1;
/**
* Cached viewport top value. Used to detect changes in viewport.
* @type {number}
* @private
*/
WorkspaceSvg.prototype.oldTop_ = 0;
/**
* Cached viewport left value. Used to detect changes in viewport.
* @type {number}
* @private
*/
WorkspaceSvg.prototype.oldLeft_ = 0;
/**
* The workspace's trashcan (if any).
* @type {Trashcan}
*/
WorkspaceSvg.prototype.trashcan = null;
/**
* This workspace's scrollbars, if they exist.
* @type {ScrollbarPair}
*/
WorkspaceSvg.prototype.scrollbar = null;
/**
* Fixed flyout providing blocks which may be dragged into this workspace.
* @type {IFlyout}
* @private
*/
WorkspaceSvg.prototype.flyout_ = null;
/**
* Category-based toolbox providing blocks which may be dragged into this
* workspace.
* @type {IToolbox}
* @private
*/
WorkspaceSvg.prototype.toolbox_ = null;
/**
* The current gesture in progress on this workspace, if any.
* @type {TouchGesture}
* @private
*/
WorkspaceSvg.prototype.currentGesture_ = null;
/**
* This workspace's surface for dragging blocks, if it exists.
* @type {BlockDragSurfaceSvg}
* @private
*/
WorkspaceSvg.prototype.blockDragSurface_ = null;
/**
* This workspace's drag surface, if it exists.
* @type {WorkspaceDragSurfaceSvg}
* @private
*/
WorkspaceSvg.prototype.workspaceDragSurface_ = null;
/**
* Whether to move workspace to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
* @type {boolean}
* @private
*/
WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
/**
* Whether the drag surface is actively in use. When true, calls to
* translate will translate the drag surface instead of the translating the
* workspace directly.
* This is set to true in setupDragSurface and to false in resetDragSurface.
* @type {boolean}
* @private
*/
WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
/**
* The first parent div with 'injectionDiv' in the name, or null if not set.
* Access this with getInjectionDiv.
* @type {Element}
* @private
*/
WorkspaceSvg.prototype.injectionDiv_ = null;
/**
* Last known position of the page scroll.
* This is used to determine whether we have recalculated screen coordinate
* stuff since the page scrolled.
* @type {Coordinate}
* @private
*/
WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
/**
* Developers may define this function to add custom menu options to the
* workspace's context menu or edit the workspace-created set of menu options.
* @param {!Array<!Object>} options List of menu options to add to.
* @param {!Event} e The right-click event that triggered the context menu.
*/
WorkspaceSvg.prototype.configureContextMenu;
/**
* In a flyout, the target workspace where blocks should be placed after a drag.
* Otherwise null.
* @type {WorkspaceSvg}
* @package
*/
WorkspaceSvg.prototype.targetWorkspace = null;
/**
* Inverted screen CTM, for use in mouseToSvg.
* @type {?SVGMatrix}
* @private
*/
WorkspaceSvg.prototype.inverseScreenCTM_ = null;
/**
* Inverted screen CTM is dirty, recalculate it.
* @type {boolean}
* @private
*/
WorkspaceSvg.prototype.inverseScreenCTMDirty_ = true;
/**
* Get the marker manager for this workspace.
* @return {!MarkerManager} The marker manager.
*/
WorkspaceSvg.prototype.getMarkerManager = function() {
return this.markerManager_;
};
/**
* Gets the metrics manager for this workspace.
* @return {!IMetricsManager} The metrics manager.
* @public
*/
WorkspaceSvg.prototype.getMetricsManager = function() {
return this.metricsManager_;
};
/**
* Sets the metrics manager for the workspace.
* @param {!IMetricsManager} metricsManager The metrics manager.
* @package
*/
WorkspaceSvg.prototype.setMetricsManager = function(metricsManager) {
this.metricsManager_ = metricsManager;
this.getMetrics = this.metricsManager_.getMetrics.bind(this.metricsManager_);
};
/**
* Gets the component manager for this workspace.
* @return {!ComponentManager} The component manager.
* @public
*/
WorkspaceSvg.prototype.getComponentManager = function() {
return this.componentManager_;
};
/**
* Add the cursor SVG to this workspaces SVG group.
* @param {SVGElement} cursorSvg The SVG root of the cursor to be added to the
* workspace SVG group.
* @package
*/
WorkspaceSvg.prototype.setCursorSvg = function(cursorSvg) {
this.markerManager_.setCursorSvg(cursorSvg);
};
/**
* Add the marker SVG to this workspaces SVG group.
* @param {SVGElement} markerSvg The SVG root of the marker to be added to the
* workspace SVG group.
* @package
*/
WorkspaceSvg.prototype.setMarkerSvg = function(markerSvg) {
this.markerManager_.setMarkerSvg(markerSvg);
};
/**
* Get the marker with the given ID.
* @param {string} id The ID of the marker.
* @return {?Marker} The marker with the given ID or null if no marker
* with the given ID exists.
* @package
*/
WorkspaceSvg.prototype.getMarker = function(id) {
if (this.markerManager_) {
return this.markerManager_.getMarker(id);
}
return null;
};
/**
* The cursor for this workspace.
* @return {?Cursor} The cursor for the workspace.
*/
WorkspaceSvg.prototype.getCursor = function() {
if (this.markerManager_) {
return this.markerManager_.getCursor();
}
return null;
};
/**
* Get the block renderer attached to this workspace.
* @return {!Renderer} The renderer attached to this
* workspace.
*/
WorkspaceSvg.prototype.getRenderer = function() {
return this.renderer_;
};
/**
* Get the theme manager for this workspace.
* @return {!ThemeManager} The theme manager for this workspace.
* @package
*/
WorkspaceSvg.prototype.getThemeManager = function() {
return this.themeManager_;
};
/**
* Get the workspace theme object.
* @return {!Theme} The workspace theme object.
*/
WorkspaceSvg.prototype.getTheme = function() {
return this.themeManager_.getTheme();
};
/**
* Set the workspace theme object.
* If no theme is passed, default to the `Classic` theme.
* @param {Theme} theme The workspace theme object.
*/
WorkspaceSvg.prototype.setTheme = function(theme) {
if (!theme) {
theme = /** @type {!Theme} */ (Classic);
}
this.themeManager_.setTheme(theme);
};
/**
* Refresh all blocks on the workspace after a theme update.
* @package
*/
WorkspaceSvg.prototype.refreshTheme = function() {
if (this.svgGroup_) {
this.renderer_.refreshDom(this.svgGroup_, this.getTheme());
}
// Update all blocks in workspace that have a style name.
this.updateBlockStyles_(this.getAllBlocks(false).filter(function(block) {
return !!block.getStyleName();
}));
// Update current toolbox selection.
this.refreshToolboxSelection();
if (this.toolbox_) {
this.toolbox_.refreshTheme();
}
// Re-render if workspace is visible
if (this.isVisible()) {
this.setVisible(true);
}
const event = new (eventUtils.get(eventUtils.THEME_CHANGE))(
this.getTheme().name, this.id);
eventUtils.fire(event);
};
/**
* Updates all the blocks with new style.
* @param {!Array<!Block>} blocks List of blocks to update the style
* on.
* @private
*/
WorkspaceSvg.prototype.updateBlockStyles_ = function(blocks) {
for (let i = 0, block; (block = blocks[i]); i++) {
const blockStyleName = block.getStyleName();
if (blockStyleName) {
block.setStyle(blockStyleName);
if (block.mutator) {
block.mutator.updateBlockStyle();
}
}
}
};
/**
* Getter for the inverted screen CTM.
* @return {?SVGMatrix} The matrix to use in mouseToSvg
*/
WorkspaceSvg.prototype.getInverseScreenCTM = function() {
// Defer getting the screen CTM until we actually need it, this should
// avoid forced reflows from any calls to updateInverseScreenCTM.
if (this.inverseScreenCTMDirty_) {
const ctm = this.getParentSvg().getScreenCTM();
if (ctm) {
this.inverseScreenCTM_ = ctm.inverse();
this.inverseScreenCTMDirty_ = false;
}
}
return this.inverseScreenCTM_;
};
/**
* Mark the inverse screen CTM as dirty.
*/
WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
this.inverseScreenCTMDirty_ = true;
};
/**
* Getter for isVisible
* @return {boolean} Whether the workspace is visible.
* False if the workspace has been hidden by calling `setVisible(false)`.
*/
WorkspaceSvg.prototype.isVisible = function() {
return this.isVisible_;
};
/**
* Return the absolute coordinates of the top-left corner of this element,
* scales that after canvas SVG element, if it's a descendant.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!SVGElement} element SVG element to find the coordinates of.
* @return {!Coordinate} Object with .x and .y properties.
* @package
*/
WorkspaceSvg.prototype.getSvgXY = function(element) {
let x = 0;
let y = 0;
let scale = 1;
if (dom.containsNode(this.getCanvas(), element) ||
dom.containsNode(this.getBubbleCanvas(), element)) {
// Before the SVG canvas, scale the coordinates.
scale = this.scale;
}
do {
// Loop through this block and every parent.
const xy = svgMath.getRelativeXY(element);
if (element === this.getCanvas() || element === this.getBubbleCanvas()) {
// After the SVG canvas, don't scale the coordinates.
scale = 1;
}
x += xy.x * scale;
y += xy.y * scale;
element = /** @type {!SVGElement} */ (element.parentNode);
} while (element && element !== this.getParentSvg());
return new Coordinate(x, y);
};
/**
* Gets the size of the workspace's parent SVG element.
* @return {!Size} The cached width and height of the workspace's
* parent SVG element.
* @package
*/
WorkspaceSvg.prototype.getCachedParentSvgSize = function() {
const size = this.cachedParentSvgSize_;
return new Size(size.width, size.height);
};
/**
* Return the position of the workspace origin relative to the injection div
* origin in pixels.
* The workspace origin is where a block would render at position (0, 0).
* It is not the upper left corner of the workspace SVG.
* @return {!Coordinate} Offset in pixels.
* @package
*/
WorkspaceSvg.prototype.getOriginOffsetInPixels = function() {
return svgMath.getInjectionDivXY(this.getCanvas());
};
/**
* Return the injection div that is a parent of this workspace.
* Walks the DOM the first time it's called, then returns a cached value.
* Note: We assume this is only called after the workspace has been injected
* into the DOM.
* @return {!Element} The first parent div with 'injectionDiv' in the name.
* @package
*/
WorkspaceSvg.prototype.getInjectionDiv = function() {
// NB: it would be better to pass this in at createDom, but is more likely to
// break existing uses of Blockly.
if (!this.injectionDiv_) {
let element = this.svgGroup_;
while (element) {
const classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {
this.injectionDiv_ = element;
break;
}
element = /** @type {!Element} */ (element.parentNode);
}
}
return /** @type {!Element} */ (this.injectionDiv_);
};
/**
* Get the SVG block canvas for the workspace.
* @return {?SVGElement} The SVG group for the workspace.
* @package
*/
WorkspaceSvg.prototype.getBlockCanvas = function() {
return this.svgBlockCanvas_;
};
/**
* Save resize handler data so we can delete it later in dispose.
* @param {!browserEvents.Data} handler Data that can be passed to
* eventHandling.unbind.
*/
WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
this.resizeHandlerWrapper_ = handler;
};
/**
* Create the workspace DOM elements.
* @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
* 'blocklyMutatorBackground'.
* @return {!Element} The workspace's SVG group.
*/
WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
/**
* <g class="blocklyWorkspace">
* <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
* [Trashcan and/or flyout may go here]
* <g class="blocklyBlockCanvas"></g>
* <g class="blocklyBubbleCanvas"></g>
* </g>
* @type {SVGElement}
*/
this.svgGroup_ =
dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}, null);
// Note that a <g> alone does not receive mouse events--it must have a
// valid target inside it. If no background class is specified, as in the
// flyout, the workspace will not receive mouse events.
if (opt_backgroundClass) {
/** @type {SVGElement} */
this.svgBackground_ = dom.createSvgElement(
Svg.RECT,
{'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
this.svgGroup_);
if (opt_backgroundClass === 'blocklyMainBackground' && this.grid_) {
this.svgBackground_.style.fill =
'url(#' + this.grid_.getPatternId() + ')';
} else {
this.themeManager_.subscribe(
this.svgBackground_, 'workspaceBackgroundColour', 'fill');
}
}
/** @type {SVGElement} */
this.svgBlockCanvas_ = dom.createSvgElement(
Svg.G, {'class': 'blocklyBlockCanvas'}, this.svgGroup_);
/** @type {SVGElement} */
this.svgBubbleCanvas_ = dom.createSvgElement(
Svg.G, {'class': 'blocklyBubbleCanvas'}, this.svgGroup_);
if (!this.isFlyout) {
browserEvents.conditionalBind(
this.svgGroup_, 'mousedown', this, this.onMouseDown_, false, true);
// This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683,
// which otherwise prevents zoom/scroll events from being observed in
// Safari. Once that bug is fixed it should be removed.
document.body.addEventListener('wheel', function() {});
browserEvents.conditionalBind(
this.svgGroup_, 'wheel', this, this.onMouseWheel_);
}
// Determine if there needs to be a category tree, or a simple list of
// blocks. This cannot be changed later, since the UI is very different.
if (this.options.hasCategories) {
const ToolboxClass =
registry.getClassFromOptions(registry.Type.TOOLBOX, this.options, true);
this.toolbox_ = new ToolboxClass(this);
}
if (this.grid_) {
this.grid_.update(this.scale);
}
this.recordDragTargets();
const CursorClass =
registry.getClassFromOptions(registry.Type.CURSOR, this.options);
CursorClass && this.markerManager_.setCursor(new CursorClass());
this.renderer_.createDom(this.svgGroup_, this.getTheme());
return this.svgGroup_;
};
/**
* Dispose of this workspace.
* Unlink from all DOM elements to prevent memory leaks.
* @suppress {checkTypes}
*/
WorkspaceSvg.prototype.dispose = function() {
// Stop rerendering.
this.rendered = false;
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
if (this.svgGroup_) {
dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
this.svgBlockCanvas_ = null;
this.svgBubbleCanvas_ = null;
if (this.toolbox_) {
this.toolbox_.dispose();
this.toolbox_ = null;
}
if (this.flyout_) {
this.flyout_.dispose();
this.flyout_ = null;
}
if (this.trashcan) {
this.trashcan.dispose();
this.trashcan = null;
}
if (this.scrollbar) {
this.scrollbar.dispose();
this.scrollbar = null;
}
if (this.zoomControls_) {
this.zoomControls_.dispose();
this.zoomControls_ = null;
}
if (this.audioManager_) {
this.audioManager_.dispose();
this.audioManager_ = null;
}
if (this.grid_) {
this.grid_.dispose();
this.grid_ = null;
}
this.renderer_.dispose();
if (this.markerManager_) {
this.markerManager_.dispose();
this.markerManager_ = null;
}
WorkspaceSvg.superClass_.dispose.call(this);
// Dispose of theme manager after all blocks and mutators are disposed of.
if (this.themeManager_) {
this.themeManager_.unsubscribeWorkspace(this);
this.themeManager_.unsubscribe(this.svgBackground_);
if (!this.options.parentWorkspace) {
this.themeManager_.dispose();
this.themeManager_ = null;
}
}
this.connectionDBList = null;
this.toolboxCategoryCallbacks_ = null;
this.flyoutButtonCallbacks_ = null;
if (!this.options.parentWorkspace) {
// Top-most workspace. Dispose of the div that the
// SVG is injected into (i.e. injectionDiv).
const parentSvg = this.getParentSvg();
if (parentSvg && parentSvg.parentNode) {
dom.removeNode(parentSvg.parentNode);
}
}
if (this.resizeHandlerWrapper_) {
browserEvents.unbind(this.resizeHandlerWrapper_);
this.resizeHandlerWrapper_ = null;
}
};
/**
* Obtain a newly created block.
*
* This block's SVG must still be initialized
* ([initSvg]{@link BlockSvg#initSvg}) and it must be rendered
* ([render]{@link BlockSvg#render}) before the block will be visible.
* @param {!string} prototypeName Name of the language object containing
* type-specific functions for this block.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @return {!BlockSvg} The created block.
* @override
*/
WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
return new BlockSvg(this, prototypeName, opt_id);
};
/**
* Add a trashcan.
* @package
*/
WorkspaceSvg.prototype.addTrashcan = function() {
const {Trashcan} = goog.module.get('Blockly.Trashcan');
if (!Trashcan) {
throw Error('Missing require for Blockly.Trashcan');
}
/** @type {Trashcan} */
this.trashcan = new Trashcan(this);
const svgTrashcan = this.trashcan.createDom();
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
};
/**
* Add zoom controls.
* @package
*/
WorkspaceSvg.prototype.addZoomControls = function() {
const {ZoomControls} = goog.module.get('Blockly.ZoomControls');
if (!ZoomControls) {
throw Error('Missing require for Blockly.ZoomControls');
}
/** @type {ZoomControls} */
this.zoomControls_ = new ZoomControls(this);
const svgZoomControls = this.zoomControls_.createDom();
this.svgGroup_.appendChild(svgZoomControls);
};
/**
* Add a flyout element in an element with the given tag name.
* @param {string|
* !Svg<!SVGSVGElement>|
* !Svg<!SVGGElement>} tagName What type of tag the
* flyout belongs in.
* @return {!Element} The element containing the flyout DOM.
* @package
*/
WorkspaceSvg.prototype.addFlyout = function(tagName) {
const workspaceOptions = new Options(
/** @type {!BlocklyOptions} */
({
'parentWorkspace': this,
'rtl': this.RTL,
'oneBasedIndex': this.options.oneBasedIndex,
'horizontalLayout': this.horizontalLayout,
'renderer': this.options.renderer,
'rendererOverrides': this.options.rendererOverrides,
'move': {
'scrollbars': true,
},
}));
workspaceOptions.toolboxPosition = this.options.toolboxPosition;
if (this.horizontalLayout) {
const HorizontalFlyout = registry.getClassFromOptions(
registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, this.options, true);
this.flyout_ = new HorizontalFlyout(workspaceOptions);
} else {
const VerticalFlyout = registry.getClassFromOptions(
registry.Type.FLYOUTS_VERTICAL_TOOLBOX, this.options, true);
this.flyout_ = new VerticalFlyout(workspaceOptions);
}
this.flyout_.autoClose = false;
this.flyout_.getWorkspace().setVisible(true);
// Return the element so that callers can place it in their desired
// spot in the DOM. For example, mutator flyouts do not go in the same place
// as main workspace flyouts.
return this.flyout_.createDom(tagName);
};
/**
* Getter for the flyout associated with this workspace. This flyout may be
* owned by either the toolbox or the workspace, depending on toolbox
* configuration. It will be null if there is no flyout.
* @param {boolean=} opt_own Whether to only return the workspace's own flyout.
* @return {?IFlyout} The flyout on this workspace.
* @package
*/
WorkspaceSvg.prototype.getFlyout = function(opt_own) {
if (this.flyout_ || opt_own) {
return this.flyout_;
}
if (this.toolbox_) {
return this.toolbox_.getFlyout();
}
return null;
};
/**
* Getter for the toolbox associated with this workspace, if one exists.
* @return {?IToolbox} The toolbox on this workspace.
* @package
*/
WorkspaceSvg.prototype.getToolbox = function() {
return this.toolbox_;
};
/**
* Update items that use screen coordinate calculations
* because something has changed (e.g. scroll position, window size).
* @private
*/
WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
this.updateInverseScreenCTM();
this.recordDragTargets();
};
/**
* If enabled, resize the parts of the workspace that change when the workspace
* contents (e.g. block positions) change. This will also scroll the
* workspace contents if needed.
* @package
*/
WorkspaceSvg.prototype.resizeContents = function() {
if (!this.resizesEnabled_ || !this.rendered) {
return;
}
if (this.scrollbar) {
this.scrollbar.resize();
}
this.updateInverseScreenCTM();
};
/**
* Resize and reposition all of the workspace chrome (toolbox,
* trash, scrollbars etc.)
* This should be called when something changes that
* requires recalculating dimensions and positions of the
* trash, zoom, toolbox, etc. (e.g. window resize).
*/
WorkspaceSvg.prototype.resize = function() {
if (this.toolbox_) {
this.toolbox_.position();
}
if (this.flyout_) {
this.flyout_.position();
}
const positionables = this.componentManager_.getComponents(
ComponentManager.Capability.POSITIONABLE, true);
const metrics = this.getMetricsManager().getUiMetrics();
const savedPositions = [];
for (let i = 0, positionable; (positionable = positionables[i]); i++) {
positionable.position(metrics, savedPositions);
const boundingRect = positionable.getBoundingRectangle();
if (boundingRect) {
savedPositions.push(boundingRect);
}
}
if (this.scrollbar) {
this.scrollbar.resize();
}
this.updateScreenCalculations_();
};
/**
* Resizes and repositions workspace chrome if the page has a new
* scroll position.
* @package
*/
WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled = function() {
/* eslint-disable indent */
const currScroll = svgMath.getDocumentScroll();
if (!Coordinate.equals(this.lastRecordedPageScroll_, currScroll)) {
this.lastRecordedPageScroll_ = currScroll;
this.updateScreenCalculations_();
}
}; /* eslint-enable indent */
/**
* Get the SVG element that forms the drawing surface.
* @return {!SVGGElement} SVG group element.
*/
WorkspaceSvg.prototype.getCanvas = function() {
return /** @type {!SVGGElement} */ (this.svgBlockCanvas_);
};
/**
* Caches the width and height of the workspace's parent SVG element for use
* with getSvgMetrics.
* @param {?number} width The width of the parent SVG element.
* @param {?number} height The height of the parent SVG element
* @package
*/
WorkspaceSvg.prototype.setCachedParentSvgSize = function(width, height) {
const svg = this.getParentSvg();
if (width) {
this.cachedParentSvgSize_.width = width;
// This is set to support the public (but deprecated) Blockly.svgSize
// method.
svg.cachedWidth_ = width;
}
if (height) {
this.cachedParentSvgSize_.height = height;
// This is set to support the public (but deprecated) Blockly.svgSize
// method.
svg.cachedHeight_ = height;
}
};
/**
* Get the SVG element that forms the bubble surface.
* @return {!SVGGElement} SVG group element.
*/
WorkspaceSvg.prototype.getBubbleCanvas = function() {
return /** @type {!SVGGElement} */ (this.svgBubbleCanvas_);
};
/**
* Get the SVG element that contains this workspace.
* Note: We assume this is only called after the workspace has been injected
* into the DOM.
* @return {!SVGElement} SVG element.
*/
WorkspaceSvg.prototype.getParentSvg = function() {
if (!this.cachedParentSvg_) {
let element = this.svgGroup_;
while (element) {
if (element.tagName === 'svg') {
this.cachedParentSvg_ = element;
break;
}
element = /** @type {!SVGElement} */ (element.parentNode);
}
}
return /** @type {!SVGElement} */ (this.cachedParentSvg_);
};
/**
* Fires a viewport event if events are enabled and there is a change in
* viewport values.
* @package
*/
WorkspaceSvg.prototype.maybeFireViewportChangeEvent = function() {
if (!eventUtils.isEnabled()) {
return;
}
const scale = this.scale;
const top = -this.scrollY;
const left = -this.scrollX;
if (scale === this.oldScale_ && Math.abs(top - this.oldTop_) < 1 &&
Math.abs(left - this.oldLeft_) < 1) {
// Ignore sub-pixel changes in top and left. Due to #4192 there are a lot of
// negligible changes in viewport top/left.
return;
}
const event = new (eventUtils.get(eventUtils.VIEWPORT_CHANGE))(
top, left, scale, this.id, this.oldScale_);
this.oldScale_ = scale;
this.oldTop_ = top;
this.oldLeft_ = left;
eventUtils.fire(event);
};
/**
* Translate this workspace to new coordinates.
* @param {number} x Horizontal translation, in pixel units relative to the
* top left of the Blockly div.
* @param {number} y Vertical translation, in pixel units relative to the
* top left of the Blockly div.
*/
WorkspaceSvg.prototype.translate = function(x, y) {
if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
this.workspaceDragSurface_.translateSurface(x, y);
} else {
const translation = 'translate(' + x + ',' + y + ') ' +
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
}
// Now update the block drag surface if we're using one.
if (this.blockDragSurface_) {
this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
}
// And update the grid if we're using one.
if (this.grid_) {
this.grid_.moveTo(x, y);
}
this.maybeFireViewportChangeEvent();
};
/**
* Called at the end of a workspace drag to take the contents
* out of the drag surface and put them back into the workspace SVG.
* Does nothing if the workspace drag surface is not enabled.
* @package
*/
WorkspaceSvg.prototype.resetDragSurface = function() {
// Don't do anything if we aren't using a drag surface.
if (!this.useWorkspaceDragSurface_) {
return;
}
this.isDragSurfaceActive_ = false;
const trans = this.workspaceDragSurface_.getSurfaceTranslation();
this.workspaceDragSurface_.clearAndHide(this.svgGroup_);
const translation = 'translate(' + trans.x + ',' + trans.y + ') ' +
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
};
/**
* Called at the beginning of a workspace drag to move contents of
* the workspace to the drag surface.
* Does nothing if the drag surface is not enabled.
* @package
*/
WorkspaceSvg.prototype.setupDragSurface = function() {
// Don't do anything if we aren't using a drag surface.
if (!this.useWorkspaceDragSurface_) {
return;
}
// This can happen if the user starts a drag, mouses up outside of the
// document where the mouseup listener is registered (e.g. outside of an
// iframe) and then moves the mouse back in the workspace. On mobile and ff,
// we get the mouseup outside the frame. On chrome and safari desktop we do
// not.
if (this.isDragSurfaceActive_) {
return;
}
this.isDragSurfaceActive_ = true;
// Figure out where we want to put the canvas back. The order
// in the is important because things are layered.
const previousElement =
/** @type {Element} */ (this.svgBlockCanvas_.previousSibling);
const width = parseInt(this.getParentSvg().getAttribute('width'), 10);
const height = parseInt(this.getParentSvg().getAttribute('height'), 10);
const coord = svgMath.getRelativeXY(this.getCanvas());
this.workspaceDragSurface_.setContentsAndShow(
this.getCanvas(), this.getBubbleCanvas(), previousElement, width, height,
this.scale);
this.workspaceDragSurface_.translateSurface(coord.x, coord.y);
};
/**
* Gets the drag surface blocks are moved to when a drag is started.
* @return {?BlockDragSurfaceSvg} This workspace's block drag surface,
* if one is in use.
* @package
*/
WorkspaceSvg.prototype.getBlockDragSurface = function() {
return this.blockDragSurface_;
};
/**
* Returns the horizontal offset of the workspace.
* Intended for LTR/RTL compatibility in XML.
* @return {number} Width.
*/
WorkspaceSvg.prototype.getWidth = function() {
const metrics = this.getMetrics();
return metrics ? metrics.viewWidth / this.scale : 0;
};
/**
* Toggles the visibility of the workspace.
* Currently only intended for main workspace.
* @param {boolean} isVisible True if workspace should be visible.
*/
WorkspaceSvg.prototype.setVisible = function(isVisible) {
this.isVisible_ = isVisible;
if (!this.svgGroup_) {
return;
}
// Tell the scrollbar whether its container is visible so it can
// tell when to hide itself.
if (this.scrollbar) {
this.scrollbar.setContainerVisible(isVisible);
}
// Tell the flyout whether its container is visible so it can
// tell when to hide itself.
if (this.getFlyout()) {
this.getFlyout().setContainerVisible(isVisible);
}
this.getParentSvg().style.display = isVisible ? 'block' : 'none';
if (this.toolbox_) {
// Currently does not support toolboxes in mutators.
this.toolbox_.setVisible(isVisible);
}
if (isVisible) {
const blocks = this.getAllBlocks(false);
// Tell each block on the workspace to mark its fields as dirty.
for (let i = blocks.length - 1; i >= 0; i--) {
blocks[i].markDirty();
}
this.render();
if (this.toolbox_) {
this.toolbox_.position();
}
} else {
this.hideChaff(true);
}
};
/**
* Render all blocks in workspace.
*/
WorkspaceSvg.prototype.render = function() {
// Generate list of all blocks.
const blocks = this.getAllBlocks(false);
// Render each block.
for (let i = blocks.length - 1; i >= 0; i--) {
blocks[i].render(false);
}
if (this.currentGesture_) {
const imList = this.currentGesture_.getInsertionMarkers();
for (let i = 0; i < imList.length; i++) {
imList[i].render(false);
}
}
this.markerManager_.updateMarkers();
};
/**
* Highlight or unhighlight a block in the workspace. Block highlighting is
* often used to visually mark blocks currently being executed.
* @param {?string} id ID of block to highlight/unhighlight,
* or null for no block (used to unhighlight all blocks).
* @param {boolean=} opt_state If undefined, highlight specified block and
* automatically unhighlight all others. If true or false, manually
* highlight/unhighlight the specified block.
*/
WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) {
if (opt_state === undefined) {
// Unhighlight all blocks.
for (let i = 0, block; (block = this.highlightedBlocks_[i]); i++) {
block.setHighlighted(false);
}
this.highlightedBlocks_.length = 0;
}
// Highlight/unhighlight the specified block.
const block = id ? this.getBlockById(id) : null;
if (block) {
const state = (opt_state === undefined) || opt_state;
// Using Set here would be great, but at the cost of IE10 support.
if (!state) {
arrayUtils.removeElem(this.highlightedBlocks_, block);
} else if (this.highlightedBlocks_.indexOf(block) === -1) {
this.highlightedBlocks_.push(block);
}
block.setHighlighted(state);
}
};
/**
* Pastes the provided block or workspace comment onto the workspace.
* Does not check whether there is remaining capacity for the object, that
* should be done before calling this method.
* @param {!Object|!Element|!DocumentFragment} state The representation of the
* thing to paste.
*/
WorkspaceSvg.prototype.paste = function(state) {
if (!this.rendered || !state['type'] && !state.tagName) {
return;
}
if (this.currentGesture_) {
this.currentGesture_.cancel(); // Dragging while pasting? No.
}
// Checks if this is JSON. JSON has a type property, while elements don't.
if (state['type']) {
this.pasteBlock_(null, /** @type {!blocks.State} */ (state));
} else {
const xmlBlock = /** @type {!Element} */ (state);
if (xmlBlock.tagName.toLowerCase() === 'comment') {
this.pasteWorkspaceComment_(xmlBlock);
} else {
this.pasteBlock_(xmlBlock, null);
}
}
};
/**
* Paste the provided block onto the workspace.
* @param {?Element} xmlBlock XML block element.
* @param {?blocks.State} jsonBlock JSON block
* representation.
* @private
*/
WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock, jsonBlock) {
eventUtils.disable();
let block;
try {
let blockX = 0;
let blockY = 0;
if (xmlBlock) {
block = Xml.domToBlock(xmlBlock, this);
blockX = parseInt(xmlBlock.getAttribute('x'), 10);
if (this.RTL) {
blockX = -blockX;
}
blockY = parseInt(xmlBlock.getAttribute('y'), 10);
} else if (jsonBlock) {
block = blocks.append(jsonBlock, this);
blockX = jsonBlock['x'] || 10;
if (this.RTL) {
blockX = this.getWidth() - blockX;
}
blockY = jsonBlock['y'] || 10;
}
// Move the duplicate to original position.
if (!isNaN(blockX) && !isNaN(blockY)) {
// Offset block until not clobbering another block and not in connection
// distance with neighbouring blocks.
let collide;
do {
collide = false;
const allBlocks = this.getAllBlocks(false);
for (let i = 0, otherBlock; (otherBlock = allBlocks[i]); i++) {
const otherXY = otherBlock.getRelativeToSurfaceXY();
if (Math.abs(blockX - otherXY.x) <= 1 &&
Math.abs(blockY - otherXY.y) <= 1) {
collide = true;
break;
}
}
if (!collide) {
// Check for blocks in snap range to any of its connections.
const connections = block.getConnections_(false);
for (let i = 0, connection; (connection = connections[i]); i++) {
const neighbour = connection.closest(
internalConstants.SNAP_RADIUS, new Coordinate(blockX, blockY));
if (neighbour.connection) {
collide = true;
break;
}
}
}
if (collide) {
if (this.RTL) {