text: /**
   * @license
   * Visual Blocks Editor
   *
   * Copyright 2012 Google Inc.
   * https://developers.google.com/blockly/
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  /**
   * @fileoverview Methods for graphically rendering a block as SVG.
   * @author fraser@google.com (Neil Fraser)
   */
  'use strict';
  
  goog.provide('Blockly.BlockSvg');
  
  goog.require('Blockly.Block');
  goog.require('Blockly.BlockAnimations');
  goog.require('Blockly.ContextMenu');
  goog.require('Blockly.Events.Ui');
  goog.require('Blockly.Events.BlockMove');
  goog.require('Blockly.Grid');
  goog.require('Blockly.RenderedConnection');
  goog.require('Blockly.Tooltip');
  goog.require('Blockly.Touch');
  goog.require('Blockly.utils');
  
  goog.require('goog.color');
  goog.require('goog.math.Coordinate');
  
  
  /**
   * Class for a block's SVG representation.
   * Not normally called directly, workspace.newBlock() is preferred.
   * @param {!Blockly.Workspace} workspace The block's workspace.
   * @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.
   * @extends {Blockly.Block}
   * @constructor
   */
  Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
    // Create core elements for the block.
    /**
     * @type {SVGElement}
     * @private
     */
    this.svgGroup_ = Blockly.utils.createSvgElement('g', {}, null);
    this.svgGroup_.translate_ = '';
  
    /**
     * @type {SVGElement}
     * @private
     */
    this.svgPathDark_ = Blockly.utils.createSvgElement('path',
        {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'},
        this.svgGroup_);
  
    /**
     * @type {SVGElement}
     * @private
     */
    this.svgPath_ = Blockly.utils.createSvgElement('path',
        {'class': 'blocklyPath', 'aria-label': prototypeName.replace(/_/g, ' ')},
        this.svgGroup_);
  
    /**
     * @type {SVGElement}
     * @private
     */
    this.svgPathLight_ = Blockly.utils.createSvgElement('path',
        {'class': 'blocklyPathLight'}, this.svgGroup_);
    this.svgPath_.tooltip = this;
  
    /** @type {boolean} */
    this.rendered = false;
  
    /**
     * Whether to move the block to the drag surface when it is dragged.
     * True if it should move, false if it should be translated directly.
     * @type {boolean}
     * @private
     */
    this.useDragSurface_ =
        Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_;
  
    Blockly.Tooltip.bindMouseEvents(this.svgPath_);
    Blockly.BlockSvg.superClass_.constructor.call(this,
        workspace, prototypeName, opt_id);
  
    // Expose this block's ID on its top-level SVG group.
    if (this.svgGroup_.dataset) {
      this.svgGroup_.dataset.id = this.id;
    }
  };
  goog.inherits(Blockly.BlockSvg, Blockly.Block);
  
  /**
   * Height of this block, not including any statement blocks above or below.
   * Height is in workspace units.
   */
  Blockly.BlockSvg.prototype.height = 0;
  /**
   * Width of this block, including any connected value blocks.
   * Width is in workspace units.
   */
  Blockly.BlockSvg.prototype.width = 0;
  
  /**
   * Original location of block being dragged.
   * @type {goog.math.Coordinate}
   * @private
   */
  Blockly.BlockSvg.prototype.dragStartXY_ = null;
  
  /**
   * Map from IDs for warnings text to PIDs of functions to apply them.
   * Used to be able to maintain multiple warnings.
   * @type {Object.<string, number>}
   * @private
   */
  Blockly.BlockSvg.prototype.warningTextDb_ = null;
  
  /**
   * Constant for identifying rows that are to be rendered inline.
   * Don't collide with Blockly.INPUT_VALUE and friends.
   * @const
   */
  Blockly.BlockSvg.INLINE = -1;
  
  /**
   * ID to give the "collapsed warnings" warning. Allows us to remove the
   * "collapsed warnings" warning without removing any warnings that belong to
   * the block.
   * @type {string}
   * @const
   */
  Blockly.BlockSvg.COLLAPSED_WARNING_ID = 'TEMP_COLLAPSED_WARNING_';
  
  /**
   * Create and initialize the SVG representation of the block.
   * May be called more than once.
   */
  Blockly.BlockSvg.prototype.initSvg = function() {
    if (!this.workspace.rendered) {
      throw TypeError('Workspace is headless.');
    }
    for (var i = 0, input; input = this.inputList[i]; i++) {
      input.init();
    }
    var icons = this.getIcons();
    for (var i = 0; i < icons.length; i++) {
      icons[i].createIcon();
    }
    this.updateColour();
    this.updateMovable();
    if (!this.workspace.options.readOnly && !this.eventsInit_) {
      Blockly.bindEventWithChecks_(
          this.getSvgRoot(), 'mousedown', this, this.onMouseDown_);
    }
    this.eventsInit_ = true;
  
    if (!this.getSvgRoot().parentNode) {
      this.workspace.getCanvas().appendChild(this.getSvgRoot());
    }
  };
  
  Blockly.BlockSvg.prototype.makeAriaLabel = function() {
    var lab = '';
    var workspace = this.workspace;
    for (var input of this.inputList) {
      // space out the text from each field in fieldRow
      lab += input.fieldRow.reduce((str, n) => str + n.getText() + ' ', '');
  
      if (input.connection && input.connection.targetConnection) {
        if (input.type === 1) {
          var targetBlock = input.connection.targetConnection.getSourceBlock();
          lab += targetBlock.makeAriaLabel() + ' ';
        }
      }
    }
  
    return lab.trim();
  }
  
  // NOTE: I ADDED THIS, NOT PART OF ORIGINAL LIBRARY
  /**
  *  Recursively updates aria-label for block and its parents
  *  @param {Blockly.Block} block The block to get text from.
  */
  Blockly.BlockSvg.prototype.updateLabel = function(recurse = true) {
    // TODO: don't use privately marked variable
    if (this.svgPath_) {
      this.svgPath_.setAttribute('aria-label', this.makeAriaLabel());
      if (recurse && this.getParent()) {
        this.getParent().updateLabel();
      }
    }
  }
  
  /**
   * Select this block.  Highlight it visually.
   */
  Blockly.BlockSvg.prototype.select = function() {
    if (this.isShadow() && this.getParent()) {
      // Shadow blocks should not be selected.
      this.getParent().select();
      return;
    }
    if (Blockly.selected == this) {
      return;
    }
    var oldId = null;
    if (Blockly.selected) {
      oldId = Blockly.selected.id;
      // Unselect any previously selected block.
      Blockly.Events.disable();
      try {
        Blockly.selected.unselect();
      } finally {
        Blockly.Events.enable();
      }
    }
    var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id);
    event.workspaceId = this.workspace.id;
    Blockly.Events.fire(event);
    Blockly.selected = this;
    this.addSelect();
  };
  
  /**
   * Unselect this block.  Remove its highlighting.
   */
  Blockly.BlockSvg.prototype.unselect = function() {
    if (Blockly.selected != this) {
      return;
    }
    var event = new Blockly.Events.Ui(null, 'selected', this.id, null);
    event.workspaceId = this.workspace.id;
    Blockly.Events.fire(event);
    Blockly.selected = null;
    this.removeSelect();
  };
  
  /**
   * Block's mutator icon (if any).
   * @type {Blockly.Mutator}
   */
  Blockly.BlockSvg.prototype.mutator = null;
  
  /**
   * Block's comment icon (if any).
   * @type {Blockly.Comment}
   */
  Blockly.BlockSvg.prototype.comment = null;
  
  /**
   * Block's warning icon (if any).
   * @type {Blockly.Warning}
   */
  Blockly.BlockSvg.prototype.warning = null;
  
  /**
   * Returns a list of mutator, comment, and warning icons.
   * @return {!Array} List of icons.
   */
  Blockly.BlockSvg.prototype.getIcons = function() {
    var icons = [];
    if (this.mutator) {
      icons.push(this.mutator);
    }
    if (this.comment) {
      icons.push(this.comment);
    }
    if (this.warning) {
      icons.push(this.warning);
    }
    return icons;
  };
  
  /**
   * Set parent of this block to be a new block or null.
   * @param {Blockly.BlockSvg} newParent New parent block.
   */
  Blockly.BlockSvg.prototype.setParent = function(newParent) {
    var oldParent = this.parentBlock_;
    if (newParent == oldParent) {
      return;
    }
  
    Blockly.Field.startCache();
    Blockly.BlockSvg.superClass_.setParent.call(this, newParent);
    Blockly.Field.stopCache();
  
    var svgRoot = this.getSvgRoot();
  
    // Bail early if workspace is clearing, or we aren't rendered.
    // We won't need to reattach ourselves anywhere.
    if (this.workspace.isClearing || !svgRoot) {
      return;
    }
  
    var oldXY = this.getRelativeToSurfaceXY();
    if (newParent) {
      newParent.getSvgRoot().appendChild(svgRoot);
      var newXY = this.getRelativeToSurfaceXY();
      // Move the connections to match the child's new position.
      this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y);
    }
    // If we are losing a parent, we want to move our DOM element to the
    // root of the workspace.
    else if (oldParent) {
      this.workspace.getCanvas().appendChild(svgRoot);
      this.translate(oldXY.x, oldXY.y);
    }
  };
  
  /**
   * Return the coordinates of the top-left corner of this block relative to the
   * drawing surface's origin (0,0), in workspace units.
   * If the block is on the workspace, (0, 0) is the origin of the workspace
   * coordinate system.
   * This does not change with workspace scale.
   * @return {!goog.math.Coordinate} Object with .x and .y properties in
   *     workspace coordinates.
   */
  Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
    var x = 0;
    var y = 0;
  
    var dragSurfaceGroup = this.useDragSurface_ ?
        this.workspace.blockDragSurface_.getGroup() : null;
  
    var element = this.getSvgRoot();
    if (element) {
      do {
        // Loop through this block and every parent.
        var xy = Blockly.utils.getRelativeXY(element);
        x += xy.x;
        y += xy.y;
        // If this element is the current element on the drag surface, include
        // the translation of the drag surface itself.
        if (this.useDragSurface_ &&
            this.workspace.blockDragSurface_.getCurrentBlock() == element) {
          var surfaceTranslation =
              this.workspace.blockDragSurface_.getSurfaceTranslation();
          x += surfaceTranslation.x;
          y += surfaceTranslation.y;
        }
        element = element.parentNode;
      } while (element && element != this.workspace.getCanvas() &&
          element != dragSurfaceGroup);
    }
    return new goog.math.Coordinate(x, y);
  };
  
  /**
   * Move a block by a relative offset.
   * @param {number} dx Horizontal offset in workspace units.
   * @param {number} dy Vertical offset in workspace units.
   */
  Blockly.BlockSvg.prototype.moveBy = function(dx, dy) {
    if (this.parentBlock_) {
      throw Error('Block has parent.');
    }
    var eventsEnabled = Blockly.Events.isEnabled();
    if (eventsEnabled) {
      var event = new Blockly.Events.BlockMove(this);
    }
    var xy = this.getRelativeToSurfaceXY();
    this.translate(xy.x + dx, xy.y + dy);
    this.moveConnections_(dx, dy);
    if (eventsEnabled) {
      event.recordNew();
      Blockly.Events.fire(event);
    }
    this.workspace.resizeContents();
  };
  
  /**
   * Move a block to a position.
   * @param {goog.math.Coordinate} xy The position to move to in workspace units.
   */
  Blockly.BlockSvg.prototype.moveTo = function(xy) {
    if (this.parentBlock_) {
      throw Error('Block has parent.');
    }
    var eventsEnabled = Blockly.Events.isEnabled();
    if (eventsEnabled) {
      var event = new Blockly.Events.BlockMove(this);
    }
    var curXY = this.getRelativeToSurfaceXY();
    this.translate(xy.x, xy.y);
    this.moveConnections_(xy.x - curXY.x, xy.y - curXY.y);
    if (eventsEnabled) {
      event.recordNew();
      Blockly.Events.fire(event);
    }
    this.workspace.resizeContents();
  };
  
  /**
   * Transforms a block by setting the translation on the transform attribute
   * of the block's SVG.
   * @param {number} x The x coordinate of the translation in workspace units.
   * @param {number} y The y coordinate of the translation in workspace units.
   */
  Blockly.BlockSvg.prototype.translate = function(x, y) {
    this.getSvgRoot().setAttribute('transform',
        'translate(' + x + ',' + y + ')');
  };
  
  /**
   * Move this block to its workspace's drag surface, accounting for positioning.
   * Generally should be called at the same time as setDragging_(true).
   * Does nothing if useDragSurface_ is false.
   * @private
   */
  Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
    if (!this.useDragSurface_) {
      return;
    }
    // The translation for drag surface blocks,
    // is equal to the current relative-to-surface position,
    // to keep the position in sync as it move on/off the surface.
    // This is in workspace coordinates.
    var xy = this.getRelativeToSurfaceXY();
    this.clearTransformAttributes_();
    this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y);
    // Execute the move on the top-level SVG component
    this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot());
  };
  
  /**
   * Move this block back to the workspace block canvas.
   * Generally should be called at the same time as setDragging_(false).
   * Does nothing if useDragSurface_ is false.
   * @param {!goog.math.Coordinate} newXY The position the block should take on
   *     on the workspace canvas, in workspace coordinates.
   * @private
   */
  Blockly.BlockSvg.prototype.moveOffDragSurface_ = function(newXY) {
    if (!this.useDragSurface_) {
      return;
    }
    // Translate to current position, turning off 3d.
    this.translate(newXY.x, newXY.y);
    this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
  };
  
  /**
   * Move this block during a drag, taking into account whether we are using a
   * drag surface to translate blocks.
   * This block must be a top-level block.
   * @param {!goog.math.Coordinate} newLoc The location to translate to, in
   *     workspace coordinates.
   * @package
   */
  Blockly.BlockSvg.prototype.moveDuringDrag = function(newLoc) {
    if (this.useDragSurface_) {
      this.workspace.blockDragSurface_.translateSurface(newLoc.x, newLoc.y);
    } else {
      this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')';
      this.svgGroup_.setAttribute('transform',
          this.svgGroup_.translate_ + this.svgGroup_.skew_);
    }
  };
  
  /**
   * Clear the block of transform="..." attributes.
   * Used when the block is switching from 3d to 2d transform or vice versa.
   * @private
   */
  Blockly.BlockSvg.prototype.clearTransformAttributes_ = function() {
    Blockly.utils.removeAttribute(this.getSvgRoot(), 'transform');
  };
  
  /**
   * Snap this block to the nearest grid point.
   */
  Blockly.BlockSvg.prototype.snapToGrid = function() {
    if (!this.workspace) {
      return;  // Deleted block.
    }
    if (this.workspace.isDragging()) {
      return;  // Don't bump blocks during a drag.
    }
    if (this.getParent()) {
      return;  // Only snap top-level blocks.
    }
    if (this.isInFlyout) {
      return;  // Don't move blocks around in a flyout.
    }
    var grid = this.workspace.getGrid();
    if (!grid || !grid.shouldSnap()) {
      return;  // Config says no snapping.
    }
    var spacing = grid.getSpacing();
    var half = spacing / 2;
    var xy = this.getRelativeToSurfaceXY();
    var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x;
    var dy = Math.round((xy.y - half) / spacing) * spacing + half - xy.y;
    dx = Math.round(dx);
    dy = Math.round(dy);
    if (dx != 0 || dy != 0) {
      this.moveBy(dx, dy);
    }
  };
  
  /**
   * Returns the coordinates of a bounding box describing the dimensions of this
   * block and any blocks stacked below it.
   * Coordinate system: workspace coordinates.
   * @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
   *    Object with top left and bottom right coordinates of the bounding box.
   */
  Blockly.BlockSvg.prototype.getBoundingRectangle = function() {
    var blockXY = this.getRelativeToSurfaceXY(this);
    var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
    var blockBounds = this.getHeightWidth();
    var topLeft;
    var bottomRight;
    if (this.RTL) {
      // Width has the tab built into it already so subtract it here.
      topLeft = new goog.math.Coordinate(blockXY.x - (blockBounds.width - tab),
          blockXY.y);
      // Add the width of the tab/puzzle piece knob to the x coordinate
      // since X is the corner of the rectangle, not the whole puzzle piece.
      bottomRight = new goog.math.Coordinate(blockXY.x + tab,
          blockXY.y + blockBounds.height);
    } else {
      // Subtract the width of the tab/puzzle piece knob to the x coordinate
      // since X is the corner of the rectangle, not the whole puzzle piece.
      topLeft = new goog.math.Coordinate(blockXY.x - tab, blockXY.y);
      // Width has the tab built into it already so subtract it here.
      bottomRight = new goog.math.Coordinate(blockXY.x + blockBounds.width - tab,
          blockXY.y + blockBounds.height);
    }
    return {topLeft: topLeft, bottomRight: bottomRight};
  };
  
  /**
   * Set whether the block is collapsed or not.
   * @param {boolean} collapsed True if collapsed.
   */
  Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) {
    if (this.collapsed_ == collapsed) {
      return;
    }
    var renderList = [];
    // Show/hide the inputs.
    for (var i = 0, input; input = this.inputList[i]; i++) {
      renderList.push.apply(renderList, input.setVisible(!collapsed));
    }
  
    var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
    if (collapsed) {
      var icons = this.getIcons();
      for (var i = 0; i < icons.length; i++) {
        icons[i].setVisible(false);
      }
      var text = this.toString(Blockly.COLLAPSE_CHARS);
      this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init();
  
      // Add any warnings on enclosed blocks to this block.
      var descendants = this.getDescendants(true);
      var nextBlock = this.getNextBlock();
      if (nextBlock) {
        var index = descendants.indexOf(nextBlock);
        descendants.splice(index, descendants.length - index);
      }
      for (var i = 1, block; block = descendants[i]; i++) {
        if (block.warning) {
          this.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'],
              Blockly.BlockSvg.COLLAPSED_WARNING_ID);
          break;
        }
      }
    } else {
      this.removeInput(COLLAPSED_INPUT_NAME);
      // Clear any warnings inherited from enclosed blocks.
      if (this.warning) {
        this.warning.setText('', Blockly.BlockSvg.COLLAPSED_WARNING_ID);
        if (!Object.keys(this.warning.text_).length) {
          this.setWarningText(null);
        }
      }
    }
    Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed);
  
    if (!renderList.length) {
      // No child blocks, just render this block.
      renderList[0] = this;
    }
    if (this.rendered) {
      for (var i = 0, block; block = renderList[i]; i++) {
        block.render();
      }
      // Don't bump neighbours.
      // Although bumping neighbours would make sense, users often collapse
      // all their functions and store them next to each other.  Expanding and
      // bumping causes all their definitions to go out of alignment.
    }
  };
  
  /**
   * Open the next (or previous) FieldTextInput.
   * @param {Blockly.Field|Blockly.Block} start Current location.
   * @param {boolean} forward If true go forward, otherwise backward.
   */
  Blockly.BlockSvg.prototype.tab = function(start, forward) {
    var list = this.createTabList_();
    var i = list.indexOf(start);
    if (i == -1) {
      // No start location, start at the beginning or end.
      i = forward ? -1 : list.length;
    }
    var target = list[forward ? i + 1 : i - 1];
    if (!target) {
      // Ran off of list.
      var parent = this.getParent();
      if (parent) {
        parent.tab(this, forward);
      }
    } else if (target instanceof Blockly.Field) {
      target.showEditor_();
    } else {
      target.tab(null, forward);
    }
  };
  
  /**
   * Create an ordered list of all text fields and connected inputs.
   * @return {!Array.<!Blockly.FieldTextInput|!Blockly.Input>} The ordered list.
   * @private
   */
  Blockly.BlockSvg.prototype.createTabList_ = function() {
    // This function need not be efficient since it runs once on a keypress.
    var list = [];
    for (var i = 0, input; input = this.inputList[i]; i++) {
      for (var j = 0, field; field = input.fieldRow[j]; j++) {
        if (field instanceof Blockly.FieldTextInput) {
          // TODO (#1276): Also support dropdown fields.
          list.push(field);
        }
      }
      if (input.connection) {
        var block = input.connection.targetBlock();
        if (block) {
          list.push(block);
        }
      }
    }
    return list;
  };
  
  /**
   * Handle a mouse-down on an SVG block.
   * @param {!Event} e Mouse down event or touch start event.
   * @private
   */
  Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
    var gesture = this.workspace && this.workspace.getGesture(e);
    if (gesture) {
      gesture.handleBlockStart(e, this);
    }
  };
  
  /**
   * Load the block's help page in a new window.
   * @private
   */
  Blockly.BlockSvg.prototype.showHelp_ = function() {
    var url = (typeof this.helpUrl == 'function') ? this.helpUrl() : this.helpUrl;
    if (url) {
      window.open(url);
    }
  };
  
  /**
   * Show the context menu for this block.
   * @param {!Event} e Mouse event.
   * @private
   */
  Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
    if (this.workspace.options.readOnly || !this.contextMenu) {
      return;
    }
    // Save the current block in a variable for use in closures.
    var block = this;
    var menuOptions = [];
  
    if (!this.isInFlyout) {
      if (this.isDeletable() && this.isMovable()) {
        menuOptions.push(Blockly.ContextMenu.blockDuplicateOption(block));
      }
  
      if (this.workspace.options.comments && !this.collapsed_ &&
          this.isEditable()) {
        menuOptions.push(Blockly.ContextMenu.blockCommentOption(block));
      }
  
      if (this.isMovable()) {
        if (!this.collapsed_) {
          // Option to make block inline.
          for (var i = 1; i < this.inputList.length; i++) {
            if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT &&
                this.inputList[i].type != Blockly.NEXT_STATEMENT) {
              // Only display this option if there are two value or dummy inputs
              // next to each other.
              var inlineOption = {enabled: true};
              var isInline = this.getInputsInline();
              inlineOption.text = isInline ?
                  Blockly.Msg['EXTERNAL_INPUTS'] : Blockly.Msg['INLINE_INPUTS'];
              inlineOption.callback = function() {
                block.setInputsInline(!isInline);
              };
              menuOptions.push(inlineOption);
              break;
            }
          }
          // Option to collapse block
          if (this.workspace.options.collapse) {
            var collapseOption = {enabled: true};
            collapseOption.text = Blockly.Msg['COLLAPSE_BLOCK'];
            collapseOption.callback = function() {
              block.setCollapsed(true);
            };
            menuOptions.push(collapseOption);
          }
        } else {
          // Option to expand block.
          if (this.workspace.options.collapse) {
            var expandOption = {enabled: true};
            expandOption.text = Blockly.Msg['EXPAND_BLOCK'];
            expandOption.callback = function() {
              block.setCollapsed(false);
            };
            menuOptions.push(expandOption);
          }
        }
      }
  
      if (this.workspace.options.disable && this.isEditable()) {
        // Option to disable/enable block.
        var disableOption = {
          text: this.disabled ?
              Blockly.Msg['ENABLE_BLOCK'] : Blockly.Msg['DISABLE_BLOCK'],
          enabled: !this.getInheritedDisabled(),
          callback: function() {
            var group = Blockly.Events.getGroup();
            if (!group) {
              Blockly.Events.setGroup(true);
            }
            block.setEnabled(!block.isEnabled());
            if (!group) {
              Blockly.Events.setGroup(false);
            }
          }
        };
        menuOptions.push(disableOption);
      }
  
      if (this.isDeletable()) {
        menuOptions.push(Blockly.ContextMenu.blockDeleteOption(block));
      }
    }
  
    menuOptions.push(Blockly.ContextMenu.blockHelpOption(block));
  
    // Allow the block to add or modify menuOptions.
    if (this.customContextMenu) {
      this.customContextMenu(menuOptions);
    }
  
    Blockly.ContextMenu.show(e, menuOptions, this.RTL);
    Blockly.ContextMenu.currentBlock = this;
  };
  
  /**
   * Move the connections for this block and all blocks attached under it.
   * Also update any attached bubbles.
   * @param {number} dx Horizontal offset from current location, in workspace
   *     units.
   * @param {number} dy Vertical offset from current location, in workspace
   *     units.
   * @private
   */
  Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) {
    if (!this.rendered) {
      // Rendering is required to lay out the blocks.
      // This is probably an invisible block attached to a collapsed block.
      return;
    }
    var myConnections = this.getConnections_(false);
    for (var i = 0; i < myConnections.length; i++) {
      myConnections[i].moveBy(dx, dy);
    }
    var icons = this.getIcons();
    for (var i = 0; i < icons.length; i++) {
      icons[i].computeIconLocation();
    }
  
    // Recurse through all blocks attached under this one.
    for (var i = 0; i < this.childBlocks_.length; i++) {
      this.childBlocks_[i].moveConnections_(dx, dy);
    }
  };
  
  /**
   * Recursively adds or removes the dragging class to this node and its children.
   * @param {boolean} adding True if adding, false if removing.
   * @package
   */
  Blockly.BlockSvg.prototype.setDragging = function(adding) {
    if (adding) {
      var group = this.getSvgRoot();
      group.translate_ = '';
      group.skew_ = '';
      Blockly.draggingConnections_ =
          Blockly.draggingConnections_.concat(this.getConnections_(true));
      Blockly.utils.addClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
    } else {
      Blockly.draggingConnections_ = [];
      Blockly.utils.removeClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
    }
    // Recurse through all blocks attached under this one.
    for (var i = 0; i < this.childBlocks_.length; i++) {
      this.childBlocks_[i].setDragging(adding);
    }
  };
  
  /**
   * Add or remove the UI indicating if this block is movable or not.
   */
  Blockly.BlockSvg.prototype.updateMovable = function() {
    if (this.isMovable()) {
      Blockly.utils.addClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
    } else {
      Blockly.utils.removeClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
    }
  };
  
  /**
   * Set whether this block is movable or not.
   * @param {boolean} movable True if movable.
   */
  Blockly.BlockSvg.prototype.setMovable = function(movable) {
    Blockly.BlockSvg.superClass_.setMovable.call(this, movable);
    this.updateMovable();
  };
  
  /**
   * Set whether this block is editable or not.
   * @param {boolean} editable True if editable.
   */
  Blockly.BlockSvg.prototype.setEditable = function(editable) {
    Blockly.BlockSvg.superClass_.setEditable.call(this, editable);
    var icons = this.getIcons();
    for (var i = 0; i < icons.length; i++) {
      icons[i].updateEditable();
    }
  };
  
  /**
   * Set whether this block is a shadow block or not.
   * @param {boolean} shadow True if a shadow.
   */
  Blockly.BlockSvg.prototype.setShadow = function(shadow) {
    Blockly.BlockSvg.superClass_.setShadow.call(this, shadow);
    this.updateColour();
  };
  
  /**
   * Set whether this block is an insertion marker block or not.
   * Once set this cannot be unset.
   * @param {boolean} insertionMarker True if an insertion marker.
   * @package
   */
  Blockly.BlockSvg.prototype.setInsertionMarker = function(insertionMarker) {
    if (this.isInsertionMarker_ == insertionMarker) {
      return;  // No change.
    }
    this.isInsertionMarker_ = insertionMarker;
    if (this.isInsertionMarker_) {
      this.setColour(Blockly.INSERTION_MARKER_COLOUR);
      Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
          'blocklyInsertionMarker');
    }
  };
  
  /**
   * Return the root node of the SVG or null if none exists.
   * @return {Element} The root SVG node (probably a group).
   */
  Blockly.BlockSvg.prototype.getSvgRoot = function() {
    return this.svgGroup_;
  };
  
  /**
   * Dispose of this block.
   * @param {boolean} healStack If true, then try to heal any gap by connecting
   *     the next statement with the previous statement.  Otherwise, dispose of
   *     all children of this block.
   * @param {boolean} animate If true, show a disposal animation and sound.
   */
  Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
    if (!this.workspace) {
      // The block has already been deleted.
      return;
    }
    Blockly.Tooltip.hide();
    Blockly.Field.startCache();
    // Save the block's workspace temporarily so we can resize the
    // contents once the block is disposed.
    var blockWorkspace = this.workspace;
    // If this block is being dragged, unlink the mouse events.
    if (Blockly.selected == this) {
      this.unselect();
      this.workspace.cancelCurrentGesture();
    }
    // If this block has a context menu open, close it.
    if (Blockly.ContextMenu.currentBlock == this) {
      Blockly.ContextMenu.hide();
    }
  
    if (animate && this.rendered) {
      this.unplug(healStack);
      Blockly.BlockAnimations.disposeUiEffect(this);
    }
    // Stop rerendering.
    this.rendered = false;
  
    // Clear pending warnings.
    if (this.warningTextDb_) {
      for (var n in this.warningTextDb_) {
        clearTimeout(this.warningTextDb_[n]);
      }
      this.warningTextDb_ = null;
    }
  
    // If the block is rendered we need to record the event before disposing of
    // the icons to prevent losing information.
    // TODO (#1969): Remove event generation/firing once comments are fixed.
    this.unplug(healStack);
    var deleteEvent;
    if (Blockly.Events.isEnabled()) {
      deleteEvent = new Blockly.Events.BlockDelete(this);
    }
    Blockly.Events.disable();
    try {
      var icons = this.getIcons();
      for (var i = 0; i < icons.length; i++) {
        icons[i].dispose();
      }
      // TODO (#1969): Move out of disable block once comments are fixed.
      Blockly.BlockSvg.superClass_.dispose.call(this, healStack);
    } finally {
      Blockly.Events.enable();
    }
    if (Blockly.Events.isEnabled() && deleteEvent) {
      Blockly.Events.fire(deleteEvent);
    }
  
    Blockly.utils.removeNode(this.svgGroup_);
    blockWorkspace.resizeContents();
    // Sever JavaScript to DOM connections.
    this.svgGroup_ = null;
    this.svgPath_ = null;
    this.svgPathLight_ = null;
    this.svgPathDark_ = null;
    Blockly.Field.stopCache();
  };
  
  /**
   * Change the colour of a block.
   */
  Blockly.BlockSvg.prototype.updateColour = function() {
    if (this.disabled) {
      // Disabled blocks don't have colour.
      return;
    }
    var hexColour = this.getColour();
    var colourSecondary = this.getColourSecondary();
    var colourTertiary = this.getColourTertiary();
    var rgb = goog.color.hexToRgb(hexColour);
  
    if (this.isShadow()) {
      hexColour = this.setShadowColour_(rgb, colourSecondary);
    } else {
      this.setBorderColour_(rgb, colourTertiary);
    }
    this.svgPath_.setAttribute('fill', hexColour);
  
    var icons = this.getIcons();
    for (var i = 0; i < icons.length; i++) {
      icons[i].updateColour();
    }
  
    // Bump every dropdown to change its colour.
    // TODO (#1456)
    for (var x = 0, input; input = this.inputList[x]; x++) {
      for (var y = 0, field; field = input.fieldRow[y]; y++) {
        field.forceRerender();
      }
    }
  };
  
  /**
   * Sets the colour of the border.
   * Removes the light and dark paths if a tertiary colour is defined.
   * @param {!string} rgb Colour of the block.
   * @param {?string} colourTertiary Colour of the border.
   */
  Blockly.BlockSvg.prototype.setBorderColour_ = function(rgb, colourTertiary) {
    if (colourTertiary) {
      this.svgPathLight_.setAttribute('stroke', 'none');
      this.svgPathDark_.setAttribute('fill', 'none');
      this.svgPath_.setAttribute('stroke', colourTertiary);
    } else {
      this.svgPathLight_.style.display = '';
      var hexLight = goog.color.rgbArrayToHex(goog.color.lighten(rgb, 0.3));
      var hexDark = goog.color.rgbArrayToHex(goog.color.darken(rgb, 0.2));
      this.svgPathLight_.setAttribute('stroke', hexLight);
      this.svgPathDark_.setAttribute('fill', hexDark);
      this.svgPath_.setAttribute('stroke', 'none');
  
    }
  };
  
  /**
   * Sets the colour of shadow blocks.
   * @param {!string} rgb Primary colour of the block.
   * @param {?string} colourSecondary Colour for shadow block.
   * @return {!string} The background colour of the block.
   */
  Blockly.BlockSvg.prototype.setShadowColour_ = function(rgb, colourSecondary) {
    var hexColour;
    if (colourSecondary) {
      this.svgPathLight_.style.display = 'none';
      this.svgPathDark_.style.display = 'none';
      this.svgPath_.setAttribute('fill', colourSecondary);
      hexColour = colourSecondary;
    } else {
      rgb = goog.color.lighten(rgb, 0.6);
      hexColour = goog.color.rgbArrayToHex(rgb);
      this.svgPathLight_.style.display = 'none';
      this.svgPathDark_.setAttribute('fill', hexColour);
    }
    return hexColour;
  };
  
  /**
   * Enable or disable a block.
   */
  Blockly.BlockSvg.prototype.updateDisabled = function() {
    if (this.disabled || this.getInheritedDisabled()) {
      var added = Blockly.utils.addClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled');
      if (added) {
        this.svgPath_.setAttribute('fill',
            'url(#' + this.workspace.options.disabledPatternId + ')');
      }
    } else {
      var removed = Blockly.utils.removeClass(
          /** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled');
      if (removed) {
        this.updateColour();
      }
    }
    var children = this.getChildren(false);
    for (var i = 0, child; child = children[i]; i++) {
      child.updateDisabled();
    }
  };
  
  /**
   * Returns the comment on this block (or '' if none).
   * @return {string} Block's comment.
   */
  Blockly.BlockSvg.prototype.getCommentText = function() {
    if (this.comment) {
      var comment = this.comment.getText();
      // Trim off trailing whitespace.
      return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n');
    }
    return '';
  };
  
  /**
   * Set this block's comment text.
   * @param {?string} text The text, or null to delete.
   */
  Blockly.BlockSvg.prototype.setCommentText = function(text) {
    var changedState = false;
    if (typeof text == 'string') {
      if (!this.comment) {
        this.comment = new Blockly.Comment(this);
        changedState = true;
      }
      this.comment.setText(/** @type {string} */ (text));
    } else {
      if (this.comment) {
        this.comment.dispose();
        changedState = true;
      }
    }
    if (changedState && this.rendered) {
      this.render();
      // Adding or removing a comment icon will cause the block to change shape.
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Set this block's warning text.
   * @param {?string} text The text, or null to delete.
   * @param {string=} opt_id An optional ID for the warning text to be able to
   *     maintain multiple warnings.
   */
  Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
    if (!this.warningTextDb_) {
      // Create a database of warning PIDs.
      // Only runs once per block (and only those with warnings).
      this.warningTextDb_ = Object.create(null);
    }
    var id = opt_id || '';
    if (!id) {
      // Kill all previous pending processes, this edit supersedes them all.
      for (var n in this.warningTextDb_) {
        clearTimeout(this.warningTextDb_[n]);
        delete this.warningTextDb_[n];
      }
    } else if (this.warningTextDb_[id]) {
      // Only queue up the latest change.  Kill any earlier pending process.
      clearTimeout(this.warningTextDb_[id]);
      delete this.warningTextDb_[id];
    }
    if (this.workspace.isDragging()) {
      // Don't change the warning text during a drag.
      // Wait until the drag finishes.
      var thisBlock = this;
      this.warningTextDb_[id] = setTimeout(function() {
        if (thisBlock.workspace) {  // Check block wasn't deleted.
          delete thisBlock.warningTextDb_[id];
          thisBlock.setWarningText(text, id);
        }
      }, 100);
      return;
    }
    if (this.isInFlyout) {
      text = null;
    }
  
    // Bubble up to add a warning on top-most collapsed block.
    var parent = this.getSurroundParent();
    var collapsedParent = null;
    while (parent) {
      if (parent.isCollapsed()) {
        collapsedParent = parent;
      }
      parent = parent.getSurroundParent();
    }
    if (collapsedParent) {
      collapsedParent.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'],
          Blockly.BlockSvg.COLLAPSED_WARNING_ID);
    }
  
    var changedState = false;
    if (typeof text == 'string') {
      if (!this.warning) {
        this.warning = new Blockly.Warning(this);
        changedState = true;
      }
      this.warning.setText(/** @type {string} */ (text), id);
    } else {
      // Dispose all warnings if no ID is given.
      if (this.warning && !id) {
        this.warning.dispose();
        changedState = true;
      } else if (this.warning) {
        var oldText = this.warning.getText();
        this.warning.setText('', id);
        var newText = this.warning.getText();
        if (!newText) {
          this.warning.dispose();
        }
        changedState = oldText != newText;
      }
    }
    if (changedState && this.rendered) {
      this.render();
      // Adding or removing a warning icon will cause the block to change shape.
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Give this block a mutator dialog.
   * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove.
   */
  Blockly.BlockSvg.prototype.setMutator = function(mutator) {
    if (this.mutator && this.mutator !== mutator) {
      this.mutator.dispose();
    }
    if (mutator) {
      mutator.block_ = this;
      this.mutator = mutator;
      mutator.createIcon();
    }
  };
  
  /**
   * Set whether the block is disabled or not.
   * @param {boolean} disabled True if disabled.
   * @deprecated May 2019
   */
  Blockly.BlockSvg.prototype.setDisabled = function(disabled) {
    console.warn('Deprecated call to Blockly.BlockSvg.prototype.setDisabled, ' +
    'use Blockly.BlockSvg.prototype.setEnabled instead.');
    this.setEnabled(!disabled);
  };
  
  /**
   * Set whether the block is enabled or not.
   * @param {boolean} enabled True if enabled.
   */
  Blockly.BlockSvg.prototype.setEnabled = function(enabled) {
    if (this.isEnabled() != enabled) {
      Blockly.BlockSvg.superClass_.setEnabled.call(this, enabled);
      if (this.rendered) {
        this.updateDisabled();
      }
    }
  };
  
  /**
   * Set whether the block is highlighted or not.  Block highlighting is
   * often used to visually mark blocks currently being executed.
   * @param {boolean} highlighted True if highlighted.
   */
  Blockly.BlockSvg.prototype.setHighlighted = function(highlighted) {
    if (!this.rendered) {
      return;
    }
    if (highlighted) {
      this.svgPath_.setAttribute('filter',
          'url(#' + this.workspace.options.embossFilterId + ')');
      this.svgPathLight_.style.display = 'none';
    } else {
      Blockly.utils.removeAttribute(this.svgPath_, 'filter');
      delete this.svgPathLight_.style.display;
    }
  };
  
  /**
   * Select this block.  Highlight it visually.
   */
  Blockly.BlockSvg.prototype.addSelect = function() {
    Blockly.utils.addClass(
        /** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
  };
  
  /**
   * Unselect this block.  Remove its highlighting.
   */
  Blockly.BlockSvg.prototype.removeSelect = function() {
    Blockly.utils.removeClass(
        /** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
  };
  
  /**
   * Update the cursor over this block by adding or removing a class.
   * @param {boolean} enable True if the delete cursor should be shown, false
   *     otherwise.
   * @package
   */
  Blockly.BlockSvg.prototype.setDeleteStyle = function(enable) {
    if (enable) {
      Blockly.utils.addClass(/** @type {!Element} */ (this.svgGroup_),
          'blocklyDraggingDelete');
    } else {
      Blockly.utils.removeClass(/** @type {!Element} */ (this.svgGroup_),
          'blocklyDraggingDelete');
    }
  };
  
  // Overrides of functions on Blockly.Block that take into account whether the
  // block has been rendered.
  
  /**
   * Change the colour of a block.
   * @param {number|string} colour HSV hue value, or #RRGGBB string.
   */
  Blockly.BlockSvg.prototype.setColour = function(colour) {
    Blockly.BlockSvg.superClass_.setColour.call(this, colour);
  
    if (this.rendered) {
      this.updateColour();
    }
  };
  
  /**
   * Move this block to the front of the visible workspace.
   * <g> tags do not respect z-index so SVG renders them in the
   * order that they are in the DOM.  By placing this block first within the
   * block group's <g>, it will render on top of any other blocks.
   * @package
   */
  Blockly.BlockSvg.prototype.bringToFront = function() {
    var block = this;
    do {
      var root = block.getSvgRoot();
      root.parentNode.appendChild(root);
      block = block.getParent();
    } while (block);
  };
  
  /**
   * Set whether this block can chain onto the bottom of another block.
   * @param {boolean} newBoolean True if there can be a previous statement.
   * @param {(string|Array.<string>|null)=} opt_check Statement type or
   *     list of statement types.  Null/undefined if any type could be connected.
   */
  Blockly.BlockSvg.prototype.setPreviousStatement = function(newBoolean,
      opt_check) {
    Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean,
        opt_check);
  
    if (this.rendered) {
      this.render();
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Set whether another block can chain onto the bottom of this block.
   * @param {boolean} newBoolean True if there can be a next statement.
   * @param {(string|Array.<string>|null)=} opt_check Statement type or
   *     list of statement types.  Null/undefined if any type could be connected.
   */
  Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) {
    Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean,
        opt_check);
  
    if (this.rendered) {
      this.render();
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Set whether this block returns a value.
   * @param {boolean} newBoolean True if there is an output.
   * @param {(string|Array.<string>|null)=} opt_check Returned type or list
   *     of returned types.  Null or undefined if any type could be returned
   *     (e.g. variable get).
   */
  Blockly.BlockSvg.prototype.setOutput = function(newBoolean, opt_check) {
    Blockly.BlockSvg.superClass_.setOutput.call(this, newBoolean, opt_check);
  
    if (this.rendered) {
      this.render();
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Set whether value inputs are arranged horizontally or vertically.
   * @param {boolean} newBoolean True if inputs are horizontal.
   */
  Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) {
    Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean);
  
    if (this.rendered) {
      this.render();
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Remove an input from this block.
   * @param {string} name The name of the input.
   * @param {boolean=} opt_quiet True to prevent error if input is not present.
   * @throws {Error} if the input is not present and
   *     opt_quiet is not true.
   */
  Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) {
    Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet);
  
    if (this.rendered) {
      this.render();
      // Removing an input will cause the block to change shape.
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Move a numbered input to a different location on this block.
   * @param {number} inputIndex Index of the input to move.
   * @param {number} refIndex Index of input that should be after the moved input.
   */
  Blockly.BlockSvg.prototype.moveNumberedInputBefore = function(
      inputIndex, refIndex) {
    Blockly.BlockSvg.superClass_.moveNumberedInputBefore.call(this, inputIndex,
        refIndex);
  
    if (this.rendered) {
      this.render();
      // Moving an input will cause the block to change shape.
      this.bumpNeighbours_();
    }
  };
  
  /**
   * Add a value input, statement input or local variable to this block.
   * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or
   *     Blockly.DUMMY_INPUT.
   * @param {string} name Language-neutral identifier which may used to find this
   *     input again.  Should be unique to this block.
   * @return {!Blockly.Input} The input object created.
   * @private
   */
  Blockly.BlockSvg.prototype.appendInput_ = function(type, name) {
    var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name);
  
    if (this.rendered) {
      this.render();
      // Adding an input will cause the block to change shape.
      this.bumpNeighbours_();
    }
    return input;
  };
  
  /**
   * Returns connections originating from this block.
   * @param {boolean} all If true, return all connections even hidden ones.
   *     Otherwise, for a non-rendered block return an empty list, and for a
   *     collapsed block don't return inputs connections.
   * @return {!Array.<!Blockly.Connection>} Array of connections.
   * @package
   */
  Blockly.BlockSvg.prototype.getConnections_ = function(all) {
    var myConnections = [];
    if (all || this.rendered) {
      if (this.outputConnection) {
        myConnections.push(this.outputConnection);
      }
      if (this.previousConnection) {
        myConnections.push(this.previousConnection);
      }
      if (this.nextConnection) {
        myConnections.push(this.nextConnection);
      }
      if (all || !this.collapsed_) {
        for (var i = 0, input; input = this.inputList[i]; i++) {
          if (input.connection) {
            myConnections.push(input.connection);
          }
        }
      }
    }
    return myConnections;
  };
  
  /**
   * Create a connection of the specified type.
   * @param {number} type The type of the connection to create.
   * @return {!Blockly.RenderedConnection} A new connection of the specified type.
   * @private
   */
  Blockly.BlockSvg.prototype.makeConnection_ = function(type) {
    return new Blockly.RenderedConnection(this, type);
  };
  
  /**
   * Bump unconnected blocks out of alignment.  Two blocks which aren't actually
   * connected should not coincidentally line up on screen.
   * @private
   */
  Blockly.BlockSvg.prototype.bumpNeighbours_ = function() {
    if (!this.workspace) {
      return;  // Deleted block.
    }
    if (this.workspace.isDragging()) {
      return;  // Don't bump blocks during a drag.
    }
    var rootBlock = this.getRootBlock();
    if (rootBlock.isInFlyout) {
      return;  // Don't move blocks around in a flyout.
    }
    // Loop through every connection on this block.
    var myConnections = this.getConnections_(false);
    for (var i = 0, connection; connection = myConnections[i]; i++) {
  
      // Spider down from this block bumping all sub-blocks.
      if (connection.isConnected() && connection.isSuperior()) {
        connection.targetBlock().bumpNeighbours_();
      }
  
      var neighbours = connection.neighbours_(Blockly.SNAP_RADIUS);
      for (var j = 0, otherConnection; otherConnection = neighbours[j]; j++) {
  
        // If both connections are connected, that's probably fine.  But if
        // either one of them is unconnected, then there could be confusion.
        if (!connection.isConnected() || !otherConnection.isConnected()) {
          // Only bump blocks if they are from different tree structures.
          if (otherConnection.getSourceBlock().getRootBlock() != rootBlock) {
  
            // Always bump the inferior block.
            if (connection.isSuperior()) {
              otherConnection.bumpAwayFrom_(connection);
            } else {
              connection.bumpAwayFrom_(otherConnection);
            }
          }
        }
      }
    }
  };
  
  /**
   * Schedule snapping to grid and bumping neighbours to occur after a brief
   * delay.
   * @package
   */
  Blockly.BlockSvg.prototype.scheduleSnapAndBump = function() {
    var block = this;
    // Ensure that any snap and bump are part of this move's event group.
    var group = Blockly.Events.getGroup();
  
    setTimeout(function() {
      Blockly.Events.setGroup(group);
      block.snapToGrid();
      Blockly.Events.setGroup(false);
    }, Blockly.BUMP_DELAY / 2);
  
    setTimeout(function() {
      Blockly.Events.setGroup(group);
      block.bumpNeighbours_();
      Blockly.Events.setGroup(false);
    }, Blockly.BUMP_DELAY);
  };
  
  /**
   * Position a block so that it doesn't move the target block when connected.
   * The block to position is usually either the first block in a dragged stack or
   * an insertion marker.
   * @param {!Blockly.Connection} sourceConnection The connection on the moving
   *     block's stack.
   * @param {!Blockly.Connection} targetConnection The connection that should stay
   *     stationary as this block is positioned.
   */
  Blockly.BlockSvg.prototype.positionNearConnection = function(sourceConnection,
      targetConnection) {
    // We only need to position the new block if it's before the existing one,
    // otherwise its position is set by the previous block.
    if (sourceConnection.type == Blockly.NEXT_STATEMENT ||
        sourceConnection.type == Blockly.INPUT_VALUE) {
      var dx = targetConnection.x_ - sourceConnection.x_;
      var dy = targetConnection.y_ - sourceConnection.y_;
  
      this.moveBy(dx, dy);
    }
  };
  