scratch-blocks/core/block_svg.js
2015-02-06 15:27:25 -08:00

1916 lines
64 KiB
JavaScript

/**
* @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.ContextMenu');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
/**
* Class for a block's SVG representation.
* @extends {Blockly.Block}
* @constructor
*/
Blockly.BlockSvg = function() {
// Create core elements for the block.
this.svgGroup_ = Blockly.createSvgElement('g', {}, null);
this.svgPathDark_ = Blockly.createSvgElement('path',
{'class': 'blocklyPathDark', 'transform': 'translate(1, 1)'},
this.svgGroup_);
this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'},
this.svgGroup_);
this.svgPathLight_ = Blockly.createSvgElement('path',
{'class': 'blocklyPathLight'}, this.svgGroup_);
this.svgPath_.tooltip = this;
Blockly.Tooltip.bindMouseEvents(this.svgPath_);
this.updateMovable();
};
goog.inherits(Blockly.BlockSvg, Blockly.Block);
/**
* Height of this block, not including any statement blocks above or below.
*/
Blockly.BlockSvg.prototype.height = 0;
/**
* Width of this block, including any connected value blocks.
*/
Blockly.BlockSvg.prototype.width = 0;
/**
* Constant for identifying rows that are to be rendered inline.
* Don't collide with Blockly.INPUT_VALUE and friends.
* @const
*/
Blockly.BlockSvg.INLINE = -1;
/**
* Create and initialize the SVG representation of the block.
* May be called more than once.
*/
Blockly.BlockSvg.prototype.initSvg = function() {
goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.');
for (var x = 0, input; input = this.inputList[x]; x++) {
input.init();
}
if (this.mutator) {
this.mutator.createIcon();
}
this.updateColour();
if (!Blockly.readOnly && !this.eventsInit_) {
Blockly.bindEvent_(this.getSvgRoot(), 'mousedown', this,
this.onMouseDown_);
}
// Bind an onchange function, if it exists.
if (goog.isFunction(this.onchange) && !this.eventsInit_) {
Blockly.bindEvent_(this.workspace.getCanvas(), 'blocklyWorkspaceChange',
this, this.onchange);
}
this.eventsInit_ = true;
if (!this.getSvgRoot().parentNode) {
this.workspace.getCanvas().appendChild(this.getSvgRoot());
}
};
/**
* Select this block. Highlight it visually.
*/
Blockly.BlockSvg.prototype.select = function() {
if (Blockly.selected) {
// Unselect any previously selected block.
Blockly.selected.unselect();
}
Blockly.selected = this;
this.addSelect();
Blockly.fireUiEvent(this.workspace.getCanvas(), 'blocklySelectChange');
};
/**
* Unselect this block. Remove its highlighting.
*/
Blockly.BlockSvg.prototype.unselect = function() {
Blockly.selected = null;
this.removeSelect();
Blockly.fireUiEvent(this.workspace.getCanvas(), 'blocklySelectChange');
};
/**
* 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;
};
/**
* Wrapper function called when a mouseUp occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.BlockSvg.onMouseUpWrapper_ = null;
/**
* Wrapper function called when a mouseMove occurs during a drag operation.
* @type {Array.<!Array>}
* @private
*/
Blockly.BlockSvg.onMouseMoveWrapper_ = null;
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.BlockSvg.terminateDrag_ = function() {
if (Blockly.BlockSvg.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_);
Blockly.BlockSvg.onMouseUpWrapper_ = null;
}
if (Blockly.BlockSvg.onMouseMoveWrapper_) {
Blockly.unbindEvent_(Blockly.BlockSvg.onMouseMoveWrapper_);
Blockly.BlockSvg.onMouseMoveWrapper_ = null;
}
var selected = Blockly.selected;
if (Blockly.dragMode_ == 2) {
// Terminate a drag operation.
if (selected) {
// Update the connection locations.
var xy = selected.getRelativeToSurfaceXY();
var dx = xy.x - selected.startDragX;
var dy = xy.y - selected.startDragY;
selected.moveConnections_(dx, dy);
delete selected.draggedBubbles_;
selected.setDragging_(false);
selected.render();
goog.Timer.callOnce(
selected.bumpNeighbours_, Blockly.BUMP_DELAY, selected);
// Fire an event to allow scrollbars to resize.
Blockly.fireUiEvent(window, 'resize');
selected.workspace.fireChangeEvent();
}
}
Blockly.dragMode_ = 0;
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
};
/**
* 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 svgRoot = this.getSvgRoot();
if (this.parentBlock_ && svgRoot) {
// Move this block up the DOM. Keep track of x/y translations.
var xy = this.getRelativeToSurfaceXY();
this.workspace.getCanvas().appendChild(svgRoot);
svgRoot.setAttribute('transform',
'translate(' + xy.x + ', ' + xy.y + ')');
}
Blockly.BlockSvg.superClass_.setParent.call(this, newParent);
if (newParent) {
var oldXY = this.getRelativeToSurfaceXY();
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);
}
};
/**
* Return the coordinates of the top-left corner of this block relative to the
* drawing surface's origin (0,0).
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
var x = 0;
var y = 0;
var element = this.getSvgRoot();
if (element) {
do {
// Loop through this block and every parent.
var xy = Blockly.getRelativeXY_(element);
x += xy.x;
y += xy.y;
element = element.parentNode;
} while (element && element != this.workspace.getCanvas());
}
return new goog.math.Coordinate(x, y);
};
/**
* Move a block by a relative offset.
* @param {number} dx Horizontal offset.
* @param {number} dy Vertical offset.
*/
Blockly.BlockSvg.prototype.moveBy = function(dx, dy) {
var xy = this.getRelativeToSurfaceXY();
this.getSvgRoot().setAttribute('transform',
'translate(' + (xy.x + dx) + ', ' + (xy.y + dy) + ')');
this.moveConnections_(dx, dy);
Blockly.Realtime.blockChanged(this);
};
/**
* Returns a bounding box describing the dimensions of this block
* and any blocks stacked below it.
* @return {!Object} Object with height and width properties.
*/
Blockly.BlockSvg.prototype.getHeightWidth = function() {
var height = this.height;
var width = this.width;
// Recursively add size of subsequent blocks.
var nextBlock = this.getNextBlock();
if (nextBlock) {
var nextHeightWidth = nextBlock.getHeightWidth();
height += nextHeightWidth.height - 4; // Height of tab.
width = Math.max(width, nextHeightWidth.width);
}
return {height: height, width: width};
};
/**
* 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;
}
Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed);
var renderList = [];
// Show/hide the inputs.
for (var x = 0, input; input = this.inputList[x]; x++) {
renderList.push.apply(renderList, input.setVisible(!collapsed));
}
var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
if (collapsed) {
var icons = this.getIcons();
for (var x = 0; x < icons.length; x++) {
icons[x].setVisible(false);
}
var text = this.toString(Blockly.COLLAPSE_CHARS);
this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init();
} else {
this.removeInput(COLLAPSED_INPUT_NAME);
}
if (!renderList.length) {
// No child blocks, just render this block.
renderList[0] = this;
}
if (this.rendered) {
for (var x = 0, block; block = renderList[x]; x++) {
block.render();
}
this.bumpNeighbours_();
}
};
/**
* Handle a mouse-down on an SVG block.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
if (this.isInFlyout) {
return;
}
// Update Blockly's knowledge of its own location.
Blockly.svgResize();
Blockly.terminateDrag_();
this.select();
Blockly.hideChaff();
if (Blockly.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
} else if (!this.isMovable()) {
// Allow unmovable blocks to be selected and context menued, but not
// dragged. Let this event bubble up to document, so the workspace may be
// dragged instead.
return;
} else {
// Left-click (or middle click)
Blockly.removeAllRanges();
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
// Look up the current translation and record it.
var xy = this.getRelativeToSurfaceXY();
this.startDragX = xy.x;
this.startDragY = xy.y;
// Record the current mouse position.
this.startDragMouseX = e.clientX;
this.startDragMouseY = e.clientY;
Blockly.dragMode_ = 1;
Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEvent_(document,
'mouseup', this, this.onMouseUp_);
Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEvent_(document,
'mousemove', this, this.onMouseMove_);
// Build a list of bubbles that need to be moved and where they started.
this.draggedBubbles_ = [];
var descendants = this.getDescendants();
for (var x = 0, descendant; descendant = descendants[x]; x++) {
var icons = descendant.getIcons();
for (var y = 0; y < icons.length; y++) {
var data = icons[y].getIconLocation();
data.bubble = icons[y];
this.draggedBubbles_.push(data);
}
}
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Handle a mouse-up anywhere in the SVG pane. Is only registered when a
* block is clicked. We can't use mouseUp on the block since a fast-moving
* cursor can briefly escape the block before it catches up.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
var this_ = this;
Blockly.doCommand(function() {
Blockly.terminateDrag_();
if (Blockly.selected && Blockly.highlightedConnection_) {
// Connect two blocks together.
Blockly.localConnection_.connect(Blockly.highlightedConnection_);
if (this_.rendered) {
// Trigger a connection animation.
// Determine which connection is inferior (lower in the source stack).
var inferiorConnection;
if (Blockly.localConnection_.isSuperior()) {
inferiorConnection = Blockly.highlightedConnection_;
} else {
inferiorConnection = Blockly.localConnection_;
}
inferiorConnection.sourceBlock_.connectionUiEffect();
}
if (this_.workspace.trashcan) {
// Don't throw an object in the trash can if it just got connected.
this_.workspace.trashcan.close();
}
} else if (this_.workspace.isDeleteArea(e) &&
Blockly.selected.isDeletable()) {
var trashcan = this_.workspace.trashcan;
if (trashcan) {
goog.Timer.callOnce(trashcan.close, 100, trashcan);
}
Blockly.selected.dispose(false, true);
// Dropping a block on the trash can will usually cause the workspace to
// resize to contain the newly positioned block. Force a second resize
// now that the block has been deleted.
Blockly.fireUiEvent(window, 'resize');
}
if (Blockly.highlightedConnection_) {
Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
}
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
});
};
/**
* Load the block's help page in a new window.
* @private
*/
Blockly.BlockSvg.prototype.showHelp_ = function() {
var url = goog.isFunction(this.helpUrl) ? 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 (Blockly.readOnly || !this.contextMenu) {
return;
}
// Save the current block in a variable for use in closures.
var block = this;
var options = [];
if (this.isDeletable() && this.isMovable() && !block.isInFlyout) {
// Option to duplicate this block.
var duplicateOption = {
text: Blockly.Msg.DUPLICATE_BLOCK,
enabled: true,
callback: function() {
block.duplicate_();
}
};
if (this.getDescendants().length > this.workspace.remainingCapacity()) {
duplicateOption.enabled = false;
}
options.push(duplicateOption);
if (this.isEditable() && !this.collapsed_ && Blockly.comments) {
// Option to add/remove a comment.
var commentOption = {enabled: true};
if (this.comment) {
commentOption.text = Blockly.Msg.REMOVE_COMMENT;
commentOption.callback = function() {
block.setCommentText(null);
};
} else {
commentOption.text = Blockly.Msg.ADD_COMMENT;
commentOption.callback = function() {
block.setCommentText('');
};
}
options.push(commentOption);
}
// Option to make block inline.
if (!this.collapsed_) {
for (var i = 0; i < this.inputList.length; i++) {
if (this.inputList[i].type == Blockly.INPUT_VALUE) {
// Only display this option if there is a value input on the block.
var inlineOption = {enabled: true};
inlineOption.text = this.inputsInline ? Blockly.Msg.EXTERNAL_INPUTS :
Blockly.Msg.INLINE_INPUTS;
inlineOption.callback = function() {
block.setInputsInline(!block.inputsInline);
};
options.push(inlineOption);
break;
}
}
}
if (Blockly.collapse) {
// Option to collapse/expand block.
if (this.collapsed_) {
var expandOption = {enabled: true};
expandOption.text = Blockly.Msg.EXPAND_BLOCK;
expandOption.callback = function() {
block.setCollapsed(false);
};
options.push(expandOption);
} else {
var collapseOption = {enabled: true};
collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK;
collapseOption.callback = function() {
block.setCollapsed(true);
};
options.push(collapseOption);
}
}
if (Blockly.disable) {
// Option to disable/enable block.
var disableOption = {
text: this.disabled ?
Blockly.Msg.ENABLE_BLOCK : Blockly.Msg.DISABLE_BLOCK,
enabled: !this.getInheritedDisabled(),
callback: function() {
block.setDisabled(!block.disabled);
}
};
options.push(disableOption);
}
// Option to delete this block.
// Count the number of blocks that are nested in this block.
var descendantCount = this.getDescendants().length;
var nextBlock = this.getNextBlock();
if (nextBlock) {
// Blocks in the current stack would survive this block's deletion.
descendantCount -= nextBlock.getDescendants().length;
}
var deleteOption = {
text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK :
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)),
enabled: true,
callback: function() {
block.dispose(true, true);
}
};
options.push(deleteOption);
}
// Option to get help.
var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl;
var helpOption = {enabled: !!url};
helpOption.text = Blockly.Msg.HELP;
helpOption.callback = function() {
block.showHelp_();
};
options.push(helpOption);
// Allow the block to add or modify options.
if (this.customContextMenu && !block.isInFlyout) {
this.customContextMenu(options);
}
Blockly.ContextMenu.show(e, options);
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.
* @param {number} dy Vertical offset from current location.
* @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 x = 0; x < myConnections.length; x++) {
myConnections[x].moveBy(dx, dy);
}
var icons = this.getIcons();
for (var x = 0; x < icons.length; x++) {
icons[x].computeIconLocation();
}
// Recurse through all blocks attached under this one.
for (var x = 0; x < this.childBlocks_.length; x++) {
this.childBlocks_[x].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.
* @private
*/
Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
if (adding) {
this.addDragging();
} else {
this.removeDragging();
}
// Recurse through all blocks attached under this one.
for (var x = 0; x < this.childBlocks_.length; x++) {
this.childBlocks_[x].setDragging_(adding);
}
};
/**
* Drag this block to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
var this_ = this;
Blockly.doCommand(function() {
if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 &&
e.button == 0) {
/* HACK:
Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove
events on certain touch actions. Ignore events with these signatures.
This may result in a one-pixel blind spot in other browsers,
but this shouldn't be noticeable. */
e.stopPropagation();
return;
}
Blockly.removeAllRanges();
var dx = e.clientX - this_.startDragMouseX;
var dy = e.clientY - this_.startDragMouseY;
if (Blockly.dragMode_ == 1) {
// Still dragging within the sticky DRAG_RADIUS.
var dr = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
if (dr > Blockly.DRAG_RADIUS) {
// Switch to unrestricted dragging.
Blockly.dragMode_ = 2;
// Push this block to the very top of the stack.
this_.setParent(null);
this_.setDragging_(true);
this_.workspace.recordDeleteAreas();
}
}
if (Blockly.dragMode_ == 2) {
// Unrestricted dragging.
var x = this_.startDragX + dx;
var y = this_.startDragY + dy;
this_.getSvgRoot().setAttribute('transform',
'translate(' + x + ', ' + y + ')');
// Drag all the nested bubbles.
for (var i = 0; i < this_.draggedBubbles_.length; i++) {
var commentData = this_.draggedBubbles_[i];
commentData.bubble.setIconLocation(commentData.x + dx,
commentData.y + dy);
}
// Check to see if any of this block's connections are within range of
// another block's connection.
var myConnections = this_.getConnections_(false);
var closestConnection = null;
var localConnection = null;
var radiusConnection = Blockly.SNAP_RADIUS;
for (var i = 0; i < myConnections.length; i++) {
var myConnection = myConnections[i];
var neighbour = myConnection.closest(radiusConnection, dx, dy);
if (neighbour.connection) {
closestConnection = neighbour.connection;
localConnection = myConnection;
radiusConnection = neighbour.radius;
}
}
// Remove connection highlighting if needed.
if (Blockly.highlightedConnection_ &&
Blockly.highlightedConnection_ != closestConnection) {
Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
Blockly.localConnection_ = null;
}
// Add connection highlighting if needed.
if (closestConnection &&
closestConnection != Blockly.highlightedConnection_) {
closestConnection.highlight();
Blockly.highlightedConnection_ = closestConnection;
Blockly.localConnection_ = localConnection;
}
// Provide visual indication of whether the block will be deleted if
// dropped here.
if (this_.isDeletable()) {
this_.workspace.isDeleteArea(e);
}
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
});
};
/**
* Add or remove the UI indicating if this block is movable or not.
*/
Blockly.BlockSvg.prototype.updateMovable = function() {
if (this.isMovable()) {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggable');
} else {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDraggable');
}
};
/**
* 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_;
};
// UI constants for rendering blocks.
/**
* Horizontal space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_X = 10;
/**
* Vertical space between elements.
* @const
*/
Blockly.BlockSvg.SEP_SPACE_Y = 10;
/**
* Vertical padding around inline elements.
* @const
*/
Blockly.BlockSvg.INLINE_PADDING_Y = 5;
/**
* Minimum height of a block.
* @const
*/
Blockly.BlockSvg.MIN_BLOCK_Y = 25;
/**
* Height of horizontal puzzle tab.
* @const
*/
Blockly.BlockSvg.TAB_HEIGHT = 20;
/**
* Width of horizontal puzzle tab.
* @const
*/
Blockly.BlockSvg.TAB_WIDTH = 8;
/**
* Width of vertical tab (inc left margin).
* @const
*/
Blockly.BlockSvg.NOTCH_WIDTH = 30;
/**
* Rounded corner radius.
* @const
*/
Blockly.BlockSvg.CORNER_RADIUS = 8;
/**
* Minimum height of field rows.
* @const
*/
Blockly.BlockSvg.FIELD_HEIGHT = 18;
/**
* Distance from shape edge to intersect with a curved corner at 45 degrees.
* Applies to highlighting on around the inside of a curve.
* @const
*/
Blockly.BlockSvg.DISTANCE_45_INSIDE = (1 - Math.SQRT1_2) *
(Blockly.BlockSvg.CORNER_RADIUS - 1) + 1;
/**
* Distance from shape edge to intersect with a curved corner at 45 degrees.
* Applies to highlighting on around the outside of a curve.
* @const
*/
Blockly.BlockSvg.DISTANCE_45_OUTSIDE = (1 - Math.SQRT1_2) *
(Blockly.BlockSvg.CORNER_RADIUS + 1) - 1;
/**
* SVG path for drawing next/previous notch from left to right.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_LEFT = 'l 6,4 3,0 6,-4';
/**
* SVG path for drawing next/previous notch from left to right with
* highlighting.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT = 'l 6.5,4 2,0 6.5,-4';
/**
* SVG path for drawing next/previous notch from right to left.
* @const
*/
Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4';
/**
* SVG path for drawing jagged teeth at the end of collapsed blocks.
* @const
*/
Blockly.BlockSvg.JAGGED_TEETH = 'l 8,0 0,4 8,4 -16,8 8,4';
/**
* Height of SVG path for jagged teeth at the end of collapsed blocks.
* @const
*/
Blockly.BlockSvg.JAGGED_TEETH_HEIGHT = 20;
/**
* Width of SVG path for jagged teeth at the end of collapsed blocks.
* @const
*/
Blockly.BlockSvg.JAGGED_TEETH_WIDTH = 15;
/**
* SVG path for drawing a horizontal puzzle tab from top to bottom.
* @const
*/
Blockly.BlockSvg.TAB_PATH_DOWN = 'v 5 c 0,10 -' + Blockly.BlockSvg.TAB_WIDTH +
',-8 -' + Blockly.BlockSvg.TAB_WIDTH + ',7.5 s ' +
Blockly.BlockSvg.TAB_WIDTH + ',-2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',7.5';
/**
* SVG path for drawing a horizontal puzzle tab from top to bottom with
* highlighting from the upper-right.
* @const
*/
Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL = 'v 6.5 m -' +
(Blockly.BlockSvg.TAB_WIDTH * 0.98) + ',2.5 q -' +
(Blockly.BlockSvg.TAB_WIDTH * .05) + ',10 ' +
(Blockly.BlockSvg.TAB_WIDTH * .27) + ',10 m ' +
(Blockly.BlockSvg.TAB_WIDTH * .71) + ',-2.5 v 1.5';
/**
* SVG start point for drawing the top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_START =
'm 0,' + Blockly.BlockSvg.CORNER_RADIUS;
/**
* SVG start point for drawing the top-left corner's highlight in RTL.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL =
'm ' + Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' +
Blockly.BlockSvg.DISTANCE_45_INSIDE;
/**
* SVG start point for drawing the top-left corner's highlight in LTR.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR =
'm 1,' + (Blockly.BlockSvg.CORNER_RADIUS - 1);
/**
* SVG path for drawing the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER =
'A ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',0';
/**
* SVG path for drawing the highlight on the rounded top-left corner.
* @const
*/
Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT =
'A ' + (Blockly.BlockSvg.CORNER_RADIUS - 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS - 1) + ' 0 0,1 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',1';
/**
* SVG path for drawing the top-left corner of a statement input.
* Includes the top notch, a horizontal space, and the rounded inside corner.
* @const
*/
Blockly.BlockSvg.INNER_TOP_LEFT_CORNER =
Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -' +
(Blockly.BlockSvg.NOTCH_WIDTH - 15 - Blockly.BlockSvg.CORNER_RADIUS) +
' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS;
/**
* SVG path for drawing the bottom-left corner of a statement input.
* Includes the rounded inside corner.
* @const
*/
Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER =
'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS;
/**
* SVG path for drawing highlight on the top-left corner of a statement
* input in RTL.
* @const
*/
Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL =
'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' +
(-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS -
Blockly.BlockSvg.DISTANCE_45_OUTSIDE);
/**
* SVG path for drawing highlight on the bottom-left corner of a statement
* input in RTL.
* @const
*/
Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL =
'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' +
(Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS + 1);
/**
* SVG path for drawing highlight on the bottom-left corner of a statement
* input in LTR.
* @const
*/
Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR =
'a ' + (Blockly.BlockSvg.CORNER_RADIUS + 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS + 1) + ' 0 0,0 ' +
(Blockly.BlockSvg.CORNER_RADIUS -
Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' +
(Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 1);
/**
* 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.
* @param {boolean} opt_dontRemoveFromWorkspace If true, don't remove this
* block from the workspace's list of top blocks.
*/
Blockly.BlockSvg.prototype.dispose = function(healStack, animate,
opt_dontRemoveFromWorkspace) {
// If there's a drag in-progress, unlink the mouse events.
Blockly.terminateDrag_();
// If this block has a context menu open, close it.
if (Blockly.ContextMenu.currentBlock == this) {
Blockly.ContextMenu.hide();
}
// Bring block to the top of the workspace.
this.unplug(healStack, false);
if (animate && this.rendered) {
this.disposeUiEffect();
}
// Stop rerendering.
this.rendered = false;
var icons = this.getIcons();
for (var x = 0; x < icons.length; x++) {
icons[x].dispose();
}
Blockly.BlockSvg.superClass_.dispose.call(this, healStack);
goog.dom.removeNode(this.svgGroup_);
// Sever JavaScript to DOM connections.
this.svgGroup_ = null;
this.svgPath_ = null;
this.svgPathLight_ = null;
this.svgPathDark_ = null;
};
/**
* Play some UI effects (sound, animation) when disposing of a block.
*/
Blockly.BlockSvg.prototype.disposeUiEffect = function() {
Blockly.playAudio('delete');
var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_));
// Deeply clone the current block.
var clone = this.svgGroup_.cloneNode(true);
clone.translateX_ = xy.x;
clone.translateY_ = xy.y;
clone.setAttribute('transform',
'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')');
Blockly.svg.appendChild(clone);
clone.bBox_ = clone.getBBox();
// Start the animation.
clone.startDate_ = new Date();
Blockly.BlockSvg.disposeUiStep_(clone);
};
/**
* Animate a cloned block and eventually dispose of it.
* @param {!Element} clone SVG element to animate and dispose of.
* @private
*/
Blockly.BlockSvg.disposeUiStep_ = function(clone) {
var ms = (new Date()) - clone.startDate_;
var percent = ms / 150;
if (percent > 1) {
goog.dom.removeNode(clone);
} else {
var x = clone.translateX_ +
(Blockly.RTL ? -1 : 1) * clone.bBox_.width / 2 * percent;
var y = clone.translateY_ + clone.bBox_.height * percent;
var translate = x + ', ' + y;
var scale = 1 - percent;
clone.setAttribute('transform', 'translate(' + translate + ')' +
' scale(' + scale + ')');
var closure = function() {
Blockly.BlockSvg.disposeUiStep_(clone);
};
setTimeout(closure, 10);
}
};
/**
* Play some UI effects (sound, ripple) after a connection has been established.
*/
Blockly.BlockSvg.prototype.connectionUiEffect = function() {
Blockly.playAudio('click');
// Determine the absolute coordinates of the inferior block.
var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_));
// Offset the coordinates based on the two connection types.
if (this.outputConnection) {
xy.x += Blockly.RTL ? 3 : -3;
xy.y += 13;
} else if (this.previousConnection) {
xy.x += Blockly.RTL ? -23 : 23;
xy.y += 3;
}
var ripple = Blockly.createSvgElement('circle',
{'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none',
'stroke': '#888', 'stroke-width': 10},
Blockly.svg);
// Start the animation.
ripple.startDate_ = new Date();
Blockly.BlockSvg.connectionUiStep_(ripple);
};
/**
* Expand a ripple around a connection.
* @param {!Element} ripple Element to animate.
* @private
*/
Blockly.BlockSvg.connectionUiStep_ = function(ripple) {
var ms = (new Date()) - ripple.startDate_;
var percent = ms / 150;
if (percent > 1) {
goog.dom.removeNode(ripple);
} else {
ripple.setAttribute('r', percent * 25);
ripple.style.opacity = 1 - percent;
var closure = function() {
Blockly.BlockSvg.connectionUiStep_(ripple);
};
setTimeout(closure, 10);
}
};
/**
* Change the colour of a block.
*/
Blockly.BlockSvg.prototype.updateColour = function() {
if (this.disabled) {
// Disabled blocks don't have colour.
return;
}
var hexColour = Blockly.makeColour(this.getColour());
var rgb = goog.color.hexToRgb(hexColour);
var rgbLight = goog.color.lighten(rgb, 0.3);
var rgbDark = goog.color.darken(rgb, 0.4);
this.svgPathLight_.setAttribute('stroke', goog.color.rgbArrayToHex(rgbLight));
this.svgPathDark_.setAttribute('fill', goog.color.rgbArrayToHex(rgbDark));
this.svgPath_.setAttribute('fill', hexColour);
var icons = this.getIcons();
for (var x = 0; x < icons.length; x++) {
icons[x].updateColour();
}
// Bump every dropdown to change its colour.
for (var x = 0, input; input = this.inputList[x]; x++) {
for (var y = 0, field; field = input.fieldRow[y]; y++) {
field.setText(null);
}
}
};
/**
* Enable or disable a block.
*/
Blockly.BlockSvg.prototype.updateDisabled = function() {
var hasClass = Blockly.hasClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDisabled');
if (this.disabled || this.getInheritedDisabled()) {
if (!hasClass) {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDisabled');
this.svgPath_.setAttribute('fill', 'url(#blocklyDisabledPattern)');
}
} else {
if (hasClass) {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDisabled');
this.updateColour();
}
}
var children = this.getChildren();
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 (goog.isString(text)) {
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.
*/
Blockly.BlockSvg.prototype.setWarningText = function(text) {
if (this.isInFlyout) {
text = null;
}
var changedState = false;
if (goog.isString(text)) {
if (!this.warning) {
this.warning = new Blockly.Warning(this);
changedState = true;
}
this.warning.setText(/** @type {string} */ (text));
} else {
if (this.warning) {
this.warning.dispose();
changedState = true;
}
}
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;
if (this.rendered) {
mutator.createIcon();
}
}
};
/**
* Set whether the block is disabled or not.
* @param {boolean} disabled True if disabled.
*/
Blockly.BlockSvg.prototype.setDisabled = function(disabled) {
if (this.disabled == disabled) {
return;
}
Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled);
if (this.rendered) {
this.updateDisabled();
}
this.workspace.fireChangeEvent();
};
/**
* Select this block. Highlight it visually.
*/
Blockly.BlockSvg.prototype.addSelect = function() {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklySelected');
// Move the selected block to the top of the stack.
this.svgGroup_.parentNode.appendChild(this.svgGroup_);
};
/**
* Unselect this block. Remove its highlighting.
*/
Blockly.BlockSvg.prototype.removeSelect = function() {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklySelected');
};
/**
* Adds the dragging class to this block.
* Also disables the highlights/shadows to improve performance.
*/
Blockly.BlockSvg.prototype.addDragging = function() {
Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDragging');
};
/**
* Removes the dragging class from this block.
*/
Blockly.BlockSvg.prototype.removeDragging = function() {
Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
'blocklyDragging');
};
/**
* Render the block.
* Lays out and reflows a block based on its contents and settings.
* @param {boolean} opt_bubble If false, just render this block.
* If true, also render block's parent, grandparent, etc. Defaults to true.
*/
Blockly.BlockSvg.prototype.render = function(opt_bubble) {
this.rendered = true;
var cursorX = Blockly.BlockSvg.SEP_SPACE_X;
if (Blockly.RTL) {
cursorX = -cursorX;
}
// Move the icons into position.
var icons = this.getIcons();
for (var x = 0; x < icons.length; x++) {
cursorX = icons[x].renderIcon(cursorX);
}
cursorX += Blockly.RTL ?
Blockly.BlockSvg.SEP_SPACE_X : -Blockly.BlockSvg.SEP_SPACE_X;
// If there are no icons, cursorX will be 0, otherwise it will be the
// width that the first label needs to move over by.
var inputRows = this.renderCompute_(cursorX);
this.renderDraw_(cursorX, inputRows);
if (opt_bubble !== false) {
// Render all blocks above this one (propagate a reflow).
var parentBlock = this.getParent();
if (parentBlock) {
parentBlock.render(true);
} else {
// Top-most block. Fire an event to allow scrollbars to resize.
Blockly.fireUiEvent(window, 'resize');
}
}
Blockly.Realtime.blockChanged(this);
};
/**
* Render a list of fields starting at the specified location.
* @param {!Array.<!Blockly.Field>} fieldList List of fields.
* @param {number} cursorX X-coordinate to start the fields.
* @param {number} cursorY Y-coordinate to start the fields.
* @return {number} X-coordinate of the end of the field row (plus a gap).
* @private
*/
Blockly.BlockSvg.prototype.renderFields_ =
function(fieldList, cursorX, cursorY) {
if (Blockly.RTL) {
cursorX = -cursorX;
}
for (var t = 0, field; field = fieldList[t]; t++) {
var root = field.getSvgRoot();
if (!root) {
continue;
}
if (Blockly.RTL) {
cursorX -= field.renderSep + field.renderWidth;
root.setAttribute('transform',
'translate(' + cursorX + ', ' + cursorY + ')');
if (field.renderWidth) {
cursorX -= Blockly.BlockSvg.SEP_SPACE_X;
}
} else {
root.setAttribute('transform',
'translate(' + (cursorX + field.renderSep) + ', ' + cursorY + ')');
if (field.renderWidth) {
cursorX += field.renderSep + field.renderWidth +
Blockly.BlockSvg.SEP_SPACE_X;
}
}
}
return Blockly.RTL ? -cursorX : cursorX;
};
/**
* Computes the height and widths for each row and field.
* @param {number} iconWidth Offset of first row due to icons.
* @return {!Array.<!Array.<!Object>>} 2D array of objects, each containing
* position information.
* @private
*/
Blockly.BlockSvg.prototype.renderCompute_ = function(iconWidth) {
var inputList = this.inputList;
var inputRows = [];
inputRows.rightEdge = iconWidth + Blockly.BlockSvg.SEP_SPACE_X * 2;
if (this.previousConnection || this.nextConnection) {
inputRows.rightEdge = Math.max(inputRows.rightEdge,
Blockly.BlockSvg.NOTCH_WIDTH + Blockly.BlockSvg.SEP_SPACE_X);
}
var fieldValueWidth = 0; // Width of longest external value field.
var fieldStatementWidth = 0; // Width of longest statement field.
var hasValue = false;
var hasStatement = false;
var hasDummy = false;
var lastType = undefined;
var isInline = this.inputsInline && !this.isCollapsed();
for (var i = 0, input; input = inputList[i]; i++) {
if (!input.isVisible()) {
continue;
}
var row;
if (!isInline || !lastType ||
lastType == Blockly.NEXT_STATEMENT ||
input.type == Blockly.NEXT_STATEMENT) {
// Create new row.
lastType = input.type;
row = [];
if (isInline && input.type != Blockly.NEXT_STATEMENT) {
row.type = Blockly.BlockSvg.INLINE;
} else {
row.type = input.type;
}
row.height = 0;
inputRows.push(row);
} else {
row = inputRows[inputRows.length - 1];
}
row.push(input);
// Compute minimum input size.
input.renderHeight = Blockly.BlockSvg.MIN_BLOCK_Y;
// The width is currently only needed for inline value inputs.
if (isInline && input.type == Blockly.INPUT_VALUE) {
input.renderWidth = Blockly.BlockSvg.TAB_WIDTH +
Blockly.BlockSvg.SEP_SPACE_X * 1.25;
} else {
input.renderWidth = 0;
}
// Expand input size if there is a connection.
if (input.connection && input.connection.targetConnection) {
var linkedBlock = input.connection.targetBlock();
var bBox = linkedBlock.getHeightWidth();
input.renderHeight = Math.max(input.renderHeight, bBox.height);
input.renderWidth = Math.max(input.renderWidth, bBox.width);
}
if (i == inputList.length - 1) {
// Last element should overhang slightly due to shadow.
input.renderHeight--;
}
row.height = Math.max(row.height, input.renderHeight);
input.fieldWidth = 0;
if (inputRows.length == 1) {
// The first row gets shifted to accommodate any icons.
input.fieldWidth += Blockly.RTL ? -iconWidth : iconWidth;
}
var previousFieldEditable = false;
for (var j = 0, field; field = input.fieldRow[j]; j++) {
if (j != 0) {
input.fieldWidth += Blockly.BlockSvg.SEP_SPACE_X;
}
// Get the dimensions of the field.
var fieldSize = field.getSize();
field.renderWidth = fieldSize.width;
field.renderSep = (previousFieldEditable && field.EDITABLE) ?
Blockly.BlockSvg.SEP_SPACE_X : 0;
input.fieldWidth += field.renderWidth + field.renderSep;
row.height = Math.max(row.height, fieldSize.height);
previousFieldEditable = field.EDITABLE;
}
if (row.type != Blockly.BlockSvg.INLINE) {
if (row.type == Blockly.NEXT_STATEMENT) {
hasStatement = true;
fieldStatementWidth = Math.max(fieldStatementWidth, input.fieldWidth);
} else {
if (row.type == Blockly.INPUT_VALUE) {
hasValue = true;
} else if (row.type == Blockly.DUMMY_INPUT) {
hasDummy = true;
}
fieldValueWidth = Math.max(fieldValueWidth, input.fieldWidth);
}
}
}
// Make inline rows a bit thicker in order to enclose the values.
for (var y = 0, row; row = inputRows[y]; y++) {
row.thicker = false;
if (row.type == Blockly.BlockSvg.INLINE) {
for (var z = 0, input; input = row[z]; z++) {
if (input.type == Blockly.INPUT_VALUE) {
row.height += 2 * Blockly.BlockSvg.INLINE_PADDING_Y;
row.thicker = true;
break;
}
}
}
}
// Compute the statement edge.
// This is the width of a block where statements are nested.
inputRows.statementEdge = 2 * Blockly.BlockSvg.SEP_SPACE_X +
fieldStatementWidth;
// Compute the preferred right edge. Inline blocks may extend beyond.
// This is the width of the block where external inputs connect.
if (hasStatement) {
inputRows.rightEdge = Math.max(inputRows.rightEdge,
inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH);
}
if (hasValue) {
inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth +
Blockly.BlockSvg.SEP_SPACE_X * 2 + Blockly.BlockSvg.TAB_WIDTH);
} else if (hasDummy) {
inputRows.rightEdge = Math.max(inputRows.rightEdge, fieldValueWidth +
Blockly.BlockSvg.SEP_SPACE_X * 2);
}
inputRows.hasValue = hasValue;
inputRows.hasStatement = hasStatement;
inputRows.hasDummy = hasDummy;
return inputRows;
};
/**
* Draw the path of the block.
* Move the fields to the correct locations.
* @param {number} iconWidth Offset of first row due to icons.
* @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each
* containing position information.
* @private
*/
Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
// Should the top and bottom left corners be rounded or square?
if (this.outputConnection) {
this.squareTopLeftCorner_ = true;
this.squareBottomLeftCorner_ = true;
} else {
this.squareTopLeftCorner_ = false;
this.squareBottomLeftCorner_ = false;
// If this block is in the middle of a stack, square the corners.
if (this.previousConnection) {
var prevBlock = this.previousConnection.targetBlock();
if (prevBlock && prevBlock.getNextBlock() == this) {
this.squareTopLeftCorner_ = true;
}
}
var nextBlock = this.getNextBlock();
if (nextBlock) {
this.squareBottomLeftCorner_ = true;
}
}
// Fetch the block's coordinates on the surface for use in anchoring
// the connections.
var connectionsXY = this.getRelativeToSurfaceXY();
// Assemble the block's path.
var steps = [];
var inlineSteps = [];
// The highlighting applies to edges facing the upper-left corner.
// Since highlighting is a two-pixel wide border, it would normally overhang
// the edge of the block by a pixel. So undersize all measurements by a pixel.
var highlightSteps = [];
var highlightInlineSteps = [];
this.renderDrawTop_(steps, highlightSteps, connectionsXY,
inputRows.rightEdge);
var cursorY = this.renderDrawRight_(steps, highlightSteps, inlineSteps,
highlightInlineSteps, connectionsXY, inputRows, iconWidth);
this.renderDrawBottom_(steps, highlightSteps, connectionsXY, cursorY);
this.renderDrawLeft_(steps, highlightSteps, connectionsXY, cursorY);
var pathString = steps.join(' ') + '\n' + inlineSteps.join(' ');
this.svgPath_.setAttribute('d', pathString);
this.svgPathDark_.setAttribute('d', pathString);
pathString = highlightSteps.join(' ') + '\n' + highlightInlineSteps.join(' ');
this.svgPathLight_.setAttribute('d', pathString);
if (Blockly.RTL) {
// Mirror the block's path.
this.svgPath_.setAttribute('transform', 'scale(-1 1)');
this.svgPathLight_.setAttribute('transform', 'scale(-1 1)');
this.svgPathDark_.setAttribute('transform', 'translate(1,1) scale(-1 1)');
}
};
/**
* Render the top edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Array.<string>} highlightSteps Path of block highlights.
* @param {!Object} connectionsXY Location of block.
* @param {number} rightEdge Minimum width of block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawTop_ =
function(steps, highlightSteps, connectionsXY, rightEdge) {
// Position the cursor at the top-left starting point.
if (this.squareTopLeftCorner_) {
steps.push('m 0,0');
highlightSteps.push('m 1,1');
} else {
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
highlightSteps.push(Blockly.RTL ?
Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_RTL :
Blockly.BlockSvg.TOP_LEFT_CORNER_START_HIGHLIGHT_LTR);
// Top-left rounded corner.
steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER);
highlightSteps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_HIGHLIGHT);
}
// Top edge.
if (this.previousConnection) {
steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15);
highlightSteps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15);
steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT);
highlightSteps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT_HIGHLIGHT);
// Create previous block connection.
var connectionX = connectionsXY.x + (Blockly.RTL ?
-Blockly.BlockSvg.NOTCH_WIDTH : Blockly.BlockSvg.NOTCH_WIDTH);
var connectionY = connectionsXY.y;
this.previousConnection.moveTo(connectionX, connectionY);
// This connection will be tightened when the parent renders.
}
steps.push('H', rightEdge);
highlightSteps.push('H', rightEdge + (Blockly.RTL ? -1 : 0));
this.width = rightEdge;
};
/**
* Render the right edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Array.<string>} highlightSteps Path of block highlights.
* @param {!Array.<string>} inlineSteps Inline block outlines.
* @param {!Array.<string>} highlightInlineSteps Inline block highlights.
* @param {!Object} connectionsXY Location of block.
* @param {!Array.<!Array.<!Object>>} inputRows 2D array of objects, each
* containing position information.
* @param {number} iconWidth Offset of first row due to icons.
* @return {number} Height of block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawRight_ = function(steps, highlightSteps,
inlineSteps, highlightInlineSteps, connectionsXY, inputRows, iconWidth) {
var cursorX;
var cursorY = 0;
var connectionX, connectionY;
for (var y = 0, row; row = inputRows[y]; y++) {
cursorX = Blockly.BlockSvg.SEP_SPACE_X;
if (y == 0) {
cursorX += Blockly.RTL ? -iconWidth : iconWidth;
}
highlightSteps.push('M', (inputRows.rightEdge - 1) + ',' + (cursorY + 1));
if (this.isCollapsed()) {
// Jagged right edge.
var input = row[0];
var fieldX = cursorX;
var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT;
this.renderFields_(input.fieldRow, fieldX, fieldY);
steps.push(Blockly.BlockSvg.JAGGED_TEETH);
if (Blockly.RTL) {
highlightSteps.push('l 8,0 0,3.8 7,3.2 m -14.5,9 l 8,4');
} else {
highlightSteps.push('h 8');
}
var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT;
steps.push('v', remainder);
if (Blockly.RTL) {
highlightSteps.push('v', remainder - 2);
}
this.width += Blockly.BlockSvg.JAGGED_TEETH_WIDTH;
} else if (row.type == Blockly.BlockSvg.INLINE) {
// Inline inputs.
for (var x = 0, input; input = row[x]; x++) {
var fieldX = cursorX;
var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT;
if (row.thicker) {
// Lower the field slightly.
fieldY += Blockly.BlockSvg.INLINE_PADDING_Y;
}
// TODO: Align inline field rows (left/right/centre).
cursorX = this.renderFields_(input.fieldRow, fieldX, fieldY);
if (input.type != Blockly.DUMMY_INPUT) {
cursorX += input.renderWidth + Blockly.BlockSvg.SEP_SPACE_X;
}
if (input.type == Blockly.INPUT_VALUE) {
inlineSteps.push('M', (cursorX - Blockly.BlockSvg.SEP_SPACE_X) +
',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y));
inlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 -
input.renderWidth);
inlineSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN);
inlineSteps.push('v', input.renderHeight + 1 -
Blockly.BlockSvg.TAB_HEIGHT);
inlineSteps.push('h', input.renderWidth + 2 -
Blockly.BlockSvg.TAB_WIDTH);
inlineSteps.push('z');
if (Blockly.RTL) {
// Highlight right edge, around back of tab, and bottom.
highlightInlineSteps.push('M',
(cursorX - Blockly.BlockSvg.SEP_SPACE_X - 3 +
Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' +
(cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1));
highlightInlineSteps.push(
Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
highlightInlineSteps.push('v',
input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 3);
highlightInlineSteps.push('h',
input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 1);
} else {
// Highlight right edge, bottom.
highlightInlineSteps.push('M',
(cursorX - Blockly.BlockSvg.SEP_SPACE_X + 1) + ',' +
(cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 1));
highlightInlineSteps.push('v', input.renderHeight + 1);
highlightInlineSteps.push('h', Blockly.BlockSvg.TAB_WIDTH - 2 -
input.renderWidth);
// Short highlight glint at bottom of tab.
highlightInlineSteps.push('M',
(cursorX - input.renderWidth - Blockly.BlockSvg.SEP_SPACE_X +
1.8) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y +
Blockly.BlockSvg.TAB_HEIGHT - 0.4));
highlightInlineSteps.push('l',
(Blockly.BlockSvg.TAB_WIDTH * 0.38) + ',-1.8');
}
// Create inline input connection.
if (Blockly.RTL) {
connectionX = connectionsXY.x - cursorX -
Blockly.BlockSvg.TAB_WIDTH + Blockly.BlockSvg.SEP_SPACE_X +
input.renderWidth + 1;
} else {
connectionX = connectionsXY.x + cursorX +
Blockly.BlockSvg.TAB_WIDTH - Blockly.BlockSvg.SEP_SPACE_X -
input.renderWidth - 1;
}
connectionY = connectionsXY.y + cursorY +
Blockly.BlockSvg.INLINE_PADDING_Y + 1;
input.connection.moveTo(connectionX, connectionY);
if (input.connection.targetConnection) {
input.connection.tighten_();
}
}
}
cursorX = Math.max(cursorX, inputRows.rightEdge);
this.width = Math.max(this.width, cursorX);
steps.push('H', cursorX);
highlightSteps.push('H', cursorX + (Blockly.RTL ? -1 : 0));
steps.push('v', row.height);
if (Blockly.RTL) {
highlightSteps.push('v', row.height - 2);
}
} else if (row.type == Blockly.INPUT_VALUE) {
// External input.
var input = row[0];
var fieldX = cursorX;
var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT;
if (input.align != Blockly.ALIGN_LEFT) {
var fieldRightX = inputRows.rightEdge - input.fieldWidth -
Blockly.BlockSvg.TAB_WIDTH - 2 * Blockly.BlockSvg.SEP_SPACE_X;
if (input.align == Blockly.ALIGN_RIGHT) {
fieldX += fieldRightX;
} else if (input.align == Blockly.ALIGN_CENTRE) {
fieldX += (fieldRightX + fieldX) / 2;
}
}
this.renderFields_(input.fieldRow, fieldX, fieldY);
steps.push(Blockly.BlockSvg.TAB_PATH_DOWN);
var v = row.height - Blockly.BlockSvg.TAB_HEIGHT;
steps.push('v', v);
if (Blockly.RTL) {
// Highlight around back of tab.
highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
highlightSteps.push('v', v);
} else {
// Short highlight glint at bottom of tab.
highlightSteps.push('M', (inputRows.rightEdge - 4.2) + ',' +
(cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.4));
highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.38) +
',-1.8');
}
// Create external input connection.
connectionX = connectionsXY.x +
(Blockly.RTL ? -inputRows.rightEdge - 1 : inputRows.rightEdge + 1);
connectionY = connectionsXY.y + cursorY;
input.connection.moveTo(connectionX, connectionY);
if (input.connection.targetConnection) {
input.connection.tighten_();
this.width = Math.max(this.width, inputRows.rightEdge +
input.connection.targetBlock().getHeightWidth().width -
Blockly.BlockSvg.TAB_WIDTH + 1);
}
} else if (row.type == Blockly.DUMMY_INPUT) {
// External naked field.
var input = row[0];
var fieldX = cursorX;
var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT;
if (input.align != Blockly.ALIGN_LEFT) {
var fieldRightX = inputRows.rightEdge - input.fieldWidth -
2 * Blockly.BlockSvg.SEP_SPACE_X;
if (inputRows.hasValue) {
fieldRightX -= Blockly.BlockSvg.TAB_WIDTH;
}
if (input.align == Blockly.ALIGN_RIGHT) {
fieldX += fieldRightX;
} else if (input.align == Blockly.ALIGN_CENTRE) {
fieldX += (fieldRightX + fieldX) / 2;
}
}
this.renderFields_(input.fieldRow, fieldX, fieldY);
steps.push('v', row.height);
if (Blockly.RTL) {
highlightSteps.push('v', row.height - 2);
}
} else if (row.type == Blockly.NEXT_STATEMENT) {
// Nested statement.
var input = row[0];
if (y == 0) {
// If the first input is a statement stack, add a small row on top.
steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y);
if (Blockly.RTL) {
highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1);
}
cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
}
var fieldX = cursorX;
var fieldY = cursorY + Blockly.BlockSvg.FIELD_HEIGHT;
if (input.align != Blockly.ALIGN_LEFT) {
var fieldRightX = inputRows.statementEdge - input.fieldWidth -
2 * Blockly.BlockSvg.SEP_SPACE_X;
if (input.align == Blockly.ALIGN_RIGHT) {
fieldX += fieldRightX;
} else if (input.align == Blockly.ALIGN_CENTRE) {
fieldX += (fieldRightX + fieldX) / 2;
}
}
this.renderFields_(input.fieldRow, fieldX, fieldY);
cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH;
steps.push('H', cursorX);
steps.push(Blockly.BlockSvg.INNER_TOP_LEFT_CORNER);
steps.push('v', row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS);
steps.push(Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER);
steps.push('H', inputRows.rightEdge);
if (Blockly.RTL) {
highlightSteps.push('M',
(cursorX - Blockly.BlockSvg.NOTCH_WIDTH +
Blockly.BlockSvg.DISTANCE_45_OUTSIDE) +
',' + (cursorY + Blockly.BlockSvg.DISTANCE_45_OUTSIDE));
highlightSteps.push(
Blockly.BlockSvg.INNER_TOP_LEFT_CORNER_HIGHLIGHT_RTL);
highlightSteps.push('v',
row.height - 2 * Blockly.BlockSvg.CORNER_RADIUS);
highlightSteps.push(
Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_RTL);
highlightSteps.push('H', inputRows.rightEdge - 1);
} else {
highlightSteps.push('M',
(cursorX - Blockly.BlockSvg.NOTCH_WIDTH +
Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' +
(cursorY + row.height - Blockly.BlockSvg.DISTANCE_45_OUTSIDE));
highlightSteps.push(
Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR);
highlightSteps.push('H', inputRows.rightEdge);
}
// Create statement connection.
connectionX = connectionsXY.x + (Blockly.RTL ? -cursorX : cursorX);
connectionY = connectionsXY.y + cursorY + 1;
input.connection.moveTo(connectionX, connectionY);
if (input.connection.targetConnection) {
input.connection.tighten_();
this.width = Math.max(this.width, inputRows.statementEdge +
input.connection.targetBlock().getHeightWidth().width);
}
if (y == inputRows.length - 1 ||
inputRows[y + 1].type == Blockly.NEXT_STATEMENT) {
// If the final input is a statement stack, add a small row underneath.
// Consecutive statement stacks are also separated by a small divider.
steps.push('v', Blockly.BlockSvg.SEP_SPACE_Y);
if (Blockly.RTL) {
highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1);
}
cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
}
}
cursorY += row.height;
}
if (!inputRows.length) {
cursorY = Blockly.BlockSvg.MIN_BLOCK_Y;
steps.push('V', cursorY);
if (Blockly.RTL) {
highlightSteps.push('V', cursorY - 1);
}
}
return cursorY;
};
/**
* Render the bottom edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Array.<string>} highlightSteps Path of block highlights.
* @param {!Object} connectionsXY Location of block.
* @param {number} cursorY Height of block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawBottom_ =
function(steps, highlightSteps, connectionsXY, cursorY) {
this.height = cursorY + 1; // Add one for the shadow.
if (this.nextConnection) {
steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH + ' ' +
Blockly.BlockSvg.NOTCH_PATH_RIGHT);
// Create next block connection.
var connectionX;
if (Blockly.RTL) {
connectionX = connectionsXY.x - Blockly.BlockSvg.NOTCH_WIDTH;
} else {
connectionX = connectionsXY.x + Blockly.BlockSvg.NOTCH_WIDTH;
}
var connectionY = connectionsXY.y + cursorY + 1;
this.nextConnection.moveTo(connectionX, connectionY);
if (this.nextConnection.targetConnection) {
this.nextConnection.tighten_();
}
this.height += 4; // Height of tab.
}
// Should the bottom-left corner be rounded or square?
if (this.squareBottomLeftCorner_) {
steps.push('H 0');
if (!Blockly.RTL) {
highlightSteps.push('M', '1,' + cursorY);
}
} else {
steps.push('H', Blockly.BlockSvg.CORNER_RADIUS);
steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' +
Blockly.BlockSvg.CORNER_RADIUS + ',-' +
Blockly.BlockSvg.CORNER_RADIUS);
if (!Blockly.RTL) {
highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' +
(cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE));
highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 1) + ',' +
(Blockly.BlockSvg.CORNER_RADIUS - 1) + ' 0 0,1 ' +
'1,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS));
}
}
};
/**
* Render the left edge of the block.
* @param {!Array.<string>} steps Path of block outline.
* @param {!Array.<string>} highlightSteps Path of block highlights.
* @param {!Object} connectionsXY Location of block.
* @param {number} cursorY Height of block.
* @private
*/
Blockly.BlockSvg.prototype.renderDrawLeft_ =
function(steps, highlightSteps, connectionsXY, cursorY) {
if (this.outputConnection) {
// Create output connection.
this.outputConnection.moveTo(connectionsXY.x, connectionsXY.y);
// This connection will be tightened when the parent renders.
steps.push('V', Blockly.BlockSvg.TAB_HEIGHT);
steps.push('c 0,-10 -' + Blockly.BlockSvg.TAB_WIDTH + ',8 -' +
Blockly.BlockSvg.TAB_WIDTH + ',-7.5 s ' + Blockly.BlockSvg.TAB_WIDTH +
',2.5 ' + Blockly.BlockSvg.TAB_WIDTH + ',-7.5');
if (Blockly.RTL) {
highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.3) + ',8.9');
highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1');
} else {
highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1);
highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) +
',-1 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) +
',-5.5 0,-11');
highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) +
',1 V 1 H 2');
}
this.width += Blockly.BlockSvg.TAB_WIDTH;
} else if (!Blockly.RTL) {
if (this.squareTopLeftCorner_) {
highlightSteps.push('V', 1);
} else {
highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
}
}
steps.push('z');
};