} steps Path of block outline.
+ * @param {!Object} connectionsXY Location of block.
+ * @param {!Object} metrics An object containing computed measurements of the
+ * block.
+ * @private
+ */
+Blockly.BlockSvg.prototype.renderDrawTop_ = function(steps, connectionsXY, metrics) {
+ if (!this.isShadow()) {
+ steps.push('a', Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 -' +
+ Blockly.BlockSvg.CORNER_RADIUS + ',-' +
+ Blockly.BlockSvg.CORNER_RADIUS);
+ } else {
+ steps.push(
+ 'a', metrics.fieldRadius + ',' + metrics.fieldRadius,
+ '0', '0,0', '-' + metrics.fieldRadius + ',-' + metrics.fieldRadius);
+ }
+ steps.push('z');
+};
+
+/**
+ * Get the field shadow block, if this block has one.
+ * This is horizontal Scratch-specific, as "fields" are implemented as inputs
+ * with shadow blocks, and there is only one per block.
+ * @return {Blockly.BlockSvg} The field shadow block, or null if not found.
+ * @private
+ */
+Blockly.BlockSvg.prototype.getFieldShadowBlock_ = function() {
+ for (var i = 0, child; child = this.childBlocks_[i]; i++) {
+ if (child.isShadow()) {
+ return child;
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Position an new block correctly, so that it doesn't move the existing block
+ * when connected to it.
+ * @param {!Blockly.Block} newBlock The block to position - either the first
+ * block in a dragged stack or an insertion marker.
+ * @param {!Blockly.Connection} newConnection The connection on the new block's
+ * stack - either a connection on newBlock, or the last NEXT_STATEMENT
+ * connection on the stack if the stack's being dropped before another
+ * block.
+ * @param {!Blockly.Connection} existingConnection The connection on the
+ * existing block, which newBlock should line up with.
+ */
+Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, existingConnection) {
+ // 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 (newConnection.type == Blockly.NEXT_STATEMENT) {
+ var dx = existingConnection.x_ - newConnection.x_;
+ var dy = existingConnection.y_ - newConnection.y_;
+
+ // When putting a c-block around another c-block, the outer block must
+ // positioned above the inner block, as its connection point will stretch
+ // downwards when connected.
+ if (newConnection == newBlock.getFirstStatementConnection()) {
+ dy -= existingConnection.sourceBlock_.getHeightWidth(true).height -
+ Blockly.BlockSvg.MIN_BLOCK_Y;
+ }
+
+ newBlock.moveBy(dx, dy);
+ }
+};
diff --git a/core/block_render_svg_vertical.js b/core/block_render_svg_vertical.js
new file mode 100644
index 0000000000..0800d1c8d5
--- /dev/null
+++ b/core/block_render_svg_vertical.js
@@ -0,0 +1,774 @@
+/**
+ * @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.render');
+
+goog.require('Blockly.BlockSvg');
+
+
+// UI constants for rendering blocks.
+/**
+* Grid unit to pixels conversion
+* @const
+*/
+Blockly.BlockSvg.GRID_UNIT = 4;
+/**
+ * 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 = 4;
+/**
+ * Do blocks with no previous or output connections have a 'hat' on top?
+ * @const
+ */
+Blockly.BlockSvg.START_HAT = true;
+/**
+ * Height of the top hat.
+ * @const
+ */
+Blockly.BlockSvg.START_HAT_HEIGHT = 15;
+/**
+ * Path of the top hat's curve.
+ * @const
+ */
+Blockly.BlockSvg.START_HAT_PATH = 'c 30,-' +
+ Blockly.BlockSvg.START_HAT_HEIGHT + ' 70,-' +
+ Blockly.BlockSvg.START_HAT_HEIGHT + ' 100,0';
+/**
+ * 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 right to left.
+ * @const
+ */
+Blockly.BlockSvg.NOTCH_PATH_RIGHT = 'l -6,4 -3,0 -6,-4';
+/**
+ * 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 start point for drawing the top-left corner.
+ * @const
+ */
+Blockly.BlockSvg.TOP_LEFT_CORNER_START =
+ 'm 0,' + Blockly.BlockSvg.CORNER_RADIUS;
+/**
+ * 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 rounded top-right corner.
+ * @const
+ */
+Blockly.BlockSvg.TOP_RIGHT_CORNER =
+ 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 ' +
+ Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS;
+
+/**
+ * SVG path for drawing the rounded bottom-right corner.
+ * @const
+ */
+Blockly.BlockSvg.BOTTOM_RIGHT_CORNER =
+ ' a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' +
+ Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS;
+
+/**
+ * SVG path for drawing the rounded bottom-left corner.
+ * @const
+ */
+Blockly.BlockSvg.BOTTOM_LEFT_CORNER =
+ 'a ' + Blockly.BlockSvg.CORNER_RADIUS + ',' +
+ Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,1 -' +
+ Blockly.BlockSvg.CORNER_RADIUS + ',-' +
+ Blockly.BlockSvg.CORNER_RADIUS;
+/**
+ * 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;
+
+/**
+ * Height of user inputs
+ * @const
+ */
+Blockly.BlockSvg.FIELD_HEIGHT = 8 * Blockly.BlockSvg.GRID_UNIT;
+
+/**
+ * Width of user inputs
+ * @const
+ */
+Blockly.BlockSvg.FIELD_WIDTH = 12 * Blockly.BlockSvg.GRID_UNIT;
+
+/**
+ * Minimum width of user inputs during editing
+ * @const
+ */
+Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT = 13 * Blockly.BlockSvg.GRID_UNIT;
+
+/**
+ * Maximum width of user inputs during editing
+ * @const
+ */
+Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT = Infinity;
+
+/**
+ * Maximum height of user inputs during editing
+ * @const
+ */
+Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT = Blockly.BlockSvg.FIELD_WIDTH;
+
+/**
+ * Top padding of user inputs
+ * @const
+ */
+Blockly.BlockSvg.FIELD_TOP_PADDING = 0;
+
+/**
+ * Max text display length for a field (per-horizontal/vertical)
+ * @const
+ */
+Blockly.BlockSvg.MAX_DISPLAY_LENGTH = Infinity;
+
+/**
+ * Change the colour of a block.
+ */
+Blockly.BlockSvg.prototype.updateColour = function() {
+ var strokeColour = this.getColourTertiary();
+ if (this.isShadow() && this.parentBlock_) {
+ // Pull shadow block stroke colour from parent block's tertiary if possible.
+ strokeColour = this.parentBlock_.getColourTertiary();
+ }
+
+ // Render block stroke
+ this.svgPath_.setAttribute('stroke', strokeColour);
+
+ // Render block fill
+ var fillColour = (this.isGlowingBlock_) ? this.getColourSecondary() : this.getColour();
+ this.svgPath_.setAttribute('fill', fillColour);
+
+ // Render opacity
+ this.svgPath_.setAttribute('fill-opacity', this.getOpacity());
+
+ // Render icon(s) if applicable
+ var icons = this.getIcons();
+ for (var i = 0; i < icons.length; i++) {
+ icons[i].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);
+ }
+ }
+};
+
+/**
+ * Returns a bounding box describing the dimensions of this block
+ * and any blocks stacked below it.
+ * @return {!{height: number, width: number}} 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);
+ } else if (!this.nextConnection && !this.outputConnection) {
+ // Add a bit of margin under blocks with no bottom tab.
+ height += 2;
+ }
+ return {height: height, width: width};
+};
+
+/**
+ * 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) {
+ Blockly.Field.startCache();
+ this.rendered = true;
+
+ var cursorX = Blockly.BlockSvg.SEP_SPACE_X;
+ if (this.RTL) {
+ cursorX = -cursorX;
+ }
+ // Move the icons into position.
+ var icons = this.getIcons();
+ for (var i = 0; i < icons.length; i++) {
+ cursorX = icons[i].renderIcon(cursorX);
+ }
+ cursorX += this.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.asyncSvgResize(this.workspace);
+ }
+ }
+ Blockly.Field.stopCache();
+};
+
+/**
+ * Render a list of fields starting at the specified location.
+ * @param {!Array.} 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) {
+ cursorY += Blockly.BlockSvg.INLINE_PADDING_Y;
+ if (this.RTL) {
+ cursorX = -cursorX;
+ }
+ for (var t = 0, field; field = fieldList[t]; t++) {
+ var root = field.getSvgRoot();
+ if (!root) {
+ continue;
+ }
+ if (this.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 this.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.>} 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;
+
+ for (var i = 0, input; input = inputList[i]; i++) {
+ if (!input.isVisible()) {
+ continue;
+ }
+ var row;
+ if (!lastType ||
+ lastType == Blockly.NEXT_STATEMENT ||
+ input.type == Blockly.NEXT_STATEMENT) {
+ // Create new row.
+ lastType = input.type;
+ row = [];
+ if (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 (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.isConnected()) {
+ 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);
+ }
+
+ 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 += this.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.>} inputRows 2D array of objects, each
+ * containing position information.
+ * @private
+ */
+Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
+ this.startHat_ = false;
+ // Should the top left corners be rounded or square?
+ // Currently, it is squared only if it's a hat.
+ this.squareTopLeftCorner_ = false;
+ if (Blockly.BlockSvg.START_HAT && !this.outputConnection && !this.previousConnection) {
+ // No output or previous connection.
+ this.squareTopLeftCorner_ = true;
+ this.startHat_ = true;
+ inputRows.rightEdge = Math.max(inputRows.rightEdge, 100);
+ }
+
+ // 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 = [];
+
+ this.renderDrawTop_(steps, connectionsXY,
+ inputRows.rightEdge);
+ var cursorY = this.renderDrawRight_(steps,
+ connectionsXY, inputRows, iconWidth);
+ this.renderDrawBottom_(steps, connectionsXY, cursorY);
+ this.renderDrawLeft_(steps, connectionsXY, cursorY);
+
+ var pathString = steps.join(' ');
+ this.svgPath_.setAttribute('d', pathString);
+
+ if (this.RTL) {
+ // Mirror the block's path.
+ // This is awesome.
+ this.svgPath_.setAttribute('transform', 'scale(-1 1)');
+ }
+};
+
+/**
+ * Render the top edge of the block.
+ * @param {!Array.} steps Path of block outline.
+ * @param {!Object} connectionsXY Location of block.
+ * @param {number} rightEdge Minimum width of block.
+ * @private
+ */
+Blockly.BlockSvg.prototype.renderDrawTop_ =
+ function(steps, connectionsXY, rightEdge) {
+ // Position the cursor at the top-left starting point.
+ if (this.squareTopLeftCorner_) {
+ steps.push('m 0,0');
+ if (this.startHat_) {
+ steps.push(Blockly.BlockSvg.START_HAT_PATH);
+ }
+ } else {
+ steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
+ // Top-left rounded corner.
+ steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER);
+ }
+
+ // Top edge.
+ if (this.previousConnection) {
+ steps.push('H', Blockly.BlockSvg.NOTCH_WIDTH - 15);
+ steps.push(Blockly.BlockSvg.NOTCH_PATH_LEFT);
+ // Create previous block connection.
+ var connectionX = connectionsXY.x + (this.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);
+ this.width = rightEdge;
+};
+
+/**
+ * Render the right edge of the block.
+ * @param {!Array.} steps Path of block outline.
+ * @param {!Object} connectionsXY Location of block.
+ * @param {!Array.>} 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,
+ connectionsXY, inputRows, iconWidth) {
+ var cursorX;
+ var cursorY = -1; // Blocks overhang their parent by 1px.
+ var connectionX, connectionY;
+ for (var y = 0, row; row = inputRows[y]; y++) {
+ cursorX = Blockly.BlockSvg.SEP_SPACE_X;
+ if (y == 0) {
+ cursorX += this.RTL ? -iconWidth : iconWidth;
+ }
+
+ if (row.type == Blockly.BlockSvg.INLINE) {
+ // Inline inputs.
+ for (var x = 0, input; input = row[x]; x++) {
+ var fieldX = cursorX;
+ var fieldY = cursorY;
+ 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) {
+ // Create inline input connection.
+ if (this.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.isConnected()) {
+ input.connection.tighten_();
+ }
+ }
+ }
+
+ cursorX = Math.max(cursorX, inputRows.rightEdge);
+ this.width = Math.max(this.width, cursorX);
+ steps.push('H', cursorX);
+ steps.push(Blockly.BlockSvg.TOP_RIGHT_CORNER);
+ // Subtract CORNER_RADIUS * 2 to account for the top right corner
+ // and also the bottom right corner. Only move vertically the non-corner length.
+ steps.push('v', row.height - Blockly.BlockSvg.CORNER_RADIUS * 2);
+ } else if (row.type == Blockly.DUMMY_INPUT) {
+ // External naked field.
+ var input = row[0];
+ var fieldX = cursorX;
+ var fieldY = cursorY;
+ 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 / 2;
+ }
+ }
+ this.renderFields_(input.fieldRow, fieldX, fieldY);
+ steps.push('v', row.height);
+ } else if (row.type == Blockly.NEXT_STATEMENT) {
+ // Nested statement.
+ var input = row[0];
+ var fieldX = cursorX;
+ var fieldY = cursorY;
+ 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 / 2;
+ }
+ }
+ this.renderFields_(input.fieldRow, fieldX, fieldY);
+ cursorX = inputRows.statementEdge + Blockly.BlockSvg.NOTCH_WIDTH;
+ steps.push(Blockly.BlockSvg.BOTTOM_RIGHT_CORNER);
+ 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);
+
+ // Create statement connection.
+ connectionX = connectionsXY.x + (this.RTL ? -cursorX : cursorX + 1);
+ connectionY = connectionsXY.y + cursorY + 1;
+ input.connection.moveTo(connectionX, connectionY);
+ if (input.connection.isConnected()) {
+ 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);
+ cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
+ }
+ }
+ cursorY += row.height;
+ }
+ if (!inputRows.length) {
+ cursorY = Blockly.BlockSvg.MIN_BLOCK_Y;
+ steps.push('V', cursorY);
+ }
+ return cursorY;
+};
+
+/**
+ * Render the bottom edge of the block.
+ * @param {!Array.} steps Path of block outline.
+ * @param {!Object} connectionsXY Location of block.
+ * @param {number} cursorY Height of block.
+ * @private
+ */
+Blockly.BlockSvg.prototype.renderDrawBottom_ = function(steps, connectionsXY,
+ cursorY) {
+ this.height = cursorY;
+ steps.push(Blockly.BlockSvg.BOTTOM_RIGHT_CORNER);
+ if (this.nextConnection) {
+ steps.push('H', (Blockly.BlockSvg.NOTCH_WIDTH + (this.RTL ? 0.5 : - 0.5)) +
+ ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT);
+ // Create next block connection.
+ var connectionX;
+ if (this.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.isConnected()) {
+ this.nextConnection.tighten_();
+ }
+ this.height += 4; // Height of tab.
+ }
+ // Bottom horizontal line
+ steps.push('H', Blockly.BlockSvg.CORNER_RADIUS);
+ // Bottom left corner
+ steps.push(Blockly.BlockSvg.BOTTOM_LEFT_CORNER);
+};
+
+/**
+ * Render the left edge of the block.
+ * @param {!Array.} steps Path of block outline.
+ * @param {!Object} connectionsXY Location of block.
+ * @param {number} cursorY Height of block.
+ * @private
+ */
+Blockly.BlockSvg.prototype.renderDrawLeft_ =
+ function(steps, 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('z');
+};
+
+/**
+ * Position an new block correctly, so that it doesn't move the existing block
+ * when connected to it.
+ * @param {!Blockly.Block} newBlock The block to position - either the first
+ * block in a dragged stack or an insertion marker.
+ * @param {!Blockly.Connection} newConnection The connection on the new block's
+ * stack - either a connection on newBlock, or the last NEXT_STATEMENT
+ * connection on the stack if the stack's being dropped before another
+ * block.
+ * @param {!Blockly.Connection} existingConnection The connection on the
+ * existing block, which newBlock should line up with.
+ */
+Blockly.BlockSvg.prototype.positionNewBlock =
+ function(newBlock, newConnection, existingConnection) {
+ // 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 (newConnection.type == Blockly.NEXT_STATEMENT) {
+ var dx = existingConnection.x_ - newConnection.x_;
+ var dy = existingConnection.y_ - newConnection.y_;
+
+ newBlock.moveBy(dx, dy);
+ }
+};
diff --git a/core/block_svg.js b/core/block_svg.js
index 8bf9625330..c62162199d 100644
--- a/core/block_svg.js
+++ b/core/block_svg.js
@@ -28,6 +28,7 @@ goog.provide('Blockly.BlockSvg');
goog.require('Blockly.Block');
goog.require('Blockly.ContextMenu');
+goog.require('Blockly.RenderedConnection');
goog.require('goog.Timer');
goog.require('goog.asserts');
goog.require('goog.dom');
@@ -48,19 +49,19 @@ goog.require('goog.userAgent');
*/
Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
// Create core elements for the block.
- /** @type {SVGElement} */
+ /**
+ * @type {SVGElement}
+ * @private
+ */
this.svgGroup_ = Blockly.createSvgElement('g', {}, null);
/** @type {SVGElement} */
- this.svgPathDark_ = Blockly.createSvgElement('path',
- {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'},
- this.svgGroup_);
- /** @type {SVGElement} */
this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'},
this.svgGroup_);
- /** @type {SVGElement} */
- this.svgPathLight_ = Blockly.createSvgElement('path',
- {'class': 'blocklyPathLight'}, this.svgGroup_);
this.svgPath_.tooltip = this;
+
+ /** @type {boolean} */
+ this.rendered = false;
+
Blockly.Tooltip.bindMouseEvents(this.svgPath_);
Blockly.BlockSvg.superClass_.constructor.call(this,
workspace, prototypeName, opt_id);
@@ -69,13 +70,23 @@ goog.inherits(Blockly.BlockSvg, Blockly.Block);
/**
* Height of this block, not including any statement blocks above or below.
+ * @type {number}
*/
Blockly.BlockSvg.prototype.height = 0;
+
/**
* Width of this block, including any connected value blocks.
+ * @type {number}
*/
Blockly.BlockSvg.prototype.width = 0;
+/**
+ * Opacity of this block between 0 and 1.
+ * @type {number}
+ * @private
+ */
+Blockly.BlockSvg.prototype.opacity_ = 1;
+
/**
* Original location of block being dragged.
* @type {goog.math.Coordinate}
@@ -83,6 +94,20 @@ Blockly.BlockSvg.prototype.width = 0;
*/
Blockly.BlockSvg.prototype.dragStartXY_ = null;
+/**
+ * Whether the block glows as if running.
+ * @type {boolean}
+ * @private
+ */
+Blockly.BlockSvg.prototype.isGlowingBlock_ = false;
+
+/**
+ * Whether the block's whole stack glows as if running.
+ * @type {boolean}
+ * @private
+ */
+Blockly.BlockSvg.prototype.isGlowingStack_ = false;
+
/**
* Constant for identifying rows that are to be rendered inline.
* Don't collide with Blockly.INPUT_VALUE and friends.
@@ -96,12 +121,14 @@ Blockly.BlockSvg.INLINE = -1;
*/
Blockly.BlockSvg.prototype.initSvg = function() {
goog.asserts.assert(this.workspace.rendered, '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();
+ if (!this.isInsertionMarker()) { // Insertion markers not allowed to have inputs or icons
+ for (var i = 0, input; input = this.inputList[i]; i++) {
+ input.init();
+ }
+ var icons = this.getIcons();
+ for (i = 0; i < icons.length; i++) {
+ icons[i].createIcon();
+ }
}
this.updateColour();
this.updateMovable();
@@ -112,11 +139,6 @@ Blockly.BlockSvg.prototype.initSvg = function() {
Blockly.bindEvent_(this.getSvgRoot(), 'touchstart', null,
function(e) {Blockly.longStart_(e, thisBlock);});
}
- // Bind an onchange function, if it exists.
- if (goog.isFunction(this.onchange) && !this.eventsInit_) {
- this.onchangeWrapper_ = Blockly.bindEvent_(this.workspace.getCanvas(),
- 'blocklyWorkspaceChange', this, this.onchange);
- }
this.eventsInit_ = true;
if (!this.getSvgRoot().parentNode) {
@@ -128,22 +150,65 @@ Blockly.BlockSvg.prototype.initSvg = function() {
* 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();
Blockly.selected.unselect();
+ 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();
- Blockly.fireUiEvent(this.workspace.getCanvas(), 'blocklySelectChange');
};
/**
* 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();
- Blockly.fireUiEvent(this.workspace.getCanvas(), 'blocklySelectChange');
+};
+
+/**
+ * Glow only this particular block, to highlight it visually as if it's running.
+ * @param {boolean} isGlowingBlock Whether the block should glow.
+ */
+Blockly.BlockSvg.prototype.setGlowBlock = function(isGlowingBlock) {
+ this.isGlowingBlock_ = isGlowingBlock;
+ this.updateColour();
+};
+
+/**
+ * Glow the stack starting with this block, to highlight it visually as if it's running.
+ * @param {boolean} isGlowingStack Whether the stack starting with this block should glow.
+ */
+Blockly.BlockSvg.prototype.setGlowStack = function(isGlowingStack) {
+ this.isGlowingStack_ = isGlowingStack;
+ // Update the applied SVG filter if the property has changed
+ var svg = this.getSvgRoot();
+ if (this.isGlowingStack_ && !svg.hasAttribute('filter')) {
+ svg.setAttribute('filter', 'url(#blocklyStackGlowFilter)');
+ } else if (!this.isGlowingStack_ && svg.hasAttribute('filter')) {
+ svg.removeAttribute('filter');
+ }
};
/**
@@ -201,7 +266,6 @@ Blockly.BlockSvg.onMouseMoveWrapper_ = null;
* @private
*/
Blockly.BlockSvg.terminateDrag_ = function() {
- Blockly.BlockSvg.disconnectUiStop_();
if (Blockly.BlockSvg.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_);
Blockly.BlockSvg.onMouseUpWrapper_ = null;
@@ -211,26 +275,47 @@ Blockly.BlockSvg.terminateDrag_ = function() {
Blockly.BlockSvg.onMouseMoveWrapper_ = null;
}
var selected = Blockly.selected;
- if (Blockly.dragMode_ == 2) {
+ if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
// Terminate a drag operation.
if (selected) {
+ if (Blockly.insertionMarker_) {
+ Blockly.Events.disable();
+ if (Blockly.insertionMarkerConnection_) {
+ Blockly.BlockSvg.disconnectInsertionMarker();
+ }
+ Blockly.insertionMarker_.dispose();
+ Blockly.insertionMarker_ = null;
+ Blockly.Events.enable();
+ }
// Update the connection locations.
var xy = selected.getRelativeToSurfaceXY();
var dxy = goog.math.Coordinate.difference(xy, selected.dragStartXY_);
+ var event = new Blockly.Events.Move(selected);
+ event.oldCoordinate = selected.dragStartXY_;
+ event.recordNew();
+ Blockly.Events.fire(event);
selected.moveConnections_(dxy.x, dxy.y);
delete selected.draggedBubbles_;
selected.setDragging_(false);
+ selected.moveOffDragSurface_();
selected.render();
- goog.Timer.callOnce(
- selected.snapToGrid, Blockly.BUMP_DELAY / 2, selected);
- goog.Timer.callOnce(
- selected.bumpNeighbours_, Blockly.BUMP_DELAY, selected);
+ // Ensure that any stap and bump are part of this move's event group.
+ var group = Blockly.Events.getGroup();
+ setTimeout(function() {
+ Blockly.Events.setGroup(group);
+ selected.snapToGrid();
+ Blockly.Events.setGroup(false);
+ }, Blockly.BUMP_DELAY / 2);
+ setTimeout(function() {
+ Blockly.Events.setGroup(group);
+ selected.bumpNeighbours_();
+ Blockly.Events.setGroup(false);
+ }, Blockly.BUMP_DELAY);
// Fire an event to allow scrollbars to resize.
- Blockly.fireUiEvent(window, 'resize');
- selected.workspace.fireChangeEvent();
+ Blockly.asyncSvgResize(this.workspace);
}
}
- Blockly.dragMode_ = 0;
+ Blockly.dragMode_ = Blockly.DRAG_NONE;
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
};
@@ -239,12 +324,19 @@ Blockly.BlockSvg.terminateDrag_ = function() {
* @param {Blockly.BlockSvg} newParent New parent block.
*/
Blockly.BlockSvg.prototype.setParent = function(newParent) {
+ if (newParent == this.parentBlock_) {
+ return;
+ }
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 + ')');
+ // Avoid moving a block up the DOM if it's currently selected/dragging,
+ // so as to avoid taking things off the drag surface.
+ if (Blockly.selected != this) {
+ this.workspace.getCanvas().appendChild(svgRoot);
+ this.translate(xy.x, xy.y);
+ }
}
Blockly.Field.startCache();
@@ -257,6 +349,11 @@ Blockly.BlockSvg.prototype.setParent = function(newParent) {
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 a shadow block, inherit tertiary colour.
+ if (this.isShadow()) {
+ this.setColour(this.getColour(), this.getColourSecondary(),
+ newParent.getColourTertiary());
+ }
}
};
@@ -266,8 +363,12 @@ Blockly.BlockSvg.prototype.setParent = function(newParent) {
* @return {!goog.math.Coordinate} Object with .x and .y properties.
*/
Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
+ // The drawing surface is relative to either the workspace canvas
+ // or to the drag surface group.
var x = 0;
var y = 0;
+ var dragSurfaceGroup = (this.workspace.dragSurface) ?
+ this.workspace.dragSurface.getGroup() : null;
var element = this.getSvgRoot();
if (element) {
do {
@@ -275,8 +376,17 @@ Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
var xy = Blockly.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.workspace.dragSurface &&
+ this.workspace.dragSurface.getCurrentBlock() == element) {
+ var surfaceTranslation = this.workspace.dragSurface.getSurfaceTranslation();
+ x += surfaceTranslation.x;
+ y += surfaceTranslation.y;
+ }
element = element.parentNode;
- } while (element && element != this.workspace.getCanvas());
+ } while (element && element != this.workspace.getCanvas() &&
+ element != dragSurfaceGroup);
}
return new goog.math.Coordinate(x, y);
};
@@ -287,10 +397,32 @@ Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
* @param {number} dy Vertical offset.
*/
Blockly.BlockSvg.prototype.moveBy = function(dx, dy) {
+ goog.asserts.assert(!this.parentBlock_, 'Block has parent.');
+ var eventsEnabled = Blockly.Events.isEnabled();
+ if (eventsEnabled) {
+ var event = new Blockly.Events.Move(this);
+ }
var xy = this.getRelativeToSurfaceXY();
- this.getSvgRoot().setAttribute('transform',
- 'translate(' + (xy.x + dx) + ',' + (xy.y + dy) + ')');
+ this.translate(xy.x + dx, xy.y + dy);
this.moveConnections_(dx, dy);
+ if (eventsEnabled) {
+ event.recordNew();
+ Blockly.Events.fire(event);
+ }
+};
+
+/**
+ * Set this block to an absolute translation.
+ * @param {number} x Horizontal translation.
+ * @param {number} y Vertical translation.
+ * @param {boolean=} opt_use3d If set, use 3d translation.
+*/
+Blockly.BlockSvg.prototype.translate = function(x, y, opt_use3d) {
+ if (opt_use3d) {
+ this.getSvgRoot().setAttribute('style', 'transform: translate3d(' + x + 'px,' + y + 'px, 0px)');
+ } else {
+ this.getSvgRoot().setAttribute('transform', 'translate(' + x + ',' + y + ')');
+ }
};
/**
@@ -300,7 +432,7 @@ Blockly.BlockSvg.prototype.snapToGrid = function() {
if (!this.workspace) {
return; // Deleted block.
}
- if (Blockly.dragMode_ != 0) {
+ if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
return; // Don't bump blocks during a drag.
}
if (this.getParent()) {
@@ -328,7 +460,8 @@ Blockly.BlockSvg.prototype.snapToGrid = function() {
/**
* Returns a bounding box describing the dimensions of this block
* and any blocks stacked below it.
- * @return {!{height: number, width: number}} Object with height and width properties.
+ * @return {!{height: number, width: number}} Object with height and width
+ * properties.
*/
Blockly.BlockSvg.prototype.getHeightWidth = function() {
var height = this.height;
@@ -346,6 +479,56 @@ Blockly.BlockSvg.prototype.getHeightWidth = function() {
return {height: height, width: width};
};
+/**
+ * Returns the coordinates of a bounding box describing the dimensions of this
+ * block and any blocks stacked below it.
+ * @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 block opacity for SVG rendering.
+ * @param {number} opacity Intended opacity, betweeen 0 and 1
+ */
+Blockly.BlockSvg.prototype.setOpacity = function(opacity) {
+ this.opacity_ = opacity;
+ if (this.rendered) {
+ this.updateColour();
+ }
+};
+
+/**
+ * Get block opacity for SVG rendering.
+ * @return {number} Intended opacity, betweeen 0 and 1
+ */
+Blockly.BlockSvg.prototype.getOpacity = function() {
+ return this.opacity_;
+};
+
/**
* Set whether the block is collapsed or not.
* @param {boolean} collapsed True if collapsed.
@@ -363,7 +546,7 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) {
var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
if (collapsed) {
var icons = this.getIcons();
- for (var i = 0; i < icons.length; i++) {
+ for (i = 0; i < icons.length; i++) {
icons[i].setVisible(false);
}
var text = this.toString(Blockly.COLLAPSE_CHARS);
@@ -388,7 +571,6 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) {
// all their functions and store them next to each other. Expanding and
// bumping causes all their definitions to go out of alignment.
}
- this.workspace.fireChangeEvent();
};
/**
@@ -414,7 +596,7 @@ Blockly.BlockSvg.prototype.tab = function(start, forward) {
}
}
}
- var i = list.indexOf(start);
+ i = list.indexOf(start);
if (i == -1) {
// No start location, start at the beginning or end.
i = forward ? -1 : list.length;
@@ -439,6 +621,9 @@ Blockly.BlockSvg.prototype.tab = function(start, forward) {
* @private
*/
Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
+ if (this.workspace.options.readOnly) {
+ return;
+ }
if (this.isInFlyout) {
e.stopPropagation();
return;
@@ -449,23 +634,26 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
Blockly.terminateDrag_();
this.select();
Blockly.hideChaff();
+ this.workspace.recordDeleteAreas();
if (Blockly.isRightButton(e)) {
// Right-click.
this.showContextMenu_(e);
} else if (!this.isMovable()) {
- // Allow unmovable blocks to be selected and context menued, but not
+ // Allow immovable 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 {
+ if (!Blockly.Events.getGroup()) {
+ Blockly.Events.setGroup(true);
+ }
// Left-click (or middle click)
- Blockly.removeAllRanges();
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
this.dragStartXY_ = this.getRelativeToSurfaceXY();
- this.workspace.startDrag(e, this.dragStartXY_.x, this.dragStartXY_.y);
+ this.workspace.startDrag(e, this.dragStartXY_);
- Blockly.dragMode_ = 1;
+ Blockly.dragMode_ = Blockly.DRAG_STICKY;
Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEvent_(document,
'mouseup', this, this.onMouseUp_);
Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEvent_(document,
@@ -484,6 +672,7 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
+ e.preventDefault();
};
/**
@@ -494,20 +683,23 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
* @private
*/
Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
+ var isNotShadowBlock = this.ioClickHackIsNotShadow_(e);
+ if (Blockly.dragMode_ != Blockly.DRAG_FREE && !Blockly.WidgetDiv.isVisible() && isNotShadowBlock) {
+ Blockly.Events.fire(
+ new Blockly.Events.Ui(this, 'click', undefined, undefined));
+ }
Blockly.terminateDrag_();
if (Blockly.selected && Blockly.highlightedConnection_) {
+ this.positionNewBlock(Blockly.selected,
+ Blockly.localConnection_, 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();
+ var inferiorConnection = Blockly.localConnection_.isSuperior() ?
+ Blockly.highlightedConnection_ : Blockly.localConnection_;
+ inferiorConnection.getSourceBlock().connectionUiEffect();
}
if (this.workspace.trashcan) {
// Don't throw an object in the trash can if it just got connected.
@@ -523,13 +715,38 @@ Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
// 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');
+ Blockly.asyncSvgResize(this.workspace);
}
if (Blockly.highlightedConnection_) {
- Blockly.highlightedConnection_.unhighlight();
Blockly.highlightedConnection_ = null;
}
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
+ if (!Blockly.WidgetDiv.isVisible()) {
+ Blockly.Events.setGroup(false);
+ }
+};
+
+/**
+ * XXX: Hack to fix drop-down clicking issue for Google I/O.
+ * We cannot just check isShadow, since `this` is the parent block.
+ * See: https://github.com/google/blockly/issues/336
+ * @param {!Event} e Mouse up event.
+ * @return {boolean} True if the block is not the drop-down shadow.
+ */
+Blockly.BlockSvg.prototype.ioClickHackIsNotShadow_ = function(e) {
+ // True if click target is a non-shadow block path.
+ if (e.target === this.svgPath_ &&
+ e.target.parentNode === this.getSvgRoot()) {
+ return true;
+ }
+ for (var i = 0, input; input = this.inputList[i]; i++) {
+ for (var j = 0, field; field = input.fieldRow[j]; j++) {
+ if (field.imageElement_ && field.imageElement_ === e.target) {
+ return true;
+ }
+ }
+ }
+ return false;
};
/**
@@ -539,7 +756,8 @@ Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
Blockly.BlockSvg.prototype.showHelp_ = function() {
var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl;
if (url) {
- window.open(url);
+ // @todo rewrite
+ alert(url);
}
};
@@ -570,8 +788,7 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
}
menuOptions.push(duplicateOption);
- if (this.isEditable() && !this.collapsed_ &&
- this.workspace.options.comments) {
+ if (this.isEditable() && this.workspace.options.comments) {
// Option to add/remove a comment.
var commentOption = {enabled: !goog.userAgent.IE};
if (this.comment) {
@@ -588,72 +805,22 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
menuOptions.push(commentOption);
}
- // Option to make block inline.
- if (!this.collapsed_) {
- 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;
- }
- }
- }
-
- if (this.workspace.options.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);
- };
- menuOptions.push(expandOption);
- } else {
- var collapseOption = {enabled: true};
- collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK;
- collapseOption.callback = function() {
- block.setCollapsed(true);
- };
- menuOptions.push(collapseOption);
- }
- }
-
- if (this.workspace.options.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);
- }
- };
- menuOptions.push(disableOption);
- }
-
// Option to delete this block.
// Count the number of blocks that are nested in this block.
- var descendantCount = this.getDescendants().length;
+ var descendantCount = this.getDescendants(true).length;
var nextBlock = this.getNextBlock();
if (nextBlock) {
// Blocks in the current stack would survive this block's deletion.
- descendantCount -= nextBlock.getDescendants().length;
+ descendantCount -= nextBlock.getDescendants(true).length;
}
var deleteOption = {
text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK :
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)),
enabled: true,
callback: function() {
+ Blockly.Events.setGroup(true);
block.dispose(true, true);
+ Blockly.Events.setGroup(false);
}
};
menuOptions.push(deleteOption);
@@ -695,12 +862,12 @@ Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) {
myConnections[i].moveBy(dx, dy);
}
var icons = this.getIcons();
- for (var i = 0; i < icons.length; i++) {
+ for (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++) {
+ for (i = 0; i < this.childBlocks_.length; i++) {
this.childBlocks_[i].moveConnections_(dx, dy);
}
};
@@ -713,8 +880,11 @@ Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) {
Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
if (adding) {
this.addDragging();
+ Blockly.draggingConnections_ =
+ Blockly.draggingConnections_.concat(this.getConnections_(true));
} else {
this.removeDragging();
+ Blockly.draggingConnections_ = [];
}
// Recurse through all blocks attached under this one.
for (var i = 0; i < this.childBlocks_.length; i++) {
@@ -722,6 +892,49 @@ Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
}
};
+/**
+ * Move this block to its workspace's drag surface, accounting for positioning.
+ * Generally should be called at the same time as setDragging_(true).
+ * @private
+ */
+Blockly.BlockSvg.prototype.moveToDragSurface_ = function() {
+ // 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.
+ var xy = this.getRelativeToSurfaceXY();
+ this.clearTransformAttributes_();
+ this.workspace.dragSurface.translateSurface(xy.x, xy.y);
+ // Execute the move on the top-level SVG component
+ this.workspace.dragSurface.setBlocksAndShow(this.getSvgRoot());
+};
+
+/**
+ * Move this block back to the workspace block canvas.
+ * Generally should be called at the same time as setDragging_(false).
+ * @private
+ */
+Blockly.BlockSvg.prototype.moveOffDragSurface_ = function() {
+ // Translate to current position, turning off 3d.
+ var xy = this.getRelativeToSurfaceXY();
+ this.clearTransformAttributes_();
+ this.translate(xy.x, xy.y, false);
+ this.workspace.dragSurface.clearAndHide(this.workspace.getCanvas());
+};
+
+/**
+ * Clear the block of style="..." and transform="..." attributes.
+ * Used when the block is switching from 3d to 2d transform or vice versa.
+ * @private
+ */
+Blockly.BlockSvg.prototype.clearTransformAttributes_ = function() {
+ if (this.getSvgRoot().hasAttribute('transform')) {
+ this.getSvgRoot().removeAttribute('transform');
+ }
+ if (this.getSvgRoot().hasAttribute('style')) {
+ this.getSvgRoot().removeAttribute('style');
+ }
+};
+
/**
* Drag this block to follow the mouse.
* @param {!Event} e Mouse move event.
@@ -738,81 +951,227 @@ Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
e.stopPropagation();
return;
}
- Blockly.removeAllRanges();
var oldXY = this.getRelativeToSurfaceXY();
var newXY = this.workspace.moveDrag(e);
- var group = this.getSvgRoot();
- if (Blockly.dragMode_ == 1) {
+ if (Blockly.dragMode_ == Blockly.DRAG_STICKY) {
// Still dragging within the sticky DRAG_RADIUS.
var dr = goog.math.Coordinate.distance(oldXY, newXY) * this.workspace.scale;
if (dr > Blockly.DRAG_RADIUS) {
// Switch to unrestricted dragging.
- Blockly.dragMode_ = 2;
+ Blockly.dragMode_ = Blockly.DRAG_FREE;
Blockly.longStop_();
- group.translate_ = '';
- group.skew_ = '';
+ // Must move to drag surface before unplug(),
+ // or else connections will calculate the wrong relative to surface XY
+ // in tighten_(). Then blocks connected to this block move around on the
+ // drag surface. By moving to the drag surface before unplug, connection
+ // positions will be calculated correctly.
+ this.moveToDragSurface_();
+ // Clear WidgetDiv/DropDownDiv without animating, in case blocks are moved
+ // around
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
if (this.parentBlock_) {
// Push this block to the very top of the stack.
- this.setParent(null);
- this.disconnectUiEffect();
+ this.unplug();
}
this.setDragging_(true);
- this.workspace.recordDeleteAreas();
}
}
- if (Blockly.dragMode_ == 2) {
- // Unrestricted dragging.
- var dx = oldXY.x - this.dragStartXY_.x;
- var dy = oldXY.y - this.dragStartXY_.y;
- group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')';
- group.setAttribute('transform', group.translate_ + group.skew_);
- // 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);
+ if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
+ this.handleDragFree_(oldXY, newXY, e);
+ }
+ // This event has been handled. No need to bubble up to the document.
+ e.stopPropagation();
+ e.preventDefault();
+};
+
+/**
+ * Handle a mouse movement when a block is already freely dragging.
+ * @param {!goog.math.Coordinate} oldXY The position of the block on screen
+ * before the most recent mouse movement.
+ * @param {!goog.math.Coordinate} newXY The new location after applying the
+ * mouse movement.
+ * @param {!Event} e Mouse move event.
+ * @private
+ */
+Blockly.BlockSvg.prototype.handleDragFree_ = function(oldXY, newXY, e) {
+ var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_);
+ this.workspace.dragSurface.translateSurface(newXY.x, newXY.y);
+ // Drag all the nested bubbles.
+ for (var i = 0; i < this.draggedBubbles_.length; i++) {
+ var commentData = this.draggedBubbles_[i];
+ commentData.bubble.setIconLocation(
+ goog.math.Coordinate.sum(commentData, dxy));
+ }
+
+ // Check to see if any of this block's connections are within range of
+ // another block's connection.
+ var myConnections = this.getConnections_(false);
+ // Also check the last connection on this stack
+ var lastOnStack = this.lastConnectionInStack_();
+ if (lastOnStack && lastOnStack != this.nextConnection) {
+ myConnections.push(lastOnStack);
+ }
+ var closestConnection = null;
+ var localConnection = null;
+ var radiusConnection = Blockly.SNAP_RADIUS;
+ for (i = 0; i < myConnections.length; i++) {
+ var myConnection = myConnections[i];
+ var neighbour = myConnection.closest(radiusConnection, dxy);
+ if (neighbour.connection) {
+ closestConnection = neighbour.connection;
+ localConnection = myConnection;
+ radiusConnection = neighbour.radius;
}
+ }
- // 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;
- }
+ var updatePreviews = true;
+ if (Blockly.localConnection_ && Blockly.highlightedConnection_) {
+ var xDiff = Blockly.localConnection_.x_ + dxy.x -
+ Blockly.highlightedConnection_.x_;
+ var yDiff = Blockly.localConnection_.y_ + dxy.y -
+ Blockly.highlightedConnection_.y_;
+ var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+
+ // Slightly prefer the existing preview over a new preview.
+ if (closestConnection && radiusConnection > curDistance -
+ Blockly.CURRENT_CONNECTION_PREFERENCE) {
+ updatePreviews = false;
}
+ }
- // Remove connection highlighting if needed.
- if (Blockly.highlightedConnection_ &&
- Blockly.highlightedConnection_ != closestConnection) {
- Blockly.highlightedConnection_.unhighlight();
- Blockly.highlightedConnection_ = null;
- Blockly.localConnection_ = null;
+ if (updatePreviews) {
+ var candidateIsLast = (localConnection == lastOnStack);
+ this.updatePreviews(closestConnection, localConnection, radiusConnection,
+ e, newXY.x - this.dragStartXY_.x, newXY.y - this.dragStartXY_.y,
+ candidateIsLast);
+ }
+};
+
+/**
+ * Preview the results of the drag if the mouse is released immediately.
+ * @param {Blockly.Connection} closestConnection The closest connection found
+ * during the search
+ * @param {Blockly.Connection} localConnection The connection on the moving
+ * block.
+ * @param {number} radiusConnection The distance between closestConnection and
+ * localConnection.
+ * @param {!Event} e Mouse move event.
+ * @param {number} dx The x distance the block has moved onscreen up to this
+ * point in the drag.
+ * @param {number} dy The y distance the block has moved onscreen up to this
+ * point in the drag.
+ * @param {boolean} candidateIsLast True if the dragging stack is more than one
+ * block long and localConnection is the last connection on the stack.
+ */
+Blockly.BlockSvg.prototype.updatePreviews = function(closestConnection,
+ localConnection, radiusConnection, e, dx, dy, candidateIsLast) {
+ // Don't fire events for insertion marker creation or movement.
+ Blockly.Events.disable();
+ // Remove an insertion marker if needed. For Scratch-Blockly we are using
+ // grayed-out blocks instead of highlighting the connection; for compatibility
+ // with Web Blockly the name "highlightedConnection" will still be used.
+ if (Blockly.highlightedConnection_ &&
+ Blockly.highlightedConnection_ != closestConnection) {
+ if (Blockly.insertionMarker_ && Blockly.insertionMarkerConnection_) {
+ Blockly.BlockSvg.disconnectInsertionMarker();
+ }
+ // If there's already an insertion marker but it's representing the wrong
+ // block, delete it so we can create the correct one.
+ if (Blockly.insertionMarker_ &&
+ ((candidateIsLast && Blockly.localConnection_.sourceBlock_ == this) ||
+ (!candidateIsLast && Blockly.localConnection_.sourceBlock_ != this))) {
+ Blockly.insertionMarker_.dispose();
+ Blockly.insertionMarker_ = null;
+ }
+ Blockly.highlightedConnection_ = null;
+ Blockly.localConnection_ = null;
+ }
+
+ // Add an insertion marker if needed.
+ if (closestConnection &&
+ closestConnection != Blockly.highlightedConnection_ &&
+ !closestConnection.sourceBlock_.isInsertionMarker()) {
+ Blockly.highlightedConnection_ = closestConnection;
+ Blockly.localConnection_ = localConnection;
+ if (!Blockly.insertionMarker_) {
+ Blockly.insertionMarker_ =
+ this.workspace.newBlock(Blockly.localConnection_.sourceBlock_.type);
+ Blockly.insertionMarker_.setInsertionMarker(true);
+ Blockly.insertionMarker_.initSvg();
}
- // Add connection highlighting if needed.
- if (closestConnection &&
- closestConnection != Blockly.highlightedConnection_) {
- closestConnection.highlight();
- Blockly.highlightedConnection_ = closestConnection;
- Blockly.localConnection_ = localConnection;
+
+ var insertionMarker = Blockly.insertionMarker_;
+ var insertionMarkerConnection = insertionMarker.getMatchingConnection(
+ localConnection.sourceBlock_, localConnection);
+ if (insertionMarkerConnection != Blockly.insertionMarkerConnection_) {
+ insertionMarker.rendered = true;
+ // Render disconnected from everything else so that we have a valid
+ // connection location.
+ insertionMarker.render();
+ insertionMarker.getSvgRoot().setAttribute('visibility', 'visible');
+
+ this.positionNewBlock(insertionMarker,
+ insertionMarkerConnection, closestConnection);
+
+ if (insertionMarkerConnection.type == Blockly.PREVIOUS_STATEMENT &&
+ !insertionMarker.nextConnection) {
+ Blockly.bumpedConnection_ = closestConnection.targetConnection;
+ }
+ // Renders insertion marker.
+ insertionMarkerConnection.connect(closestConnection);
+ Blockly.insertionMarkerConnection_ = insertionMarkerConnection;
}
- // Provide visual indication of whether the block will be deleted if
- // dropped here.
- if (this.isDeletable()) {
- this.workspace.isDeleteArea(e);
+ }
+ // Reenable events.
+ Blockly.Events.enable();
+
+ // Provide visual indication of whether the block will be deleted if
+ // dropped here.
+ if (this.isDeletable()) {
+ this.workspace.isDeleteArea(e);
+ }
+};
+
+/**
+ * Disconnect the current insertion marker from the stack, and heal the stack to
+ * its previous state.
+ */
+Blockly.BlockSvg.disconnectInsertionMarker = function() {
+ // The insertion marker is the first block in a stack, either because it
+ // doesn't have a previous connection or because the previous connection is
+ // not connected. Unplug won't do anything in that case. Instead, unplug the
+ // following block.
+ if (Blockly.insertionMarkerConnection_ ==
+ Blockly.insertionMarker_.nextConnection &&
+ (!Blockly.insertionMarker_.previousConnection ||
+ !Blockly.insertionMarker_.previousConnection.targetConnection)) {
+ Blockly.insertionMarkerConnection_.targetBlock().unplug(false);
+ }
+ // Inside of a C-block, first statement connection.
+ else if (Blockly.insertionMarkerConnection_.type == Blockly.NEXT_STATEMENT &&
+ Blockly.insertionMarkerConnection_ !=
+ Blockly.insertionMarker_.nextConnection) {
+ var innerConnection = Blockly.insertionMarkerConnection_.targetConnection;
+ innerConnection.sourceBlock_.unplug(false);
+ var previousBlockNextConnection =
+ Blockly.insertionMarker_.previousConnection.targetConnection;
+ Blockly.insertionMarker_.unplug(true);
+ if (previousBlockNextConnection) {
+ previousBlockNextConnection.connect(innerConnection);
}
}
- // This event has been handled. No need to bubble up to the document.
- e.stopPropagation();
+ else {
+ Blockly.insertionMarker_.unplug(true /* healStack */);
+ }
+
+ if (Blockly.insertionMarkerConnection_.targetConnection) {
+ throw 'insertionMarkerConnection still connected at the end of disconnectInsertionMarker';
+ }
+ Blockly.insertionMarkerConnection_ = null;
+ Blockly.insertionMarker_.getSvgRoot().setAttribute('visibility', 'hidden');
};
/**
@@ -839,13 +1198,14 @@ Blockly.BlockSvg.prototype.setMovable = function(movable) {
/**
* Set whether this block is editable or not.
- * @param {boolean} movable True if editable.
+ * @param {boolean} editable True if editable.
*/
Blockly.BlockSvg.prototype.setEditable = function(editable) {
Blockly.BlockSvg.superClass_.setEditable.call(this, editable);
if (this.rendered) {
- for (var i = 0; i < this.icons_.length; i++) {
- this.icons_[i].updateEditable();
+ var icons = this.getIcons();
+ for (var i = 0; i < icons.length; i++) {
+ icons[i].updateEditable();
}
}
};
@@ -859,6 +1219,15 @@ Blockly.BlockSvg.prototype.setShadow = function(shadow) {
this.updateColour();
};
+/**
+ * Set whether this block is an insertion marker block or not.
+ * @param {boolean} insertionMarker True if an insertion marker.
+ */
+Blockly.BlockSvg.prototype.setInsertionMarker = function(insertionMarker) {
+ Blockly.BlockSvg.superClass_.setInsertionMarker.call(this, insertionMarker);
+ this.updateColour();
+};
+
/**
* Return the root node of the SVG or null if none exists.
* @return {Element} The root SVG node (probably a group).
@@ -867,241 +1236,18 @@ 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;
-/**
- * Do blocks with no previous or output connections have a 'hat' on top?
- * @const
- */
-Blockly.BlockSvg.START_HAT = false;
-/**
- * Path of the top hat's curve.
- * @const
- */
-Blockly.BlockSvg.START_HAT_PATH = 'c 30,-15 70,-15 100,0';
-/**
- * Path of the top hat's curve's highlight in LTR.
- * @const
- */
-Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR =
- 'c 17.8,-9.2 45.3,-14.9 75,-8.7 M 100.5,0.5';
-/**
- * Path of the top hat's curve's highlight in RTL.
- * @const
- */
-Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL =
- 'm 25,-8.7 c 29.7,-6.2 57.2,-0.5 75,8.7';
-/**
- * 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 - 0.5) + 0.5;
-/**
- * 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 + 0.5) - 0.5;
-/**
- * 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,4 3,0 6,-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.97) + ',3 q -' +
- (Blockly.BlockSvg.TAB_WIDTH * 0.05) + ',10 ' +
- (Blockly.BlockSvg.TAB_WIDTH * 0.3) + ',9.5 m ' +
- (Blockly.BlockSvg.TAB_WIDTH * 0.67) + ',-1.9 v 1.4';
-
-/**
- * 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 0.5,' + (Blockly.BlockSvg.CORNER_RADIUS - 0.5);
-/**
- * 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 - 0.5) + ',' +
- (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' +
- Blockly.BlockSvg.CORNER_RADIUS + ',0.5';
-/**
- * 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) +
- ' h -0.5 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 + ',' +
- Blockly.BlockSvg.CORNER_RADIUS + ' 0 0,0 ' +
- (-Blockly.BlockSvg.DISTANCE_45_OUTSIDE - 0.5) + ',' +
- (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 + 0.5) + ',' +
- (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' +
- (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ',' +
- (Blockly.BlockSvg.CORNER_RADIUS + 0.5);
-/**
- * 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 + 0.5) + ',' +
- (Blockly.BlockSvg.CORNER_RADIUS + 0.5) + ' 0 0,0 ' +
- (Blockly.BlockSvg.CORNER_RADIUS -
- Blockly.BlockSvg.DISTANCE_45_OUTSIDE) + ',' +
- (Blockly.BlockSvg.DISTANCE_45_OUTSIDE + 0.5);
-
/**
* 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) {
+Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
Blockly.Field.startCache();
- // Terminate onchange event calls.
- if (this.onchangeWrapper_) {
- Blockly.unbindEvent_(this.onchangeWrapper_);
- this.onchangeWrapper_ = null;
- }
// If this block is being dragged, unlink the mouse events.
if (Blockly.selected == this) {
+ this.unselect();
Blockly.terminateDrag_();
}
// If this block has a context menu open, close it.
@@ -1110,25 +1256,24 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate,
}
if (animate && this.rendered) {
- this.unplug(healStack, false);
+ this.unplug(healStack);
this.disposeUiEffect();
}
// Stop rerendering.
this.rendered = false;
+ Blockly.Events.disable();
var icons = this.getIcons();
for (var i = 0; i < icons.length; i++) {
icons[i].dispose();
}
-
+ Blockly.Events.enable();
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;
Blockly.Field.stopCache();
};
@@ -1153,6 +1298,13 @@ Blockly.BlockSvg.prototype.disposeUiEffect = function() {
this.workspace.scale);
};
+/**
+ * Play some UI effects (sound) after a connection has been established.
+ */
+Blockly.BlockSvg.prototype.connectionUiEffect = function() {
+ this.workspace.playAudio('click');
+};
+
/**
* Animate a cloned block and eventually dispose of it.
* This is a class method, not an instace method since the original block has
@@ -1182,191 +1334,11 @@ Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) {
}
};
-/**
- * Play some UI effects (sound, ripple) after a connection has been established.
- */
-Blockly.BlockSvg.prototype.connectionUiEffect = function() {
- this.workspace.playAudio('click');
- if (this.workspace.scale < 1) {
- return; // Too small to care about visual effects.
- }
- // Determine the absolute coordinates of the inferior block.
- var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_),
- this.workspace);
- // Offset the coordinates based on the two connection types, fix scale.
- if (this.outputConnection) {
- xy.x += (this.RTL ? 3 : -3) * this.workspace.scale;
- xy.y += 13 * this.workspace.scale;
- } else if (this.previousConnection) {
- xy.x += (this.RTL ? -23 : 23) * this.workspace.scale;
- xy.y += 3 * this.workspace.scale;
- }
- var ripple = Blockly.createSvgElement('circle',
- {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none',
- 'stroke': '#888', 'stroke-width': 10},
- this.workspace.getParentSvg());
- // Start the animation.
- Blockly.BlockSvg.connectionUiStep_(ripple, new Date(), this.workspace.scale);
-};
-
-/**
- * Expand a ripple around a connection.
- * @param {!Element} ripple Element to animate.
- * @param {!Date} start Date of animation's start.
- * @param {number} workspaceScale Scale of workspace.
- * @private
- */
-Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) {
- var ms = (new Date()) - start;
- var percent = ms / 150;
- if (percent > 1) {
- goog.dom.removeNode(ripple);
- } else {
- ripple.setAttribute('r', percent * 25 * workspaceScale);
- ripple.style.opacity = 1 - percent;
- var closure = function() {
- Blockly.BlockSvg.connectionUiStep_(ripple, start, workspaceScale);
- };
- Blockly.BlockSvg.disconnectUiStop_.pid_ = setTimeout(closure, 10);
- }
-};
-
-/**
- * Play some UI effects (sound, animation) when disconnecting a block.
- */
-Blockly.BlockSvg.prototype.disconnectUiEffect = function() {
- this.workspace.playAudio('disconnect');
- if (this.workspace.scale < 1) {
- return; // Too small to care about visual effects.
- }
- // Horizontal distance for bottom of block to wiggle.
- var DISPLACEMENT = 10;
- // Scale magnitude of skew to height of block.
- var height = this.getHeightWidth().height;
- var magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180;
- if (!this.RTL) {
- magnitude *= -1;
- }
- // Start the animation.
- Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date());
-};
-
-/**
- * Animate a brief wiggle of a disconnected block.
- * @param {!Element} group SVG element to animate.
- * @param {number} magnitude Maximum degrees skew (reversed for RTL).
- * @param {!Date} start Date of animation's start.
- * @private
- */
-Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) {
- var DURATION = 200; // Milliseconds.
- var WIGGLES = 3; // Half oscillations.
-
- var ms = (new Date()) - start;
- var percent = ms / DURATION;
-
- if (percent > 1) {
- group.skew_ = '';
- } else {
- var skew = Math.round(Math.sin(percent * Math.PI * WIGGLES) *
- (1 - percent) * magnitude);
- group.skew_ = 'skewX(' + skew + ')';
- var closure = function() {
- Blockly.BlockSvg.disconnectUiStep_(group, magnitude, start);
- };
- Blockly.BlockSvg.disconnectUiStop_.group = group;
- Blockly.BlockSvg.disconnectUiStop_.pid = setTimeout(closure, 10);
- }
- group.setAttribute('transform', group.translate_ + group.skew_);
-};
-
-/**
- * Stop the disconnect UI animation immediately.
- * @private
- */
-Blockly.BlockSvg.disconnectUiStop_ = function() {
- if (Blockly.BlockSvg.disconnectUiStop_.group) {
- clearTimeout(Blockly.BlockSvg.disconnectUiStop_.pid);
- var group = Blockly.BlockSvg.disconnectUiStop_.group
- group.skew_ = '';
- group.setAttribute('transform', group.translate_);
- Blockly.BlockSvg.disconnectUiStop_.group = null;
- }
-};
-
-/**
- * PID of disconnect UI animation. There can only be one at a time.
- * @type {number}
- */
-Blockly.BlockSvg.disconnectUiStop_.pid = 0;
-
-/**
- * SVG group of wobbling block. There can only be one at a time.
- * @type {Element}
- */
-Blockly.BlockSvg.disconnectUiStop_.group = null;
-
-/**
- * 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 rgb = goog.color.hexToRgb(hexColour);
- if (this.isShadow()) {
- rgb = goog.color.lighten(rgb, 0.6);
- hexColour = goog.color.rgbArrayToHex(rgb);
- this.svgPathLight_.style.display = 'none';
- this.svgPathDark_.setAttribute('fill', hexColour);
- } 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('fill', hexColour);
-
- var icons = this.getIcons();
- for (var i = 0; i < icons.length; i++) {
- icons[i].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(#' + this.workspace.options.disabledPatternId + ')');
- }
- } 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();
- }
+ // not supported
};
/**
@@ -1431,7 +1403,7 @@ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
clearTimeout(this.setWarningText.pid_[id]);
delete this.setWarningText.pid_[id];
}
- if (Blockly.dragMode_ == 2) {
+ if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
// Don't change the warning text during a drag.
// Wait until the drag finishes.
var thisBlock = this;
@@ -1447,19 +1419,6 @@ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
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(text, 'collapsed ' + this.id + ' ' + id);
- }
-
var changedState = false;
if (goog.isString(text)) {
if (!this.warning) {
@@ -1504,21 +1463,6 @@ Blockly.BlockSvg.prototype.setMutator = function(mutator) {
}
};
-/**
- * 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.
*/
@@ -1539,7 +1483,6 @@ Blockly.BlockSvg.prototype.removeSelect = function() {
/**
* 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_),
@@ -1554,698 +1497,180 @@ Blockly.BlockSvg.prototype.removeDragging = function() {
'blocklyDragging');
};
+// Overrides of functions on Blockly.Block that take into account whether the
+// block has been rendered.
+
/**
- * 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.
+ * Change the colour of a block.
+ * @param {number|string} colour HSV hue value, or #RRGGBB string.
+ * @param {number|string} colourSecondary Secondary HSV hue value, or #RRGGBB
+ * string.
+ * @param {number|string} colourTertiary Tertiary HSV hue value, or #RRGGBB
+ * string.
*/
-Blockly.BlockSvg.prototype.render = function(opt_bubble) {
- Blockly.Field.startCache();
- this.rendered = true;
+Blockly.BlockSvg.prototype.setColour = function(colour, colourSecondary,
+ colourTertiary) {
+ Blockly.BlockSvg.superClass_.setColour.call(this, colour, colourSecondary,
+ colourTertiary);
- var cursorX = Blockly.BlockSvg.SEP_SPACE_X;
- if (this.RTL) {
- cursorX = -cursorX;
- }
- // Move the icons into position.
- var icons = this.getIcons();
- for (var i = 0; i < icons.length; i++) {
- cursorX = icons[i].renderIcon(cursorX);
- }
- cursorX += this.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');
- }
+ if (this.rendered) {
+ this.updateColour();
}
- Blockly.Field.stopCache();
};
/**
- * Render a list of fields starting at the specified location.
- * @param {!Array.} 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
+ * 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.|null|undefined} opt_check Statement type or
+ * list of statement types. Null/undefined if any type could be connected.
*/
-Blockly.BlockSvg.prototype.renderFields_ =
- function(fieldList, cursorX, cursorY) {
- cursorY += Blockly.BlockSvg.INLINE_PADDING_Y;
- if (this.RTL) {
- cursorX = -cursorX;
- }
- for (var t = 0, field; field = fieldList[t]; t++) {
- var root = field.getSvgRoot();
- if (!root) {
- continue;
- }
- if (this.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;
- }
- }
+Blockly.BlockSvg.prototype.setPreviousStatement =
+ function(newBoolean, opt_check) {
+ Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean,
+ opt_check);
+
+ if (this.rendered) {
+ this.render();
+ this.bumpNeighbours_();
}
- return this.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.>} 2D array of objects, each containing
- * position information.
- * @private
+ * 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.|null|undefined} opt_check Statement type or
+ * list of statement types. Null/undefined if any type could be connected.
*/
-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.getInputsInline() && !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);
- }
- // Blocks have a one pixel shadow that should sometimes overhang.
- if (!isInline && i == inputList.length - 1) {
- // Last value input should overhang.
- input.renderHeight--;
- } else if (!isInline && input.type == Blockly.INPUT_VALUE &&
- inputList[i + 1] && inputList[i + 1].type == Blockly.NEXT_STATEMENT) {
- // Value input above statement input should overhang.
- 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 += this.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;
- }
+Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) {
+ Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean,
+ opt_check);
- 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);
- }
- }
+ if (this.rendered) {
+ this.render();
+ this.bumpNeighbours_();
}
+};
- // 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;
- }
- }
- }
- }
+/**
+ * Set whether this block returns a value.
+ * @param {boolean} newBoolean True if there is an output.
+ * @param {string|Array.|null|undefined} 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);
- // 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;
+ if (this.rendered) {
+ this.render();
+ this.bumpNeighbours_();
+ }
};
-
/**
- * 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.>} inputRows 2D array of objects, each
- * containing position information.
- * @private
+ * Set whether value inputs are arranged horizontally or vertically.
+ * @param {boolean} newBoolean True if inputs are horizontal.
*/
-Blockly.BlockSvg.prototype.renderDraw_ = function(iconWidth, inputRows) {
- this.startHat_ = false;
- // 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;
- }
- } else if (Blockly.BlockSvg.START_HAT) {
- // No output or previous connection.
- this.squareTopLeftCorner_ = true;
- this.startHat_ = true;
- inputRows.rightEdge = Math.max(inputRows.rightEdge, 100);
- }
- var nextBlock = this.getNextBlock();
- if (nextBlock) {
- this.squareBottomLeftCorner_ = true;
- }
+Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) {
+ Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean);
+
+ if (this.rendered) {
+ this.render();
+ this.bumpNeighbours_();
}
+};
- // 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 (this.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)');
+/**
+ * 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 {goog.asserts.AssertionError} 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_();
}
};
/**
- * Render the top edge of the block.
- * @param {!Array.} steps Path of block outline.
- * @param {!Array.} highlightSteps Path of block highlights.
- * @param {!Object} connectionsXY Location of block.
- * @param {number} rightEdge Minimum width of block.
- * @private
+ * 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.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 0.5,0.5');
- if (this.startHat_) {
- steps.push(Blockly.BlockSvg.START_HAT_PATH);
- highlightSteps.push(this.RTL ?
- Blockly.BlockSvg.START_HAT_HIGHLIGHT_RTL :
- Blockly.BlockSvg.START_HAT_HIGHLIGHT_LTR);
- }
- } else {
- steps.push(Blockly.BlockSvg.TOP_LEFT_CORNER_START);
- highlightSteps.push(this.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 + (this.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 - 0.5);
- this.width = rightEdge;
+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_();
+ }
};
/**
- * Render the right edge of the block.
- * @param {!Array.} steps Path of block outline.
- * @param {!Array.} highlightSteps Path of block highlights.
- * @param {!Array.} inlineSteps Inline block outlines.
- * @param {!Array.} highlightInlineSteps Inline block highlights.
- * @param {!Object} connectionsXY Location of block.
- * @param {!Array.>} inputRows 2D array of objects, each
- * containing position information.
- * @param {number} iconWidth Offset of first row due to icons.
- * @return {number} Height of block.
+ * 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.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 += this.RTL ? -iconWidth : iconWidth;
- }
- highlightSteps.push('M', (inputRows.rightEdge - 0.5) + ',' +
- (cursorY + 0.5));
- if (this.isCollapsed()) {
- // Jagged right edge.
- var input = row[0];
- var fieldX = cursorX;
- var fieldY = cursorY;
- this.renderFields_(input.fieldRow, fieldX, fieldY);
- steps.push(Blockly.BlockSvg.JAGGED_TEETH);
- highlightSteps.push('h 8');
- var remainder = row.height - Blockly.BlockSvg.JAGGED_TEETH_HEIGHT;
- steps.push('v', remainder);
- if (this.RTL) {
- highlightSteps.push('v 3.9 l 7.2,3.4 m -14.5,8.9 l 7.3,3.5');
- highlightSteps.push('v', remainder - 0.7);
- }
- 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;
- 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 (this.RTL) {
- // Highlight right edge, around back of tab, and bottom.
- highlightInlineSteps.push('M',
- (cursorX - Blockly.BlockSvg.SEP_SPACE_X - 2.5 +
- Blockly.BlockSvg.TAB_WIDTH - input.renderWidth) + ',' +
- (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5));
- highlightInlineSteps.push(
- Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
- highlightInlineSteps.push('v',
- input.renderHeight - Blockly.BlockSvg.TAB_HEIGHT + 2.5);
- highlightInlineSteps.push('h',
- input.renderWidth - Blockly.BlockSvg.TAB_WIDTH + 2);
- } else {
- // Highlight right edge, bottom.
- highlightInlineSteps.push('M',
- (cursorX - Blockly.BlockSvg.SEP_SPACE_X + 0.5) + ',' +
- (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y + 0.5));
- 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 +
- 0.9) + ',' + (cursorY + Blockly.BlockSvg.INLINE_PADDING_Y +
- Blockly.BlockSvg.TAB_HEIGHT - 0.7));
- highlightInlineSteps.push('l',
- (Blockly.BlockSvg.TAB_WIDTH * 0.46) + ',-2.1');
- }
- // Create inline input connection.
- if (this.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_();
- }
- }
- }
+Blockly.BlockSvg.prototype.appendInput_ = function(type, name) {
+ var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name);
- cursorX = Math.max(cursorX, inputRows.rightEdge);
- this.width = Math.max(this.width, cursorX);
- steps.push('H', cursorX);
- highlightSteps.push('H', cursorX - 0.5);
- steps.push('v', row.height);
- if (this.RTL) {
- highlightSteps.push('v', row.height - 1);
- }
- } else if (row.type == Blockly.INPUT_VALUE) {
- // External input.
- var input = row[0];
- var fieldX = cursorX;
- var fieldY = cursorY;
- 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 / 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 (this.RTL) {
- // Highlight around back of tab.
- highlightSteps.push(Blockly.BlockSvg.TAB_PATH_DOWN_HIGHLIGHT_RTL);
- highlightSteps.push('v', v + 0.5);
- } else {
- // Short highlight glint at bottom of tab.
- highlightSteps.push('M', (inputRows.rightEdge - 5) + ',' +
- (cursorY + Blockly.BlockSvg.TAB_HEIGHT - 0.7));
- highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * 0.46) +
- ',-2.1');
- }
- // Create external input connection.
- connectionX = connectionsXY.x +
- (this.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;
- 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 / 2;
- }
- }
- this.renderFields_(input.fieldRow, fieldX, fieldY);
- steps.push('v', row.height);
- if (this.RTL) {
- highlightSteps.push('v', row.height - 1);
- }
- } 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 (this.RTL) {
- highlightSteps.push('v', Blockly.BlockSvg.SEP_SPACE_Y - 1);
- }
- cursorY += Blockly.BlockSvg.SEP_SPACE_Y;
- }
- var fieldX = cursorX;
- var fieldY = cursorY;
- 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 / 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 (this.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 - 0.5);
- } 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 - 0.5);
- }
- // Create statement connection.
- connectionX = connectionsXY.x + (this.RTL ? -cursorX : cursorX + 1);
- 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 (this.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 (this.RTL) {
- highlightSteps.push('V', cursorY - 1);
- }
+ if (this.rendered) {
+ this.render();
+ // Adding an input will cause the block to change shape.
+ this.bumpNeighbours_();
}
- return cursorY;
+ return input;
};
/**
- * Render the bottom edge of the block.
- * @param {!Array.} steps Path of block outline.
- * @param {!Array.} highlightSteps Path of block highlights.
- * @param {!Object} connectionsXY Location of block.
- * @param {number} cursorY Height of block.
+ * 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.} Array of connections.
* @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 + (this.RTL ? 0.5 : - 0.5)) +
- ' ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT);
- // Create next block connection.
- var connectionX;
- if (this.RTL) {
- connectionX = connectionsXY.x - Blockly.BlockSvg.NOTCH_WIDTH;
- } else {
- connectionX = connectionsXY.x + Blockly.BlockSvg.NOTCH_WIDTH;
+Blockly.BlockSvg.prototype.getConnections_ = function(all) {
+ var myConnections = [];
+ if (all || this.rendered) {
+ if (this.outputConnection) {
+ myConnections.push(this.outputConnection);
}
- var connectionY = connectionsXY.y + cursorY + 1;
- this.nextConnection.moveTo(connectionX, connectionY);
- if (this.nextConnection.targetConnection) {
- this.nextConnection.tighten_();
+ if (this.previousConnection) {
+ myConnections.push(this.previousConnection);
}
- this.height += 4; // Height of tab.
- }
-
- // Should the bottom-left corner be rounded or square?
- if (this.squareBottomLeftCorner_) {
- steps.push('H 0');
- if (!this.RTL) {
- highlightSteps.push('M', '0.5,' + (cursorY - 0.5));
+ if (this.nextConnection) {
+ myConnections.push(this.nextConnection);
}
- } 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 (!this.RTL) {
- highlightSteps.push('M', Blockly.BlockSvg.DISTANCE_45_INSIDE + ',' +
- (cursorY - Blockly.BlockSvg.DISTANCE_45_INSIDE));
- highlightSteps.push('A', (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ',' +
- (Blockly.BlockSvg.CORNER_RADIUS - 0.5) + ' 0 0,1 ' +
- '0.5,' + (cursorY - Blockly.BlockSvg.CORNER_RADIUS));
+ if (all || !this.collapsed_) {
+ for (var i = 0, input; input = this.inputList[i]; i++) {
+ if (input.connection) {
+ myConnections.push(input.connection);
+ }
+ }
}
}
+ return myConnections;
};
/**
- * Render the left edge of the block.
- * @param {!Array.} steps Path of block outline.
- * @param {!Array.} highlightSteps Path of block highlights.
- * @param {!Object} connectionsXY Location of block.
- * @param {number} cursorY Height of block.
+ * 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.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 (this.RTL) {
- highlightSteps.push('M', (Blockly.BlockSvg.TAB_WIDTH * -0.25) + ',8.4');
- highlightSteps.push('l', (Blockly.BlockSvg.TAB_WIDTH * -0.45) + ',-2.1');
- } else {
- highlightSteps.push('V', Blockly.BlockSvg.TAB_HEIGHT - 1.5);
- highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * -0.92) +
- ',-0.5 q ' + (Blockly.BlockSvg.TAB_WIDTH * -0.19) +
- ',-5.5 0,-11');
- highlightSteps.push('m', (Blockly.BlockSvg.TAB_WIDTH * 0.92) +
- ',1 V 0.5 H 1');
- }
- this.width += Blockly.BlockSvg.TAB_WIDTH;
- } else if (!this.RTL) {
- if (this.squareTopLeftCorner_) {
- // Statement block in a stack.
- highlightSteps.push('V', 0.5);
- } else {
- highlightSteps.push('V', Blockly.BlockSvg.CORNER_RADIUS);
- }
- }
- steps.push('z');
+Blockly.BlockSvg.prototype.makeConnection_ = function(type) {
+ return new Blockly.RenderedConnection(this, type);
};
diff --git a/core/blockly.js b/core/blockly.js
index 07403d972a..c6729c230d 100644
--- a/core/blockly.js
+++ b/core/blockly.js
@@ -27,16 +27,21 @@
// Top level object for Blockly.
goog.provide('Blockly');
-goog.require('Blockly.BlockSvg');
+goog.require('Blockly.BlockSvg.render');
+goog.require('Blockly.DropDownDiv');
+goog.require('Blockly.Events');
goog.require('Blockly.FieldAngle');
+goog.require('Blockly.FieldNumber');
goog.require('Blockly.FieldCheckbox');
goog.require('Blockly.FieldColour');
// Date picker commented out since it increases footprint by 60%.
// Add it only if you need it.
//goog.require('Blockly.FieldDate');
goog.require('Blockly.FieldDropdown');
+goog.require('Blockly.FieldIconMenu');
goog.require('Blockly.FieldImage');
goog.require('Blockly.FieldTextInput');
+goog.require('Blockly.FieldNumber');
goog.require('Blockly.FieldVariable');
goog.require('Blockly.Generator');
goog.require('Blockly.Msg');
@@ -44,6 +49,7 @@ goog.require('Blockly.Procedures');
goog.require('Blockly.Toolbox');
goog.require('Blockly.WidgetDiv');
goog.require('Blockly.WorkspaceSvg');
+goog.require('Blockly.constants');
goog.require('Blockly.inject');
goog.require('Blockly.utils');
goog.require('goog.color');
@@ -54,97 +60,11 @@ goog.require('goog.userAgent');
var CLOSURE_DEFINES = {'goog.DEBUG': false};
/**
- * Required name space for SVG elements.
- * @const
- */
-Blockly.SVG_NS = 'http://www.w3.org/2000/svg';
-/**
- * Required name space for HTML elements.
- * @const
- */
-Blockly.HTML_NS = 'http://www.w3.org/1999/xhtml';
-
-/**
- * The richness of block colours, regardless of the hue.
- * Must be in the range of 0 (inclusive) to 1 (exclusive).
- */
-Blockly.HSV_SATURATION = 0.45;
-/**
- * The intensity of block colours, regardless of the hue.
- * Must be in the range of 0 (inclusive) to 1 (exclusive).
- */
-Blockly.HSV_VALUE = 0.65;
-
-/**
- * Sprited icons and images.
- */
-Blockly.SPRITE = {
- width: 96,
- height: 124,
- url: 'sprites.png'
-};
-
-/**
- * Convert a hue (HSV model) into an RGB hex triplet.
- * @param {number} hue Hue on a colour wheel (0-360).
- * @return {string} RGB code, e.g. '#5ba65b'.
- */
-Blockly.hueToRgb = function(hue) {
- return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION,
- Blockly.HSV_VALUE * 255);
-};
-
-/**
- * ENUM for a right-facing value input. E.g. 'set item to' or 'return'.
- * @const
- */
-Blockly.INPUT_VALUE = 1;
-/**
- * ENUM for a left-facing value output. E.g. 'random fraction'.
- * @const
- */
-Blockly.OUTPUT_VALUE = 2;
-/**
- * ENUM for a down-facing block stack. E.g. 'if-do' or 'else'.
- * @const
- */
-Blockly.NEXT_STATEMENT = 3;
-/**
- * ENUM for an up-facing block stack. E.g. 'break out of loop'.
- * @const
- */
-Blockly.PREVIOUS_STATEMENT = 4;
-/**
- * ENUM for an dummy input. Used to add field(s) with no input.
- * @const
- */
-Blockly.DUMMY_INPUT = 5;
-
-/**
- * ENUM for left alignment.
- * @const
- */
-Blockly.ALIGN_LEFT = -1;
-/**
- * ENUM for centre alignment.
- * @const
- */
-Blockly.ALIGN_CENTRE = 0;
-/**
- * ENUM for right alignment.
- * @const
- */
-Blockly.ALIGN_RIGHT = 1;
-
-/**
- * Lookup table for determining the opposite type of a connection.
- * @const
+ * The main workspace most recently used.
+ * Set by Blockly.WorkspaceSvg.prototype.markFocused
+ * @type {Blockly.Workspace}
*/
-Blockly.OPPOSITE_TYPE = [];
-Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE;
-Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE;
-Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT;
-Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT;
+Blockly.mainWorkspace = null;
/**
* Currently selected block.
@@ -167,36 +87,35 @@ Blockly.highlightedConnection_ = null;
Blockly.localConnection_ = null;
/**
- * Number of pixels the mouse must move before a drag starts.
- */
-Blockly.DRAG_RADIUS = 5;
-
-/**
- * Maximum misalignment between connections for them to snap together.
- */
-Blockly.SNAP_RADIUS = 20;
-
-/**
- * Delay in ms between trigger and bumping unconnected block out of alignment.
+ * All of the connections on blocks that are currently being dragged.
+ * @type {!Array.}
+ * @private
*/
-Blockly.BUMP_DELAY = 250;
+Blockly.draggingConnections_ = [];
/**
- * Number of characters to truncate a collapsed block to.
+ * Connection on the insertion marker block that matches
+ * Blockly.localConnection_ on the dragged block.
+ * @type {Blockly.Connection}
+ * @private
*/
-Blockly.COLLAPSE_CHARS = 30;
+Blockly.insertionMarkerConnection_ = null;
/**
- * Length in ms for a touch to become a long press.
+ * Grayed-out block that indicates to the user what will happen if they release
+ * a drag immediately.
+ * @type {Blockly.Block}
+ * @private
*/
-Blockly.LONGPRESS = 750;
+Blockly.insertionMarker_ = null;
/**
- * The main workspace most recently used.
- * Set by Blockly.WorkspaceSvg.prototype.markFocused
- * @type {Blockly.Workspace}
+ * Connection that was bumped out of the way by an insertion marker, and may
+ * need to be put back as the drag continues.
+ * @type {Blockly.Connection}
+ * @private
*/
-Blockly.mainWorkspace = null;
+Blockly.bumpedConnection_ = null;
/**
* Contents of the local clipboard.
@@ -219,7 +138,7 @@ Blockly.clipboardSource_ = null;
* 2 - Freely draggable.
* @private
*/
-Blockly.dragMode_ = 0;
+Blockly.dragMode_ = Blockly.DRAG_NONE;
/**
* Wrapper function called when a touch mouseUp occurs during a drag operation.
@@ -228,6 +147,16 @@ Blockly.dragMode_ = 0;
*/
Blockly.onTouchUpWrapper_ = null;
+/**
+ * Convert a hue (HSV model) into an RGB hex triplet.
+ * @param {number} hue Hue on a colour wheel (0-360).
+ * @return {string} RGB code, e.g. '#5ba65b'.
+ */
+Blockly.hueToRgb = function(hue) {
+ return goog.color.hsvToHex(hue, Blockly.HSV_SATURATION,
+ Blockly.HSV_VALUE * 255);
+};
+
/**
* Returns the dimensions of the specified SVG image.
* @param {!Element} svg SVG image.
@@ -238,12 +167,37 @@ Blockly.svgSize = function(svg) {
height: svg.cachedHeight_};
};
+/**
+ * Schedule a call to the resize handler. Groups of simultaneous events (e.g.
+ * a tree of blocks being deleted) are merged into one call.
+ * @param {Blockly.WorkspaceSvg} workspace Any workspace in the SVG.
+ */
+Blockly.asyncSvgResize = function(workspace) {
+ if (Blockly.svgResizePending_) {
+ return;
+ }
+ if (!workspace) {
+ workspace = Blockly.getMainWorkspace();
+ }
+ Blockly.svgResizePending_ = true;
+ setTimeout(function() {Blockly.svgResize(workspace);}, 0);
+};
+
+/**
+ * Flag indicating a resize event is scheduled.
+ * Used to fire only one resize after multiple changes.
+ * @type {boolean}
+ * @private
+ */
+Blockly.svgResizePending_ = false;
+
/**
* Size the SVG image to completely fill its container.
* Record the height/width of the SVG image.
* @param {!Blockly.WorkspaceSvg} workspace Any workspace in the SVG.
*/
Blockly.svgResize = function(workspace) {
+ Blockly.svgResizePending_ = false;
var mainWorkspace = workspace;
while (mainWorkspace.options.parentWorkspace) {
mainWorkspace = mainWorkspace.options.parentWorkspace;
@@ -251,7 +205,7 @@ Blockly.svgResize = function(workspace) {
var svg = mainWorkspace.getParentSvg();
var div = svg.parentNode;
if (!div) {
- // Workspace deteted, or something.
+ // Workspace deleted, or something.
return;
}
var width = div.offsetWidth;
@@ -276,7 +230,6 @@ Blockly.onMouseUp_ = function(e) {
var workspace = Blockly.getMainWorkspace();
Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
workspace.isScrolling = false;
-
// Unbind the touch event if it exists.
if (Blockly.onTouchUpWrapper_) {
Blockly.unbindEvent_(Blockly.onTouchUpWrapper_);
@@ -299,27 +252,17 @@ Blockly.onMouseMove_ = function(e) {
}
var workspace = Blockly.getMainWorkspace();
if (workspace.isScrolling) {
- Blockly.removeAllRanges();
var dx = e.clientX - workspace.startDragMouseX;
var dy = e.clientY - workspace.startDragMouseY;
- var metrics = workspace.startDragMetrics;
var x = workspace.startScrollX + dx;
var y = workspace.startScrollY + dy;
- x = Math.min(x, -metrics.contentLeft);
- y = Math.min(y, -metrics.contentTop);
- x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
- metrics.contentWidth);
- y = Math.max(y, metrics.viewHeight - metrics.contentTop -
- metrics.contentHeight);
-
- // Move the scrollbars and the page will scroll automatically.
- workspace.scrollbar.set(-x - metrics.contentLeft,
- -y - metrics.contentTop);
+ workspace.scroll(x, y);
// Cancel the long-press if the drag has moved too far.
if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
Blockly.longStop_();
}
e.stopPropagation();
+ e.preventDefault();
}
};
@@ -329,26 +272,19 @@ Blockly.onMouseMove_ = function(e) {
* @private
*/
Blockly.onKeyDown_ = function(e) {
- if (Blockly.isTargetInput_(e)) {
+ if (Blockly.mainWorkspace.options.readOnly || Blockly.isTargetInput_(e)) {
+ // No key actions on readonly workspaces.
// When focused on an HTML text input widget, don't trap any keys.
return;
}
- var deleteBlock = false;
if (e.keyCode == 27) {
- // Pressing esc closes the context menu.
+ // Pressing esc closes the context menu and any drop-down
Blockly.hideChaff();
+ Blockly.DropDownDiv.hide();
} else if (e.keyCode == 8 || e.keyCode == 46) {
// Delete or backspace.
- try {
- if (Blockly.selected && Blockly.selected.isDeletable()) {
- deleteBlock = true;
- }
- } finally {
- // Stop the browser from going back to the previous page.
- // Use a finally so that any error in delete code above doesn't disappear
- // from the console when the page rolls back.
- e.preventDefault();
- }
+ // Stop the browser from going back to the previous page.
+ e.preventDefault();
} else if (e.altKey || e.ctrlKey || e.metaKey) {
if (Blockly.selected &&
Blockly.selected.isDeletable() && Blockly.selected.isMovable()) {
@@ -359,7 +295,13 @@ Blockly.onKeyDown_ = function(e) {
} else if (e.keyCode == 88) {
// 'x' for cut.
Blockly.copy_(Blockly.selected);
- deleteBlock = true;
+ Blockly.hideChaff();
+ var heal = Blockly.dragMode_ != Blockly.DRAG_FREE;
+ Blockly.selected.dispose(heal, true);
+ if (Blockly.highlightedConnection_) {
+ Blockly.highlightedConnection_.unhighlight();
+ Blockly.highlightedConnection_ = null;
+ }
}
}
if (e.keyCode == 86) {
@@ -367,16 +309,10 @@ Blockly.onKeyDown_ = function(e) {
if (Blockly.clipboardXml_) {
Blockly.clipboardSource_.paste(Blockly.clipboardXml_);
}
- }
- }
- if (deleteBlock) {
- // Common code for delete and cut.
- Blockly.hideChaff();
- var heal = Blockly.dragMode_ != 2;
- Blockly.selected.dispose(heal, true);
- if (Blockly.highlightedConnection_) {
- Blockly.highlightedConnection_.unhighlight();
- Blockly.highlightedConnection_ = null;
+ } else if (e.keyCode == 90) {
+ // 'z' for undo 'Z' is for redo.
+ Blockly.hideChaff();
+ Blockly.mainWorkspace.undo(e.shiftKey);
}
}
};
@@ -433,8 +369,8 @@ Blockly.longStop_ = function() {
* @private
*/
Blockly.copy_ = function(block) {
- var xmlBlock = Blockly.Xml.blockToDom_(block);
- if (Blockly.dragMode_ != 2) {
+ var xmlBlock = Blockly.Xml.blockToDom(block);
+ if (Blockly.dragMode_ != Blockly.DRAG_FREE) {
Blockly.Xml.deleteNext(xmlBlock);
}
// Encode start position in XML.
@@ -520,12 +456,8 @@ Blockly.getMainWorkspaceMetrics_ = function() {
var MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS - 1;
var viewWidth = svgSize.width - MARGIN;
var viewHeight = svgSize.height - MARGIN;
- try {
- var blockBox = this.getCanvas().getBBox();
- } catch (e) {
- // Firefox has trouble with hidden elements (Bug 528969).
- return null;
- }
+ var blockBox = this.getBlocksBoundingBox();
+
// Fix scale.
var contentWidth = blockBox.width * this.scale;
var contentHeight = blockBox.height * this.scale;
@@ -549,7 +481,7 @@ Blockly.getMainWorkspaceMetrics_ = function() {
var bottomEdge = topEdge + blockBox.height;
}
var absoluteLeft = 0;
- if (!this.RTL && this.toolbox_) {
+ if (this.toolbox_ && this.toolbox_.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
absoluteLeft = this.toolbox_.width;
}
var metrics = {
@@ -598,20 +530,6 @@ Blockly.setMainWorkspaceMetrics_ = function(xyRatio) {
}
};
-/**
- * When something in Blockly's workspace changes, call a function.
- * @param {!Function} func Function to call.
- * @return {!Array.} Opaque data that can be passed to
- * removeChangeListener.
- * @deprecated April 2015
- */
-Blockly.addChangeListener = function(func) {
- // Backwards compatability from before there could be multiple workspaces.
- console.warn('Deprecated call to Blockly.addChangeListener, ' +
- 'use workspace.addChangeListener instead.');
- return Blockly.getMainWorkspace().addChangeListener(func);
-};
-
/**
* Returns the main workspace. Returns the last used main workspace (based on
* focus).
diff --git a/core/bubble.js b/core/bubble.js
index 144ff05a25..d4c1e2719f 100644
--- a/core/bubble.js
+++ b/core/bubble.js
@@ -29,23 +29,23 @@ goog.provide('Blockly.Bubble');
goog.require('Blockly.Workspace');
goog.require('goog.dom');
goog.require('goog.math');
+goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
/**
* Class for UI bubble.
- * @param {!Blockly.Workspace} workspace The workspace on which to draw the
+ * @param {!Blockly.WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!Element} content SVG content for the bubble.
* @param {Element} shape SVG element to avoid eclipsing.
- * @param {number} anchorX Absolute horizontal position of bubbles anchor point.
- * @param {number} anchorY Absolute vertical position of bubbles anchor point.
+ * @param {!goog.math.Coodinate} anchorXY Absolute position of bubble's anchor
+ * point.
* @param {?number} bubbleWidth Width of bubble, or null if not resizable.
* @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* @constructor
*/
-Blockly.Bubble = function(workspace, content, shape,
- anchorX, anchorY,
+Blockly.Bubble = function(workspace, content, shape, anchorXY,
bubbleWidth, bubbleHeight) {
this.workspace_ = workspace;
this.content_ = content;
@@ -60,7 +60,7 @@ Blockly.Bubble = function(workspace, content, shape,
var canvas = workspace.getBubbleCanvas();
canvas.appendChild(this.createDom_(content, !!(bubbleWidth && bubbleHeight)));
- this.setAnchorLocation(anchorX, anchorY);
+ this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
var bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
bubbleWidth = bBox.width + 2 * Blockly.Bubble.BORDER_WIDTH;
@@ -123,6 +123,12 @@ Blockly.Bubble.onMouseUpWrapper_ = null;
*/
Blockly.Bubble.onMouseMoveWrapper_ = null;
+/**
+ * Function to call on resize of bubble.
+ * @type {Function}
+ */
+Blockly.Bubble.prototype.resizeCallback_ = null;
+
/**
* Stop binding to the global mouseup and mousemove events.
* @private
@@ -145,16 +151,11 @@ Blockly.Bubble.unbindDragEvents_ = function() {
Blockly.Bubble.prototype.rendered_ = false;
/**
- * Absolute X coordinate of anchor point.
- * @private
- */
-Blockly.Bubble.prototype.anchorX_ = 0;
-
-/**
- * Absolute Y coordinate of anchor point.
+ * Absolute coordinate of anchor point.
+ * @type {goog.math.Coordinate}
* @private
*/
-Blockly.Bubble.prototype.anchorY_ = 0;
+Blockly.Bubble.prototype.anchorXY_ = null;
/**
* Relative X coordinate of bubble with respect to the anchor's centre.
@@ -269,9 +270,9 @@ Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) {
// Left-click (or middle click)
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
- this.workspace_.startDrag(e,
+ this.workspace_.startDrag(e, new goog.math.Coordinate(
this.workspace_.RTL ? -this.relativeLeft_ : this.relativeLeft_,
- this.relativeTop_);
+ this.relativeTop_));
Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document,
'mouseup', this, Blockly.Bubble.unbindDragEvents_);
@@ -312,8 +313,8 @@ Blockly.Bubble.prototype.resizeMouseDown_ = function(e) {
// Left-click (or middle click)
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
- this.workspace_.startDrag(e,
- this.workspace_.RTL ? -this.width_ : this.width_, this.height_);
+ this.workspace_.startDrag(e, new goog.math.Coordinate(
+ this.workspace_.RTL ? -this.width_ : this.width_, this.height_));
Blockly.Bubble.onMouseUpWrapper_ = Blockly.bindEvent_(document,
'mouseup', this, Blockly.Bubble.unbindDragEvents_);
@@ -341,11 +342,10 @@ Blockly.Bubble.prototype.resizeMouseMove_ = function(e) {
/**
* Register a function as a callback event for when the bubble is resized.
- * @param {Object} thisObject The value of 'this' in the callback.
* @param {!Function} callback The function to call on resize.
*/
-Blockly.Bubble.prototype.registerResizeEvent = function(thisObject, callback) {
- Blockly.bindEvent_(this.bubbleGroup_, 'resize', thisObject, callback);
+Blockly.Bubble.prototype.registerResizeEvent = function(callback) {
+ this.resizeCallback_ = callback;
};
/**
@@ -360,12 +360,10 @@ Blockly.Bubble.prototype.promote_ = function() {
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
- * @param {number} x Absolute horizontal location.
- * @param {number} y Absolute vertical location.
+ * @param {!goog.math.Coordinate} xy Absolute location.
*/
-Blockly.Bubble.prototype.setAnchorLocation = function(x, y) {
- this.anchorX_ = x;
- this.anchorY_ = y;
+Blockly.Bubble.prototype.setAnchorLocation = function(xy) {
+ this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
@@ -383,31 +381,32 @@ Blockly.Bubble.prototype.layoutBubble_ = function() {
var metrics = this.workspace_.getMetrics();
metrics.viewWidth /= this.workspace_.scale;
metrics.viewLeft /= this.workspace_.scale;
+ var anchorX = this.anchorXY_.x;
if (this.workspace_.RTL) {
- if (this.anchorX_ - metrics.viewLeft - relativeLeft - this.width_ <
+ if (anchorX - metrics.viewLeft - relativeLeft - this.width_ <
Blockly.Scrollbar.scrollbarThickness) {
// Slide the bubble right until it is onscreen.
- relativeLeft = this.anchorX_ - metrics.viewLeft - this.width_ -
+ relativeLeft = anchorX - metrics.viewLeft - this.width_ -
Blockly.Scrollbar.scrollbarThickness;
- } else if (this.anchorX_ - metrics.viewLeft - relativeLeft >
+ } else if (anchorX - metrics.viewLeft - relativeLeft >
metrics.viewWidth) {
// Slide the bubble left until it is onscreen.
- relativeLeft = this.anchorX_ - metrics.viewLeft - metrics.viewWidth;
+ relativeLeft = anchorX - metrics.viewLeft - metrics.viewWidth;
}
} else {
- if (this.anchorX_ + relativeLeft < metrics.viewLeft) {
+ if (anchorX + relativeLeft < metrics.viewLeft) {
// Slide the bubble right until it is onscreen.
- relativeLeft = metrics.viewLeft - this.anchorX_;
+ relativeLeft = metrics.viewLeft - anchorX;
} else if (metrics.viewLeft + metrics.viewWidth <
- this.anchorX_ + relativeLeft + this.width_ +
+ anchorX + relativeLeft + this.width_ +
Blockly.BlockSvg.SEP_SPACE_X +
Blockly.Scrollbar.scrollbarThickness) {
// Slide the bubble left until it is onscreen.
- relativeLeft = metrics.viewLeft + metrics.viewWidth - this.anchorX_ -
+ relativeLeft = metrics.viewLeft + metrics.viewWidth - anchorX -
this.width_ - Blockly.Scrollbar.scrollbarThickness;
}
}
- if (this.anchorY_ + relativeTop < metrics.viewTop) {
+ if (this.anchorXY_.y + relativeTop < metrics.viewTop) {
// Slide the bubble below the block.
var bBox = /** @type {SVGLocatable} */ (this.shape_).getBBox();
relativeTop = bBox.height;
@@ -421,13 +420,13 @@ Blockly.Bubble.prototype.layoutBubble_ = function() {
* @private
*/
Blockly.Bubble.prototype.positionBubble_ = function() {
- var left;
+ var left = this.anchorXY_.x;
if (this.workspace_.RTL) {
- left = this.anchorX_ - this.relativeLeft_ - this.width_;
+ left -= this.relativeLeft_ + this.width_;
} else {
- left = this.anchorX_ + this.relativeLeft_;
+ left += this.relativeLeft_;
}
- var top = this.relativeTop_ + this.anchorY_;
+ var top = this.relativeTop_ + this.anchorXY_.y;
this.bubbleGroup_.setAttribute('transform',
'translate(' + left + ',' + top + ')');
};
@@ -473,8 +472,10 @@ Blockly.Bubble.prototype.setBubbleSize = function(width, height) {
this.positionBubble_();
this.renderArrow_();
}
- // Fire an event to allow the contents to resize.
- Blockly.fireUiEvent(this.bubbleGroup_, 'resize');
+ // Allow the contents to resize.
+ if (this.resizeCallback_) {
+ this.resizeCallback_();
+ }
};
/**
diff --git a/core/colours.js b/core/colours.js
new file mode 100644
index 0000000000..bf26ffccb3
--- /dev/null
+++ b/core/colours.js
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Massachusetts Institute of Technology
+ * All rights reserved.
+ *
+ * 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.
+ */
+
+'use strict';
+
+goog.provide('Blockly.Colours');
+
+Blockly.Colours = {
+ // SVG colours: these must be specificed in #RRGGBB style
+ // To add an opacity, this must be specified as a separate property (for SVG fill-opacity)
+ "motion": {
+ "primary": "#4C97FF",
+ "secondary": "#4280D7",
+ "tertiary": "#3373CC"
+ },
+ "looks": {
+ "primary": "#9966FF",
+ "secondary": "#855CD6",
+ "tertiary": "#774DCB"
+ },
+ "sounds": {
+ "primary": "#D65CD6",
+ "secondary": "#BF40BF",
+ "tertiary": "#A63FA6"
+ },
+ "control": {
+ "primary": "#FFAB19",
+ "secondary": "#EC9C13",
+ "tertiary": "#CF8B17"
+ },
+ "event": {
+ "primary": "#FFD500",
+ "secondary": "#DBC200",
+ "tertiary": "#CCAA00"
+ },
+ "text": "#575E75",
+ "workspace": "#F5F8FF",
+ "toolbox": "#DDDDDD",
+ "toolboxText": "#000000",
+ "flyout": "#DDDDDD",
+ "scrollbar": "#CCCCCC",
+ "scrollbarHover": '#BBBBBB',
+ "textField": "#FFFFFF",
+ "insertionMarker": "#949494",
+ "insertionMarkerOpacity": 0.6,
+ "dragShadowOpacity": 0.3,
+ "stackGlow": "#FFF200",
+ "stackGlowOpacity": 1,
+ // CSS colours: support RGBA
+ "fieldShadow": "rgba(0,0,0,0.1)",
+ "dropDownShadow": "rgba(0, 0, 0, .3)",
+ "numPadBackground": "#547AB2",
+ "numPadBorder": "#435F91",
+ "numPadActiveBackground": "#435F91",
+ "numPadText": "#FFFFFF"
+};
diff --git a/core/comment.js b/core/comment.js
index e308994b22..2121530c05 100644
--- a/core/comment.js
+++ b/core/comment.js
@@ -106,16 +106,27 @@ Blockly.Comment.prototype.createEditor_ = function() {
var body = document.createElementNS(Blockly.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.HTML_NS);
body.className = 'blocklyMinimalBody';
- this.textarea_ = document.createElementNS(Blockly.HTML_NS, 'textarea');
- this.textarea_.className = 'blocklyCommentTextarea';
- this.textarea_.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
- body.appendChild(this.textarea_);
+ var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea');
+ textarea.className = 'blocklyCommentTextarea';
+ textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
+ body.appendChild(textarea);
+ this.textarea_ = textarea;
this.foreignObject_.appendChild(body);
- Blockly.bindEvent_(this.textarea_, 'mouseup', this, this.textareaFocus_);
+ Blockly.bindEvent_(textarea, 'mouseup', this, this.textareaFocus_);
// Don't zoom with mousewheel.
- Blockly.bindEvent_(this.textarea_, 'wheel', this, function(e) {
+ Blockly.bindEvent_(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
+ Blockly.bindEvent_(textarea, 'change', this, function(e) {
+ if (this.text_ != textarea.value) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.block_, 'comment', null, this.text_, textarea.value));
+ this.text_ = textarea.value;
+ }
+ });
+ setTimeout(function() {
+ textarea.focus();
+ }, 0);
return this.foreignObject_;
};
@@ -139,12 +150,14 @@ Blockly.Comment.prototype.updateEditable = function() {
* @private
*/
Blockly.Comment.prototype.resizeBubble_ = function() {
- var size = this.bubble_.getBubbleSize();
- var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
- this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth);
- this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth);
- this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px';
- this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px';
+ if (this.isVisible()) {
+ var size = this.bubble_.getBubbleSize();
+ var doubleBorderWidth = 2 * Blockly.Bubble.BORDER_WIDTH;
+ this.foreignObject_.setAttribute('width', size.width - doubleBorderWidth);
+ this.foreignObject_.setAttribute('height', size.height - doubleBorderWidth);
+ this.textarea_.style.width = (size.width - doubleBorderWidth - 4) + 'px';
+ this.textarea_.style.height = (size.height - doubleBorderWidth - 4) + 'px';
+ }
};
/**
@@ -156,6 +169,8 @@ Blockly.Comment.prototype.setVisible = function(visible) {
// No change.
return;
}
+ Blockly.Events.fire(
+ new Blockly.Events.Ui(this.block_, 'commentOpen', !visible, visible));
if ((!this.block_.isEditable() && !this.textarea_) || goog.userAgent.IE) {
// Steal the code from warnings to make an uneditable text bubble.
// MSIE does not support foreignobject; textareas are impossible.
@@ -170,13 +185,11 @@ Blockly.Comment.prototype.setVisible = function(visible) {
if (visible) {
// Create the bubble.
this.bubble_ = new Blockly.Bubble(
- /** @type {!Blockly.Workspace} */ (this.block_.workspace),
+ /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.svgPath_,
- this.iconX_, this.iconY_,
- this.width_, this.height_);
- this.bubble_.registerResizeEvent(this, this.resizeBubble_);
+ this.iconXY_, this.width_, this.height_);
+ this.bubble_.registerResizeEvent(this.resizeBubble_.bind(this));
this.updateColour();
- this.text_ = null;
} else {
// Dispose of the bubble.
this.bubble_.dispose();
@@ -243,10 +256,13 @@ Blockly.Comment.prototype.getText = function() {
* @param {string} text Comment text.
*/
Blockly.Comment.prototype.setText = function(text) {
+ if (this.text_ != text) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.block_, 'comment', null, this.text_, text));
+ this.text_ = text;
+ }
if (this.textarea_) {
this.textarea_.value = text;
- } else {
- this.text_ = text;
}
};
@@ -254,6 +270,9 @@ Blockly.Comment.prototype.setText = function(text) {
* Dispose of this comment.
*/
Blockly.Comment.prototype.dispose = function() {
+ if (Blockly.Events.isEnabled()) {
+ this.setText(''); // Fire event to delete comment.
+ }
this.block_.comment = null;
Blockly.Icon.prototype.dispose.call(this);
};
diff --git a/core/connection.js b/core/connection.js
index db5a8a6b98..a11f39c564 100644
--- a/core/connection.js
+++ b/core/connection.js
@@ -25,8 +25,8 @@
'use strict';
goog.provide('Blockly.Connection');
-goog.provide('Blockly.ConnectionDB');
+goog.require('goog.asserts');
goog.require('goog.dom');
@@ -37,7 +37,10 @@ goog.require('goog.dom');
* @constructor
*/
Blockly.Connection = function(source, type) {
- /** @type {!Blockly.Block} */
+ /**
+ * @type {!Blockly.Block}
+ * @private
+ */
this.sourceBlock_ = source;
/** @type {number} */
this.type = type;
@@ -50,6 +53,34 @@ Blockly.Connection = function(source, type) {
}
};
+/**
+ * Constant for identifying connections that accept a boolean.
+ * @const
+ */
+Blockly.Connection.BOOLEAN = 1;
+
+/**
+ * Constant for identifying connections that accept a string.
+ * @const
+ */
+Blockly.Connection.STRING = 2;
+
+/**
+ * Constant for identifying connections that accept a number OR null.
+ * @const
+ */
+Blockly.Connection.NUMBER = 3;
+
+/**
+ * Constants for checking whether two connections are compatible.
+ */
+Blockly.Connection.CAN_CONNECT = 0;
+Blockly.Connection.REASON_SELF_CONNECTION = 1;
+Blockly.Connection.REASON_WRONG_TYPE = 2;
+Blockly.Connection.REASON_TARGET_NULL = 3;
+Blockly.Connection.REASON_CHECKS_FAILED = 4;
+Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5;
+
/**
* Connection this connection connects to. Null if not connected.
* @type {Blockly.Connection}
@@ -113,11 +144,128 @@ Blockly.Connection.prototype.dbOpposite_ = null;
*/
Blockly.Connection.prototype.hidden_ = null;
+/**
+ * Connect two connections together. This is the connection on the superior
+ * block.
+ * @param {!Blockly.Connection} childConnection Connection on inferior block.
+ * @private
+ */
+Blockly.Connection.prototype.connect_ = function(childConnection) {
+ var parentConnection = this;
+ var parentBlock = parentConnection.getSourceBlock();
+ var childBlock = childConnection.getSourceBlock();
+ var isSurroundingC = false;
+ if (parentConnection == parentBlock.getFirstStatementConnection()) {
+ isSurroundingC = true;
+ }
+ // Disconnect any existing parent on the child connection.
+ if (childConnection.isConnected()) {
+ // Scratch-specific behaviour:
+ // If we're using a c-shaped block to surround a stack, remember where the
+ // stack used to be connected.
+ if (isSurroundingC) {
+ var previousParentConnection = childConnection.targetConnection;
+ }
+ childConnection.disconnect();
+ }
+ if (parentConnection.isConnected()) {
+ // Other connection is already connected to something.
+ // Disconnect it and reattach it or bump it as needed.
+ var orphanBlock = parentConnection.targetBlock();
+ var shadowDom = parentConnection.getShadowDom();
+ // Temporarily set the shadow DOM to null so it does not respawn.
+ parentConnection.setShadowDom(null);
+ // Displaced shadow blocks dissolve rather than reattaching or bumping.
+ if (orphanBlock.isShadow()) {
+ // Save the shadow block so that field values are preserved.
+ shadowDom = Blockly.Xml.blockToDom(orphanBlock);
+ orphanBlock.dispose();
+ orphanBlock = null;
+ } else if (parentConnection.type == Blockly.INPUT_VALUE) {
+ // Value connections.
+ // If female block is already connected, disconnect and bump the male.
+ if (!orphanBlock.outputConnection) {
+ throw 'Orphan block does not have an output connection.';
+ }
+ // Attempt to reattach the orphan at the end of the newly inserted
+ // block. Since this block may be a row, walk down to the end
+ // or to the first (and only) shadow block.
+ var connection = Blockly.Connection.lastConnectionInRow_(
+ childBlock, orphanBlock);
+ if (connection) {
+ orphanBlock.outputConnection.connect(connection);
+ orphanBlock = null;
+ }
+ } else if (parentConnection.type == Blockly.NEXT_STATEMENT) {
+ // Statement connections.
+ // Statement blocks may be inserted into the middle of a stack.
+ // Split the stack.
+ if (!orphanBlock.previousConnection) {
+ throw 'Orphan block does not have a previous connection.';
+ }
+ // Attempt to reattach the orphan at the bottom of the newly inserted
+ // block. Since this block may be a stack, walk down to the end.
+ var newBlock = childBlock;
+ while (newBlock.nextConnection) {
+ if (newBlock.nextConnection.isConnected()) {
+ newBlock = newBlock.getNextBlock();
+ } else {
+ if (orphanBlock.previousConnection.checkType_(
+ newBlock.nextConnection)) {
+ newBlock.nextConnection.connect(orphanBlock.previousConnection);
+ orphanBlock = null;
+ }
+ break;
+ }
+ }
+ }
+ if (orphanBlock) {
+ // Unable to reattach orphan.
+ parentConnection.disconnect();
+ if (Blockly.Events.recordUndo) {
+ // Bump it off to the side after a moment.
+ var group = Blockly.Events.getGroup();
+ setTimeout(function() {
+ // Verify orphan hasn't been deleted or reconnected (user on meth).
+ if (orphanBlock.workspace && !orphanBlock.getParent()) {
+ Blockly.Events.setGroup(group);
+ if (orphanBlock.outputConnection) {
+ orphanBlock.outputConnection.bumpAwayFrom_(parentConnection);
+ } else if (orphanBlock.previousConnection) {
+ orphanBlock.previousConnection.bumpAwayFrom_(parentConnection);
+ }
+ Blockly.Events.setGroup(false);
+ }
+ }, Blockly.BUMP_DELAY);
+ }
+ }
+ // Restore the shadow DOM.
+ parentConnection.setShadowDom(shadowDom);
+ }
+
+ if (isSurroundingC && previousParentConnection) {
+ previousParentConnection.connect(parentBlock.previousConnection);
+ }
+
+ var event;
+ if (Blockly.Events.isEnabled()) {
+ event = new Blockly.Events.Move(childBlock);
+ }
+ // Establish the connections.
+ Blockly.Connection.connectReciprocally_(parentConnection, childConnection);
+ // Demote the inferior block so that one is a child of the superior one.
+ childBlock.setParent(parentBlock);
+ if (event) {
+ event.recordNew();
+ Blockly.Events.fire(event);
+ }
+};
+
/**
* Sever all links to this connection (not including from the source object).
*/
Blockly.Connection.prototype.dispose = function() {
- if (this.targetConnection) {
+ if (this.isConnected()) {
throw 'Disconnect connection before disposing of it.';
}
if (this.inDB_) {
@@ -133,6 +281,22 @@ Blockly.Connection.prototype.dispose = function() {
this.dbOpposite_ = null;
};
+/**
+ * @return {boolean} true if the connection is not connected or is connected to
+ * an insertion marker, false otherwise.
+ */
+Blockly.Connection.prototype.isConnectedToNonInsertionMarker = function() {
+ return this.targetConnection && !this.targetBlock().isInsertionMarker();
+};
+
+/**
+ * Get the source block for this connection.
+ * @return {Blockly.Block} The source block, or null if there is none.
+ */
+Blockly.Connection.prototype.getSourceBlock = function() {
+ return this.sourceBlock_;
+};
+
/**
* Does the connection belong to a superior block (higher in the source stack)?
* @return {boolean} True if connection faces down or right.
@@ -143,130 +307,207 @@ Blockly.Connection.prototype.isSuperior = function() {
};
/**
- * Connect this connection to another connection.
- * @param {!Blockly.Connection} otherConnection Connection to connect to.
+ * Is the connection connected?
+ * @return {boolean} True if connection is connected to another connection.
*/
-Blockly.Connection.prototype.connect = function(otherConnection) {
- if (this.sourceBlock_ == otherConnection.sourceBlock_) {
- throw 'Attempted to connect a block to itself.';
+Blockly.Connection.prototype.isConnected = function() {
+ return !!this.targetConnection;
+};
+
+/**
+ * Checks whether the current connection can connect with the target
+ * connection.
+ * @param {Blockly.Connection} target Connection to check compatibility with.
+ * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal,
+ * an error code otherwise.
+ * @private
+ */
+Blockly.Connection.prototype.canConnectWithReason_ = function(target) {
+ if (!target) {
+ return Blockly.Connection.REASON_TARGET_NULL;
+ } else if (this.sourceBlock_ &&
+ target.getSourceBlock() == this.sourceBlock_) {
+ return Blockly.Connection.REASON_SELF_CONNECTION;
+ } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) {
+ return Blockly.Connection.REASON_WRONG_TYPE;
+ } else if (this.sourceBlock_ && target.getSourceBlock() &&
+ this.sourceBlock_.workspace !== target.getSourceBlock().workspace) {
+ return Blockly.Connection.REASON_DIFFERENT_WORKSPACES;
+ } else if (!this.checkType_(target)) {
+ return Blockly.Connection.REASON_CHECKS_FAILED;
+ }
+ return Blockly.Connection.CAN_CONNECT;
+};
+
+/**
+ * Checks whether the current connection and target connection are compatible
+ * and throws an exception if they are not.
+ * @param {Blockly.Connection} target The connection to check compatibility
+ * with.
+ * @private
+ */
+Blockly.Connection.prototype.checkConnection_ = function(target) {
+ switch (this.canConnectWithReason_(target)) {
+ case Blockly.Connection.CAN_CONNECT:
+ break;
+ case Blockly.Connection.REASON_SELF_CONNECTION:
+ throw 'Attempted to connect a block to itself.';
+ case Blockly.Connection.REASON_DIFFERENT_WORKSPACES:
+ // Usually this means one block has been deleted.
+ throw 'Blocks not on same workspace.';
+ case Blockly.Connection.REASON_WRONG_TYPE:
+ throw 'Attempt to connect incompatible types.';
+ case Blockly.Connection.REASON_TARGET_NULL:
+ throw 'Target connection is null.';
+ case Blockly.Connection.REASON_CHECKS_FAILED:
+ throw 'Connection checks failed.';
+ default:
+ throw 'Unknown connection failure: this should never happen!';
}
- if (this.sourceBlock_.workspace !== otherConnection.sourceBlock_.workspace) {
- throw 'Blocks are on different workspaces.';
+};
+
+/**
+ * Check if the two connections can be dragged to connect to each other.
+ * This is used by the connection database when searching for the closest
+ * connection.
+ * @param {!Blockly.Connection} candidate A nearby connection to check.
+ * @return {boolean} True if the connection is allowed, false otherwise.
+ */
+Blockly.Connection.prototype.isConnectionAllowed = function(candidate) {
+
+ // Don't consider insertion markers.
+ if (candidate.sourceBlock_.isInsertionMarker()) {
+ return false;
}
- if (Blockly.OPPOSITE_TYPE[this.type] != otherConnection.type) {
- throw 'Attempt to connect incompatible types.';
+
+ // Type checking.
+ var canConnect = this.canConnectWithReason_(candidate);
+ if (canConnect != Blockly.Connection.CAN_CONNECT &&
+ canConnect != Blockly.Connection.REASON_MUST_DISCONNECT) {
+ return false;
}
- if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) {
- if (this.targetConnection) {
- // Can't make a value connection if male block is already connected.
- throw 'Source connection already connected (value).';
- } else if (otherConnection.targetConnection) {
- // If female block is already connected, disconnect and bump the male.
- var orphanBlock = otherConnection.targetBlock();
- orphanBlock.setParent(null);
- if (orphanBlock.isShadow()) {
- orphanBlock.dispose();
- } else {
- if (!orphanBlock.outputConnection) {
- throw 'Orphan block does not have an output connection.';
+
+ var firstStatementConnection =
+ this.sourceBlock_.getFirstStatementConnection();
+ switch (candidate.type) {
+ case Blockly.PREVIOUS_STATEMENT: {
+ if (!firstStatementConnection || this != firstStatementConnection) {
+ if (this.targetConnection) {
+ return false;
}
- // Attempt to reattach the orphan at the end of the newly inserted
- // block. Since this block may be a row, walk down to the end.
- var newBlock = this.sourceBlock_;
- var connection;
- while (connection = Blockly.Connection.singleConnection_(
- /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) {
- // '=' is intentional in line above.
- newBlock = connection.targetBlock();
- if (!newBlock || newBlock.isShadow()) {
- orphanBlock.outputConnection.connect(connection);
- orphanBlock = null;
- break;
+ if (candidate.targetConnection) {
+ // If the other side of this connection is the active insertion marker
+ // connection, we've obviously already decided that this is a good
+ // connection.
+ if (candidate.targetConnection ==
+ Blockly.insertionMarkerConnection_) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ // Scratch-specific behaviour:
+ // If this is a c-shaped block, statement blocks cannot be connected
+ // anywhere other than inside the first statement input.
+ if (firstStatementConnection) {
+ // Can't connect if there is already a block inside the first statement
+ // input.
+ if (this == firstStatementConnection) {
+ if (this.targetConnection) {
+ return false;
}
}
- if (orphanBlock) {
- // Unable to reattach orphan. Bump it off to the side.
- setTimeout(function() {
- orphanBlock.outputConnection.bumpAwayFrom_(otherConnection);
- }, Blockly.BUMP_DELAY);
+ // Can't connect this block's next connection unless we're connecting
+ // in front of the first block on a stack.
+ else if (this == this.sourceBlock_.nextConnection &&
+ candidate.isConnectedToNonInsertionMarker()) {
+ return false;
}
}
+ break;
}
- } else {
- if (this.targetConnection) {
- throw 'Source connection already connected (block).';
- } else if (otherConnection.targetConnection) {
- // Statement blocks may be inserted into the middle of a stack.
- if (this.type != Blockly.PREVIOUS_STATEMENT) {
- throw 'Can only do a mid-stack connection with the top of a block.';
+ case Blockly.OUTPUT_VALUE: {
+ // Don't offer to connect an already connected left (male) value plug to
+ // an available right (female) value plug.
+ if (candidate.targetConnection || this.targetConnection) {
+ return false;
}
- // Split the stack.
- var orphanBlock = otherConnection.targetBlock();
- orphanBlock.setParent(null);
- if (!orphanBlock.previousConnection) {
- throw 'Orphan block does not have a previous connection.';
+ break;
+ }
+ case Blockly.INPUT_VALUE: {
+ // Offering to connect the left (male) of a value block to an already
+ // connected value pair is ok, we'll splice it in.
+ // However, don't offer to splice into an unmovable block.
+ if (candidate.targetConnection &&
+ !candidate.targetBlock().isMovable() &&
+ !candidate.targetBlock().isShadow()) {
+ return false;
}
- // Attempt to reattach the orphan at the bottom of the newly inserted
- // block. Since this block may be a stack, walk down to the end.
- var newBlock = this.sourceBlock_;
- while (newBlock.nextConnection) {
- if (newBlock.nextConnection.targetConnection) {
- newBlock = newBlock.getNextBlock();
- } else {
- if (orphanBlock.previousConnection.checkType_(
- newBlock.nextConnection)) {
- newBlock.nextConnection.connect(orphanBlock.previousConnection);
- orphanBlock = null;
- }
- break;
- }
+ break;
+ }
+ case Blockly.NEXT_STATEMENT: {
+ // Scratch-specific behaviour:
+ // If this is a c-block, we can't connect this block's
+ // previous connection unless we're connecting to the end of the last
+ // block on a stack or there's already a block connected inside the c.
+ if (firstStatementConnection &&
+ this == this.sourceBlock_.previousConnection &&
+ candidate.isConnectedToNonInsertionMarker() &&
+ !firstStatementConnection.targetConnection) {
+ return false;
}
- if (orphanBlock) {
- // Unable to reattach orphan. Bump it off to the side.
- setTimeout(function() {
- orphanBlock.previousConnection.bumpAwayFrom_(otherConnection);
- }, Blockly.BUMP_DELAY);
+ // Don't let a block with no next connection bump other blocks out of the
+ // stack.
+ if (candidate.isConnectedToNonInsertionMarker() &&
+ !this.sourceBlock_.nextConnection) {
+ return false;
}
+ break;
}
+ default:
+ throw 'Unknown connection type in isConnectionAllowed';
+ }
+
+ // Don't let blocks try to connect to themselves or ones they nest.
+ if (Blockly.draggingConnections_.indexOf(candidate) != -1) {
+ return false;
}
+ return true;
+};
+
+/**
+ * Connect this connection to another connection.
+ * @param {!Blockly.Connection} otherConnection Connection to connect to.
+ */
+Blockly.Connection.prototype.connect = function(otherConnection) {
+ if (this.targetConnection == otherConnection) {
+ // Already connected together. NOP.
+ return;
+ }
+ this.checkConnection_(otherConnection);
// Determine which block is superior (higher in the source stack).
- var parentBlock, childBlock;
if (this.isSuperior()) {
// Superior block.
- parentBlock = this.sourceBlock_;
- childBlock = otherConnection.sourceBlock_;
+ this.connect_(otherConnection);
} else {
// Inferior block.
- parentBlock = otherConnection.sourceBlock_;
- childBlock = this.sourceBlock_;
+ otherConnection.connect_(this);
}
+};
- // Establish the connections.
- this.targetConnection = otherConnection;
- otherConnection.targetConnection = this;
-
- // Demote the inferior block so that one is a child of the superior one.
- childBlock.setParent(parentBlock);
-
- if (parentBlock.rendered) {
- parentBlock.updateDisabled();
- }
- if (childBlock.rendered) {
- childBlock.updateDisabled();
- }
- if (parentBlock.rendered && childBlock.rendered) {
- if (this.type == Blockly.NEXT_STATEMENT ||
- this.type == Blockly.PREVIOUS_STATEMENT) {
- // Child block may need to square off its corners if it is in a stack.
- // Rendering a child will render its parent.
- childBlock.render();
- } else {
- // Child block does not change shape. Rendering the parent node will
- // move its connected children into position.
- parentBlock.render();
- }
- }
+/**
+ * Update two connections to target each other.
+ * @param {Blockly.Connection} first The first connection to update.
+ * @param {Blockly.Connection} second The second conneciton to update.
+ * @private
+ */
+Blockly.Connection.connectReciprocally_ = function(first, second) {
+ goog.asserts.assert(first && second, 'Cannot connect null connections.');
+ first.targetConnection = second;
+ second.targetConnection = first;
};
/**
@@ -293,302 +534,112 @@ Blockly.Connection.singleConnection_ = function(block, orphanBlock) {
return connection;
};
+/**
+ * Walks down a row a blocks, at each stage checking if there are any
+ * connections that will accept the orphaned block. If at any point there
+ * are zero or multiple eligible connections, returns null. Otherwise
+ * returns the only input on the last block in the chain.
+ * Terminates early for shadow blocks.
+ * @param {!Blockly.Block} startBlock The block on which to start the search.
+ * @param {!Blockly.Block} orphanBlock The block that is looking for a home.
+ * @return {Blockly.Connection} The suitable connection point on the chain
+ * of blocks, or null.
+ * @private
+ */
+Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) {
+ var newBlock = startBlock;
+ var connection;
+ while (connection = Blockly.Connection.singleConnection_(
+ /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) {
+ // '=' is intentional in line above.
+ newBlock = connection.targetBlock();
+ if (!newBlock || newBlock.isShadow()) {
+ return connection;
+ }
+ }
+ return null;
+};
+
/**
* Disconnect this connection.
*/
Blockly.Connection.prototype.disconnect = function() {
var otherConnection = this.targetConnection;
- if (!otherConnection) {
- throw 'Source connection not connected.';
- } else if (otherConnection.targetConnection != this) {
- throw 'Target connection not connected to source connection.';
- }
- otherConnection.targetConnection = null;
- this.targetConnection = null;
+ goog.asserts.assert(otherConnection, 'Source connection not connected.');
+ goog.asserts.assert(otherConnection.targetConnection == this,
+ 'Target connection not connected to source connection.');
- // Rerender the parent so that it may reflow.
var parentBlock, childBlock, parentConnection;
if (this.isSuperior()) {
// Superior block.
parentBlock = this.sourceBlock_;
- childBlock = otherConnection.sourceBlock_;
+ childBlock = otherConnection.getSourceBlock();
parentConnection = this;
} else {
// Inferior block.
- parentBlock = otherConnection.sourceBlock_;
+ parentBlock = otherConnection.getSourceBlock();
childBlock = this.sourceBlock_;
parentConnection = otherConnection;
}
- var shadow = parentConnection.getShadowDom();
- if (parentBlock.workspace && !childBlock.isShadow() && shadow) {
- // Respawn the shadow block.
- var blockShadow =
- Blockly.Xml.domToBlock(parentBlock.workspace, shadow);
- if (blockShadow.outputConnection) {
- parentConnection.connect(blockShadow.outputConnection);
- } else if (blockShadow.previousConnection) {
- parentConnection.connect(blockShadow.previousConnection);
- } else {
- throw 'Child block does not have output or previous statement.';
- }
- blockShadow.initSvg();
- blockShadow.render(false);
- }
- if (parentBlock.rendered) {
- parentBlock.render();
- }
- if (childBlock.rendered) {
- childBlock.updateDisabled();
- childBlock.render();
- }
+ this.disconnectInternal_(parentBlock, childBlock);
+ parentConnection.respawnShadow_();
};
/**
- * Returns the block that this connection connects to.
- * @return {Blockly.Block} The connected block or null if none is connected.
- */
-Blockly.Connection.prototype.targetBlock = function() {
- if (this.targetConnection) {
- return this.targetConnection.sourceBlock_;
- }
- return null;
-};
-
-/**
- * Move the block(s) belonging to the connection to a point where they don't
- * visually interfere with the specified connection.
- * @param {!Blockly.Connection} staticConnection The connection to move away
- * from.
+ * Disconnect two blocks that are connected by this connection.
+ * @param {!Blockly.Block} parentBlock The superior block.
+ * @param {!Blockly.Block} childBlock The inferior block.
* @private
*/
-Blockly.Connection.prototype.bumpAwayFrom_ = function(staticConnection) {
- if (Blockly.dragMode_ != 0) {
- // Don't move blocks around while the user is doing the same.
- return;
- }
- // Move the root block.
- var rootBlock = this.sourceBlock_.getRootBlock();
- if (rootBlock.isInFlyout) {
- // Don't move blocks around in a flyout.
- return;
- }
- var reverse = false;
- if (!rootBlock.isMovable()) {
- // Can't bump an uneditable block away.
- // Check to see if the other block is movable.
- rootBlock = staticConnection.sourceBlock_.getRootBlock();
- if (!rootBlock.isMovable()) {
- return;
- }
- // Swap the connections and move the 'static' connection instead.
- staticConnection = this;
- reverse = true;
- }
- // Raise it to the top for extra visibility.
- rootBlock.getSvgRoot().parentNode.appendChild(rootBlock.getSvgRoot());
- var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS) - this.x_;
- var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS) - this.y_;
- if (reverse) {
- // When reversing a bump due to an uneditable block, bump up.
- dy = -dy;
- }
- if (rootBlock.RTL) {
- dx = -dx;
- }
- rootBlock.moveBy(dx, dy);
-};
-
-/**
- * Change the connection's coordinates.
- * @param {number} x New absolute x coordinate.
- * @param {number} y New absolute y coordinate.
- */
-Blockly.Connection.prototype.moveTo = function(x, y) {
- // Remove it from its old location in the database (if already present)
- if (this.inDB_) {
- this.db_.removeConnection_(this);
+Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock,
+ childBlock) {
+ var event;
+ if (Blockly.Events.isEnabled()) {
+ event = new Blockly.Events.Move(childBlock);
}
- this.x_ = x;
- this.y_ = y;
- // Insert it into its new location in the database.
- if (!this.hidden_) {
- this.db_.addConnection_(this);
- }
-};
-
-/**
- * Change the connection's coordinates.
- * @param {number} dx Change to x coordinate.
- * @param {number} dy Change to y coordinate.
- */
-Blockly.Connection.prototype.moveBy = function(dx, dy) {
- this.moveTo(this.x_ + dx, this.y_ + dy);
-};
-
-/**
- * Add highlighting around this connection.
- */
-Blockly.Connection.prototype.highlight = function() {
- var steps;
- if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) {
- var tabWidth = this.sourceBlock_.RTL ? -Blockly.BlockSvg.TAB_WIDTH :
- Blockly.BlockSvg.TAB_WIDTH;
- steps = 'm 0,0 v 5 c 0,10 ' + -tabWidth + ',-8 ' + -tabWidth + ',7.5 s ' +
- tabWidth + ',-2.5 ' + tabWidth + ',7.5 v 5';
- } else {
- if (this.sourceBlock_.RTL) {
- steps = 'm 20,0 h -5 ' + Blockly.BlockSvg.NOTCH_PATH_RIGHT + ' h -5';
- } else {
- steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5';
- }
+ var otherConnection = this.targetConnection;
+ otherConnection.targetConnection = null;
+ this.targetConnection = null;
+ childBlock.setParent(null);
+ if (event) {
+ event.recordNew();
+ Blockly.Events.fire(event);
}
- var xy = this.sourceBlock_.getRelativeToSurfaceXY();
- var x = this.x_ - xy.x;
- var y = this.y_ - xy.y;
- Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path',
- {'class': 'blocklyHighlightedConnectionPath',
- 'd': steps,
- transform: 'translate(' + x + ',' + y + ')'},
- this.sourceBlock_.getSvgRoot());
};
/**
- * Remove the highlighting around this connection.
- */
-Blockly.Connection.prototype.unhighlight = function() {
- goog.dom.removeNode(Blockly.Connection.highlightedPath_);
- delete Blockly.Connection.highlightedPath_;
-};
-
-/**
- * Move the blocks on either side of this connection right next to each other.
+ * Respawn the shadow block if there was one connected to the this connection.
+ * @return {Blockly.Block} The newly spawned shadow block, or null if none was
+ * spawned.
* @private
*/
-Blockly.Connection.prototype.tighten_ = function() {
- var dx = this.targetConnection.x_ - this.x_;
- var dy = this.targetConnection.y_ - this.y_;
- if (dx != 0 || dy != 0) {
- var block = this.targetBlock();
- var svgRoot = block.getSvgRoot();
- if (!svgRoot) {
- throw 'block is not rendered.';
+Blockly.Connection.prototype.respawnShadow_ = function() {
+ var parentBlock = this.getSourceBlock();
+ var shadow = this.getShadowDom();
+ if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
+ var blockShadow =
+ Blockly.Xml.domToBlock(shadow, parentBlock.workspace);
+ if (blockShadow.outputConnection) {
+ this.connect(blockShadow.outputConnection);
+ } else if (blockShadow.previousConnection) {
+ this.connect(blockShadow.previousConnection);
+ } else {
+ throw 'Child block does not have output or previous statement.';
}
- var xy = Blockly.getRelativeXY_(svgRoot);
- block.getSvgRoot().setAttribute('transform',
- 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
- block.moveConnections_(-dx, -dy);
+ return blockShadow;
}
+ return null;
};
/**
- * Find the closest compatible connection to this connection.
- * @param {number} maxLimit The maximum radius to another connection.
- * @param {number} dx Horizontal offset between this connection's location
- * in the database and the current location (as a result of dragging).
- * @param {number} dy Vertical offset between this connection's location
- * in the database and the current location (as a result of dragging).
- * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two properties: 'connection' which is either
- * another connection or null, and 'radius' which is the distance.
+ * Returns the block that this connection connects to.
+ * @return {Blockly.Block} The connected block or null if none is connected.
*/
-Blockly.Connection.prototype.closest = function(maxLimit, dx, dy) {
- if (this.targetConnection) {
- // Don't offer to connect to a connection that's already connected.
- return {connection: null, radius: maxLimit};
- }
- // Determine the opposite type of connection.
- var db = this.dbOpposite_;
-
- // Since this connection is probably being dragged, add the delta.
- var currentX = this.x_ + dx;
- var currentY = this.y_ + dy;
-
- // Binary search to find the closest y location.
- var pointerMin = 0;
- var pointerMax = db.length - 2;
- var pointerMid = pointerMax;
- while (pointerMin < pointerMid) {
- if (db[pointerMid].y_ < currentY) {
- pointerMin = pointerMid;
- } else {
- pointerMax = pointerMid;
- }
- pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- }
-
- // Walk forward and back on the y axis looking for the closest x,y point.
- pointerMin = pointerMid;
- pointerMax = pointerMid;
- var closestConnection = null;
- var sourceBlock = this.sourceBlock_;
- var thisConnection = this;
- if (db.length) {
- while (pointerMin >= 0 && checkConnection_(pointerMin)) {
- pointerMin--;
- }
- do {
- pointerMax++;
- } while (pointerMax < db.length && checkConnection_(pointerMax));
- }
-
- /**
- * Computes if the current connection is within the allowed radius of another
- * connection.
- * This function is a closure and has access to outside variables.
- * @param {number} yIndex The other connection's index in the database.
- * @return {boolean} True if the search needs to continue: either the current
- * connection's vertical distance from the other connection is less than
- * the allowed radius, or if the connection is not compatible.
- * @private
- */
- function checkConnection_(yIndex) {
- var connection = db[yIndex];
- if (connection.type == Blockly.OUTPUT_VALUE ||
- connection.type == Blockly.PREVIOUS_STATEMENT) {
- // Don't offer to connect an already connected left (male) value plug to
- // an available right (female) value plug. Don't offer to connect the
- // bottom of a statement block to one that's already connected.
- if (connection.targetConnection) {
- return true;
- }
- }
- // Offering to connect the top of a statement block to an already connected
- // connection is ok, we'll just insert it into the stack.
-
- // Offering to connect the left (male) of a value block to an already
- // connected value pair is ok, we'll splice it in.
- // However, don't offer to splice into an unmovable block.
- if (connection.type == Blockly.INPUT_VALUE &&
- connection.targetConnection &&
- !connection.targetBlock().isMovable() &&
- !connection.targetBlock().isShadow()) {
- return true;
- }
-
- // Do type checking.
- if (!thisConnection.checkType_(connection)) {
- return true;
- }
-
- // Don't let blocks try to connect to themselves or ones they nest.
- var targetSourceBlock = connection.sourceBlock_;
- do {
- if (sourceBlock == targetSourceBlock) {
- return true;
- }
- targetSourceBlock = targetSourceBlock.getParent();
- } while (targetSourceBlock);
-
- // Only connections within the maxLimit radius.
- var dx = currentX - connection.x_;
- var dy = currentY - connection.y_;
- var r = Math.sqrt(dx * dx + dy * dy);
- if (r <= maxLimit) {
- closestConnection = connection;
- maxLimit = r;
- }
- return Math.abs(dy) < maxLimit;
+Blockly.Connection.prototype.targetBlock = function() {
+ if (this.isConnected()) {
+ return this.targetConnection.getSourceBlock();
}
- return {connection: closestConnection, radius: maxLimit};
+ return null;
};
/**
@@ -599,17 +650,6 @@ Blockly.Connection.prototype.closest = function(maxLimit, dx, dy) {
* @private
*/
Blockly.Connection.prototype.checkType_ = function(otherConnection) {
- // Don't split a connection where both sides are immovable.
- var thisTargetBlock = this.targetBlock();
- if (thisTargetBlock && !thisTargetBlock.isMovable() &&
- !this.sourceBlock_.isMovable()) {
- return false;
- }
- var otherTargetBlock = otherConnection.targetBlock();
- if (otherTargetBlock && !otherTargetBlock.isMovable() &&
- !otherConnection.sourceBlock_.isMovable()) {
- return false;
- }
if (!this.check_ || !otherConnection.check_) {
// One or both sides are promiscuous enough that anything will fit.
return true;
@@ -639,12 +679,9 @@ Blockly.Connection.prototype.setCheck = function(check) {
}
this.check_ = check;
// The new value type may not be compatible with the existing connection.
- if (this.targetConnection && !this.checkType_(this.targetConnection)) {
- if (this.isSuperior()) {
- this.targetBlock().setParent(null);
- } else {
- this.sourceBlock_.setParent(null);
- }
+ if (this.isConnected() && !this.checkType_(this.targetConnection)) {
+ var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
+ child.unplug();
// Bump away.
this.sourceBlock_.bumpNeighbours_();
}
@@ -654,6 +691,22 @@ Blockly.Connection.prototype.setCheck = function(check) {
return this;
};
+/**
+ * Returns a shape enum for this connection.
+ * @return {number} Enum representing shape.
+ */
+Blockly.Connection.prototype.getOutputShape = function() {
+ if (!this.check_) return Blockly.Connection.NUMBER;
+ if (this.check_.indexOf('Boolean') !== -1) {
+ return Blockly.Connection.BOOLEAN;
+ }
+ if (this.check_.indexOf('String') !== -1) {
+ return Blockly.Connection.STRING;
+ }
+
+ return Blockly.Connection.NUMBER;
+};
+
/**
* Change a connection's shadow block.
* @param {Element} shadow DOM representation of a block or null.
@@ -669,251 +722,3 @@ Blockly.Connection.prototype.setShadowDom = function(shadow) {
Blockly.Connection.prototype.getShadowDom = function() {
return this.shadowDom_;
};
-
-/**
- * Find all nearby compatible connections to this connection.
- * Type checking does not apply, since this function is used for bumping.
- * @param {number} maxLimit The maximum radius to another connection.
- * @return {!Array.} List of connections.
- * @private
- */
-Blockly.Connection.prototype.neighbours_ = function(maxLimit) {
- // Determine the opposite type of connection.
- var db = this.dbOpposite_;
-
- var currentX = this.x_;
- var currentY = this.y_;
-
- // Binary search to find the closest y location.
- var pointerMin = 0;
- var pointerMax = db.length - 2;
- var pointerMid = pointerMax;
- while (pointerMin < pointerMid) {
- if (db[pointerMid].y_ < currentY) {
- pointerMin = pointerMid;
- } else {
- pointerMax = pointerMid;
- }
- pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- }
-
- // Walk forward and back on the y axis looking for the closest x,y point.
- pointerMin = pointerMid;
- pointerMax = pointerMid;
- var neighbours = [];
- var sourceBlock = this.sourceBlock_;
- if (db.length) {
- while (pointerMin >= 0 && checkConnection_(pointerMin)) {
- pointerMin--;
- }
- do {
- pointerMax++;
- } while (pointerMax < db.length && checkConnection_(pointerMax));
- }
-
- /**
- * Computes if the current connection is within the allowed radius of another
- * connection.
- * This function is a closure and has access to outside variables.
- * @param {number} yIndex The other connection's index in the database.
- * @return {boolean} True if the current connection's vertical distance from
- * the other connection is less than the allowed radius.
- */
- function checkConnection_(yIndex) {
- var dx = currentX - db[yIndex].x_;
- var dy = currentY - db[yIndex].y_;
- var r = Math.sqrt(dx * dx + dy * dy);
- if (r <= maxLimit) {
- neighbours.push(db[yIndex]);
- }
- return dy < maxLimit;
- }
- return neighbours;
-};
-
-/**
- * Set whether this connections is hidden (not tracked in a database) or not.
- * @param {boolean} hidden True if connection is hidden.
- */
-Blockly.Connection.prototype.setHidden = function(hidden) {
- this.hidden_ = hidden;
- if (hidden && this.inDB_) {
- this.db_.removeConnection_(this);
- } else if (!hidden && !this.inDB_) {
- this.db_.addConnection_(this);
- }
-};
-
-/**
- * Hide this connection, as well as all down-stream connections on any block
- * attached to this connection. This happens when a block is collapsed.
- * Also hides down-stream comments.
- */
-Blockly.Connection.prototype.hideAll = function() {
- this.setHidden(true);
- if (this.targetConnection) {
- var blocks = this.targetBlock().getDescendants();
- for (var b = 0; b < blocks.length; b++) {
- var block = blocks[b];
- // Hide all connections of all children.
- var connections = block.getConnections_(true);
- for (var c = 0; c < connections.length; c++) {
- connections[c].setHidden(true);
- }
- // Close all bubbles of all children.
- var icons = block.getIcons();
- for (var i = 0; i < icons.length; i++) {
- icons[i].setVisible(false);
- }
- }
- }
-};
-
-/**
- * Unhide this connection, as well as all down-stream connections on any block
- * attached to this connection. This happens when a block is expanded.
- * Also unhides down-stream comments.
- * @return {!Array.} List of blocks to render.
- */
-Blockly.Connection.prototype.unhideAll = function() {
- this.setHidden(false);
- // All blocks that need unhiding must be unhidden before any rendering takes
- // place, since rendering requires knowing the dimensions of lower blocks.
- // Also, since rendering a block renders all its parents, we only need to
- // render the leaf nodes.
- var renderList = [];
- if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) {
- // Only spider down.
- return renderList;
- }
- var block = this.targetBlock();
- if (block) {
- var connections;
- if (block.isCollapsed()) {
- // This block should only be partially revealed since it is collapsed.
- connections = [];
- block.outputConnection && connections.push(block.outputConnection);
- block.nextConnection && connections.push(block.nextConnection);
- block.previousConnection && connections.push(block.previousConnection);
- } else {
- // Show all connections of this block.
- connections = block.getConnections_(true);
- }
- for (var c = 0; c < connections.length; c++) {
- renderList.push.apply(renderList, connections[c].unhideAll());
- }
- if (renderList.length == 0) {
- // Leaf block.
- renderList[0] = block;
- }
- }
- return renderList;
-};
-
-
-/**
- * Database of connections.
- * Connections are stored in order of their vertical component. This way
- * connections in an area may be looked up quickly using a binary search.
- * @constructor
- */
-Blockly.ConnectionDB = function() {
-};
-
-Blockly.ConnectionDB.prototype = new Array();
-/**
- * Don't inherit the constructor from Array.
- * @type {!Function}
- */
-Blockly.ConnectionDB.constructor = Blockly.ConnectionDB;
-
-/**
- * Add a connection to the database. Must not already exist in DB.
- * @param {!Blockly.Connection} connection The connection to be added.
- * @private
- */
-Blockly.ConnectionDB.prototype.addConnection_ = function(connection) {
- if (connection.inDB_) {
- throw 'Connection already in database.';
- }
- if (connection.sourceBlock_.isInFlyout) {
- // Don't bother maintaining a database of connections in a flyout.
- return;
- }
- // Insert connection using binary search.
- var pointerMin = 0;
- var pointerMax = this.length;
- while (pointerMin < pointerMax) {
- var pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- if (this[pointerMid].y_ < connection.y_) {
- pointerMin = pointerMid + 1;
- } else if (this[pointerMid].y_ > connection.y_) {
- pointerMax = pointerMid;
- } else {
- pointerMin = pointerMid;
- break;
- }
- }
- this.splice(pointerMin, 0, connection);
- connection.inDB_ = true;
-};
-
-/**
- * Remove a connection from the database. Must already exist in DB.
- * @param {!Blockly.Connection} connection The connection to be removed.
- * @private
- */
-Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) {
- if (!connection.inDB_) {
- throw 'Connection not in database.';
- }
- connection.inDB_ = false;
- // Find the connection using a binary search.
- // About 10% faster than a linear search using indexOf.
- var pointerMin = 0;
- var pointerMax = this.length - 2;
- var pointerMid = pointerMax;
- while (pointerMin < pointerMid) {
- if (this[pointerMid].y_ < connection.y_) {
- pointerMin = pointerMid;
- } else {
- pointerMax = pointerMid;
- }
- pointerMid = Math.floor((pointerMin + pointerMax) / 2);
- }
-
- // Walk forward and back on the y axis looking for the connection.
- // When found, splice it out of the array.
- pointerMin = pointerMid;
- pointerMax = pointerMid;
- while (pointerMin >= 0 && this[pointerMin].y_ == connection.y_) {
- if (this[pointerMin] == connection) {
- this.splice(pointerMin, 1);
- return;
- }
- pointerMin--;
- }
- do {
- if (this[pointerMax] == connection) {
- this.splice(pointerMax, 1);
- return;
- }
- pointerMax++;
- } while (pointerMax < this.length &&
- this[pointerMax].y_ == connection.y_);
- throw 'Unable to find connection in connectionDB.';
-};
-
-/**
- * Initialize a set of connection DBs for a specified workspace.
- * @param {!Blockly.Workspace} workspace The workspace this DB is for.
- */
-Blockly.ConnectionDB.init = function(workspace) {
- // Create four databases, one for each connection type.
- var dbList = [];
- dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB();
- dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB();
- dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB();
- dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB();
- workspace.connectionDBList = dbList;
-};
diff --git a/core/connection_db.js b/core/connection_db.js
new file mode 100644
index 0000000000..9d143e3671
--- /dev/null
+++ b/core/connection_db.js
@@ -0,0 +1,300 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2011 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 Components for managing connections between blocks.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.ConnectionDB');
+
+goog.require('Blockly.Connection');
+
+
+/**
+ * Database of connections.
+ * Connections are stored in order of their vertical component. This way
+ * connections in an area may be looked up quickly using a binary search.
+ * @constructor
+ */
+Blockly.ConnectionDB = function() {
+};
+
+Blockly.ConnectionDB.prototype = new Array();
+/**
+ * Don't inherit the constructor from Array.
+ * @type {!Function}
+ */
+Blockly.ConnectionDB.constructor = Blockly.ConnectionDB;
+
+/**
+ * Add a connection to the database. Must not already exist in DB.
+ * @param {!Blockly.Connection} connection The connection to be added.
+ */
+Blockly.ConnectionDB.prototype.addConnection = function(connection) {
+ if (connection.inDB_) {
+ throw 'Connection already in database.';
+ }
+ if (connection.getSourceBlock().isInFlyout) {
+ // Don't bother maintaining a database of connections in a flyout.
+ return;
+ }
+ var position = this.findPositionForConnection_(connection);
+ this.splice(position, 0, connection);
+ connection.inDB_ = true;
+};
+
+/**
+ * Find the given connection.
+ * Starts by doing a binary search to find the approximate location, then
+ * linearly searches nearby for the exact connection.
+ * @param {!Blockly.Connection} conn The connection to find.
+ * @return {number} The index of the connection, or -1 if the connection was
+ * not found.
+ */
+Blockly.ConnectionDB.prototype.findConnection = function(conn) {
+ if (!this.length) {
+ return -1;
+ }
+
+ var bestGuess = this.findPositionForConnection_(conn);
+ if (bestGuess >= this.length) {
+ // Not in list
+ return -1;
+ }
+
+ var yPos = conn.y_;
+ // Walk forward and back on the y axis looking for the connection.
+ var pointerMin = bestGuess;
+ var pointerMax = bestGuess;
+ while (pointerMin >= 0 && this[pointerMin].y_ == yPos) {
+ if (this[pointerMin] == conn) {
+ return pointerMin;
+ }
+ pointerMin--;
+ }
+
+ while (pointerMax < this.length && this[pointerMax].y_ == yPos) {
+ if (this[pointerMax] == conn) {
+ return pointerMax;
+ }
+ pointerMax++;
+ }
+ return -1;
+};
+
+/**
+ * Finds a candidate position for inserting this connection into the list.
+ * This will be in the correct y order but makes no guarantees about ordering in
+ * the x axis.
+ * @param {!Blockly.Connection} connection The connection to insert.
+ * @return {number} The candidate index.
+ * @private
+ */
+Blockly.ConnectionDB.prototype.findPositionForConnection_ =
+ function(connection) {
+ if (!this.length) {
+ return 0;
+ }
+ var pointerMin = 0;
+ var pointerMax = this.length;
+ while (pointerMin < pointerMax) {
+ var pointerMid = Math.floor((pointerMin + pointerMax) / 2);
+ if (this[pointerMid].y_ < connection.y_) {
+ pointerMin = pointerMid + 1;
+ } else if (this[pointerMid].y_ > connection.y_) {
+ pointerMax = pointerMid;
+ } else {
+ pointerMin = pointerMid;
+ break;
+ }
+ }
+ return pointerMin;
+};
+
+/**
+ * Remove a connection from the database. Must already exist in DB.
+ * @param {!Blockly.Connection} connection The connection to be removed.
+ * @private
+ */
+Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) {
+ if (!connection.inDB_) {
+ throw 'Connection not in database.';
+ }
+ var removalIndex = this.findConnection(connection);
+ if (removalIndex == -1) {
+ throw 'Unable to find connection in connectionDB.';
+ }
+ connection.inDB_ = false;
+ this.splice(removalIndex, 1);
+};
+
+/**
+ * Find all nearby connections to the given connection.
+ * Type checking does not apply, since this function is used for bumping.
+ * @param {!Blockly.Connection} connection The connection whose neighbours
+ * should be returned.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @return {!Array.} List of connections.
+ */
+Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) {
+ var db = this;
+ var currentX = connection.x_;
+ var currentY = connection.y_;
+
+ // Binary search to find the closest y location.
+ var pointerMin = 0;
+ var pointerMax = db.length - 2;
+ var pointerMid = pointerMax;
+ while (pointerMin < pointerMid) {
+ if (db[pointerMid].y_ < currentY) {
+ pointerMin = pointerMid;
+ } else {
+ pointerMax = pointerMid;
+ }
+ pointerMid = Math.floor((pointerMin + pointerMax) / 2);
+ }
+
+ var neighbours = [];
+ /**
+ * Computes if the current connection is within the allowed radius of another
+ * connection.
+ * This function is a closure and has access to outside variables.
+ * @param {number} yIndex The other connection's index in the database.
+ * @return {boolean} True if the current connection's vertical distance from
+ * the other connection is less than the allowed radius.
+ */
+ function checkConnection_(yIndex) {
+ var dx = currentX - db[yIndex].x_;
+ var dy = currentY - db[yIndex].y_;
+ var r = Math.sqrt(dx * dx + dy * dy);
+ if (r <= maxRadius) {
+ neighbours.push(db[yIndex]);
+ }
+ return dy < maxRadius;
+ }
+
+ // Walk forward and back on the y axis looking for the closest x,y point.
+ pointerMin = pointerMid;
+ pointerMax = pointerMid;
+ if (db.length) {
+ while (pointerMin >= 0 && checkConnection_(pointerMin)) {
+ pointerMin--;
+ }
+ do {
+ pointerMax++;
+ } while (pointerMax < db.length && checkConnection_(pointerMax));
+ }
+
+ return neighbours;
+};
+
+
+/**
+ * Is the candidate connection close to the reference connection.
+ * Extremely fast; only looks at Y distance.
+ * @param {number} index Index in database of candidate connection.
+ * @param {number} baseY Reference connection's Y value.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @return {boolean} True if connection is in range.
+ * @private
+ */
+Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) {
+ return (Math.abs(this[index].y_ - baseY) <= maxRadius);
+};
+
+/**
+ * Find the closest compatible connection to this connection.
+ * @param {!Blockly.Connection} conn The connection searching for a compatible
+ * mate.
+ * @param {number} maxRadius The maximum radius to another connection.
+ * @param {!goog.math.Coordinate} dxy Offset between this connection's location
+ * in the database and the current location (as a result of dragging).
+ * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two
+ * properties:' connection' which is either another connection or null,
+ * and 'radius' which is the distance.
+ */
+Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius,
+ dxy) {
+ // Don't bother.
+ if (!this.length) {
+ return {connection: null, radius: maxRadius};
+ }
+
+ // Stash the values of x and y from before the drag.
+ var baseY = conn.y_;
+ var baseX = conn.x_;
+
+ conn.x_ = baseX + dxy.x;
+ conn.y_ = baseY + dxy.y;
+
+ // findPositionForConnection finds an index for insertion, which is always
+ // after any block with the same y index. We want to search both forward
+ // and back, so search on both sides of the index.
+ var closestIndex = this.findPositionForConnection_(conn);
+
+ var bestConnection = null;
+ var bestRadius = maxRadius;
+ var temp;
+
+ // Walk forward and back on the y axis looking for the closest x,y point.
+ var pointerMin = closestIndex - 1;
+ while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y_, maxRadius)) {
+ temp = this[pointerMin];
+ if (conn.isConnectionAllowed(temp, bestRadius)) {
+ bestConnection = temp;
+ bestRadius = temp.distanceFrom(conn);
+ }
+ pointerMin--;
+ }
+
+ var pointerMax = closestIndex;
+ while (pointerMax < this.length && this.isInYRange_(pointerMax, conn.y_,
+ maxRadius)) {
+ temp = this[pointerMax];
+ if (conn.isConnectionAllowed(temp, bestRadius)) {
+ bestConnection = temp;
+ bestRadius = temp.distanceFrom(conn);
+ }
+ pointerMax++;
+ }
+
+ // Reset the values of x and y.
+ conn.x_ = baseX;
+ conn.y_ = baseY;
+
+ // If there were no valid connections, bestConnection will be null.
+ return {connection: bestConnection, radius: bestRadius};
+};
+
+/**
+ * Initialize a set of connection DBs for a specified workspace.
+ * @param {!Blockly.Workspace} workspace The workspace this DB is for.
+ */
+Blockly.ConnectionDB.init = function(workspace) {
+ // Create four databases, one for each connection type.
+ var dbList = [];
+ dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB();
+ dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB();
+ dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB();
+ dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB();
+ workspace.connectionDBList = dbList;
+};
diff --git a/core/constants.js b/core/constants.js
new file mode 100644
index 0000000000..66cd2a8864
--- /dev/null
+++ b/core/constants.js
@@ -0,0 +1,195 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 Blockly constants.
+ * @author fenichel@google.com (Rachel Fenichel)
+ */
+'use strict';
+
+goog.provide('Blockly.constants');
+
+
+/**
+ * Number of pixels the mouse must move before a drag starts.
+ */
+Blockly.DRAG_RADIUS = 5;
+
+/**
+ * Maximum misalignment between connections for them to snap together.
+ */
+Blockly.SNAP_RADIUS = 72;
+
+/**
+ * How much to prefer staying connected to the current connection over moving to
+ * a new connection. The current previewed connection is considered to be this
+ * much closer to the matching connection on the block than it actually is.
+ */
+Blockly.CURRENT_CONNECTION_PREFERENCE = 20;
+
+/**
+ * Delay in ms between trigger and bumping unconnected block out of alignment.
+ */
+Blockly.BUMP_DELAY = 250;
+
+/**
+ * Number of characters to truncate a collapsed block to.
+ */
+Blockly.COLLAPSE_CHARS = 30;
+
+/**
+ * Length in ms for a touch to become a long press.
+ */
+Blockly.LONGPRESS = 750;
+
+/**
+ * The richness of block colours, regardless of the hue.
+ * Must be in the range of 0 (inclusive) to 1 (exclusive).
+ */
+Blockly.HSV_SATURATION = 0.45;
+
+/**
+ * The intensity of block colours, regardless of the hue.
+ * Must be in the range of 0 (inclusive) to 1 (exclusive).
+ */
+Blockly.HSV_VALUE = 0.65;
+
+/**
+ * Sprited icons and images.
+ */
+Blockly.SPRITE = {
+ width: 96,
+ height: 124,
+ url: 'sprites.png'
+};
+
+// Constants below this point are not intended to be changed.
+
+/**
+ * Required name space for SVG elements.
+ * @const
+ */
+Blockly.SVG_NS = 'http://www.w3.org/2000/svg';
+
+/**
+ * Required name space for HTML elements.
+ * @const
+ */
+Blockly.HTML_NS = 'http://www.w3.org/1999/xhtml';
+
+/**
+ * ENUM for a right-facing value input. E.g. 'set item to' or 'return'.
+ * @const
+ */
+Blockly.INPUT_VALUE = 1;
+
+/**
+ * ENUM for a left-facing value output. E.g. 'random fraction'.
+ * @const
+ */
+Blockly.OUTPUT_VALUE = 2;
+
+/**
+ * ENUM for a down-facing block stack. E.g. 'if-do' or 'else'.
+ * @const
+ */
+Blockly.NEXT_STATEMENT = 3;
+
+/**
+ * ENUM for an up-facing block stack. E.g. 'break out of loop'.
+ * @const
+ */
+Blockly.PREVIOUS_STATEMENT = 4;
+
+/**
+ * ENUM for an dummy input. Used to add field(s) with no input.
+ * @const
+ */
+Blockly.DUMMY_INPUT = 5;
+
+/**
+ * ENUM for left alignment.
+ * @const
+ */
+Blockly.ALIGN_LEFT = -1;
+
+/**
+ * ENUM for centre alignment.
+ * @const
+ */
+Blockly.ALIGN_CENTRE = 0;
+
+/**
+ * ENUM for right alignment.
+ * @const
+ */
+Blockly.ALIGN_RIGHT = 1;
+
+/**
+ * ENUM for no drag operation.
+ * @const
+ */
+Blockly.DRAG_NONE = 0;
+
+/**
+ * ENUM for inside the sticky DRAG_RADIUS.
+ * @const
+ */
+Blockly.DRAG_STICKY = 1;
+
+/**
+ * ENUM for freely draggable.
+ * @const
+ */
+Blockly.DRAG_FREE = 2;
+
+/**
+ * Lookup table for determining the opposite type of a connection.
+ * @const
+ */
+Blockly.OPPOSITE_TYPE = [];
+Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE;
+Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE;
+Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT;
+Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT;
+
+/**
+ * ENUM for toolbox and flyout at top of screen.
+ * @const
+ */
+Blockly.TOOLBOX_AT_TOP = 0;
+
+/**
+ * ENUM for toolbox and flyout at bottom of screen.
+ * @const
+ */
+Blockly.TOOLBOX_AT_BOTTOM = 1;
+
+/**
+ * ENUM for toolbox and flyout at left of screen.
+ * @const
+ */
+Blockly.TOOLBOX_AT_LEFT = 2;
+
+/**
+ * ENUM for toolbox and flyout at right of screen.
+ * @const
+ */
+Blockly.TOOLBOX_AT_RIGHT = 3;
diff --git a/core/contextmenu.js b/core/contextmenu.js
index defdd1f318..ebd92df17d 100644
--- a/core/contextmenu.js
+++ b/core/contextmenu.js
@@ -58,7 +58,7 @@ Blockly.ContextMenu.show = function(e, options, rtl) {
*/
var menu = new goog.ui.Menu();
menu.setRightToLeft(rtl);
- for (var x = 0, option; option = options[x]; x++) {
+ for (var i = 0, option; option = options[i]; i++) {
var menuItem = new goog.ui.MenuItem(option.text);
menuItem.setRightToLeft(rtl);
menu.addChild(menuItem, true);
@@ -123,7 +123,8 @@ Blockly.ContextMenu.hide = function() {
*/
Blockly.ContextMenu.callbackFactory = function(block, xml) {
return function() {
- var newBlock = Blockly.Xml.domToBlock(block.workspace, xml);
+ Blockly.Events.disable();
+ var newBlock = Blockly.Xml.domToBlock(xml, block.workspace);
// Move the new block next to the old block.
var xy = block.getRelativeToSurfaceXY();
if (block.RTL) {
@@ -133,6 +134,10 @@ Blockly.ContextMenu.callbackFactory = function(block, xml) {
}
xy.y += Blockly.SNAP_RADIUS * 2;
newBlock.moveBy(xy.x, xy.y);
+ Blockly.Events.enable();
+ if (Blockly.Events.isEnabled() && !newBlock.isShadow()) {
+ Blockly.Events.fire(new Blockly.Events.Create(newBlock));
+ }
newBlock.select();
};
};
diff --git a/core/css.js b/core/css.js
index f1cafb0a9b..c284cd6dfa 100644
--- a/core/css.js
+++ b/core/css.js
@@ -26,6 +26,9 @@
goog.provide('Blockly.Css');
+goog.require('Blockly.Colours');
+
+goog.require('goog.userAgent');
/**
* List of cursors.
@@ -84,6 +87,17 @@ Blockly.Css.inject = function(hasCss, pathToMedia) {
// Strip off any trailing slash (either Unix or Windows).
Blockly.Css.mediaPath_ = pathToMedia.replace(/[\\\/]$/, '');
text = text.replace(/<<>>/g, Blockly.Css.mediaPath_);
+ // Dynamically replace colours in the CSS text, in case they have
+ // been set at run-time injection.
+ for (var colourProperty in Blockly.Colours) {
+ if (Blockly.Colours.hasOwnProperty(colourProperty)) {
+ // Replace all
+ text = text.replace(
+ new RegExp('\\$colour\\_' + colourProperty, 'g'),
+ Blockly.Colours[colourProperty]
+ );
+ }
+ }
// Inject CSS tag.
var cssNode = document.createElement('style');
document.head.appendChild(cssNode);
@@ -98,6 +112,11 @@ Blockly.Css.inject = function(hasCss, pathToMedia) {
* @param {Blockly.Css.Cursor} cursor Enum.
*/
Blockly.Css.setCursor = function(cursor) {
+ if (goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD) {
+ // Don't try to switch the mouse cursor on a mobile device.
+ // This is an optimization - since we almost never have cursors on mobile anyway.
+ return;
+ }
if (Blockly.Css.currentCursor_ == cursor) {
return;
}
@@ -132,17 +151,41 @@ Blockly.Css.setCursor = function(cursor) {
*/
Blockly.Css.CONTENT = [
'.blocklySvg {',
- 'background-color: #fff;',
+ 'background-color: $colour_workspace;',
'outline: none;',
'overflow: hidden;', /* IE overflows by default. */
'}',
+ /* Necessary to position the drag surface */
+ '.blocklyRelativeWrapper {',
+ 'position: relative;',
+ 'width: 100%;',
+ 'height: 100%;',
+ '}',
+
'.blocklyWidgetDiv {',
'display: none;',
'position: absolute;',
'z-index: 999;',
'}',
+ '.blocklyWidgetDiv.fieldTextInput {',
+ 'overflow: hidden;',
+ 'border: 1px solid;',
+ 'box-sizing: border-box;',
+ 'transform-origin: 0 0;',
+ '-ms-transform-origin: 0 0;',
+ '-moz-transform-origin: 0 0;',
+ '-webkit-transform-origin: 0 0;',
+ '}',
+
+ '.blocklyNonSelectable {',
+ 'user-select: none;',
+ '-moz-user-select: none;',
+ '-webkit-user-select: none;',
+ '-ms-user-select: none;',
+ '}',
+
'.blocklyTooltipDiv {',
'background-color: #ffffc7;',
'border: 1px solid #ddc;',
@@ -157,6 +200,118 @@ Blockly.Css.CONTENT = [
'z-index: 1000;',
'}',
+ '.blocklyDragSurface {',
+ 'display: none;',
+ 'position: absolute;',
+ 'top: 0;',
+ 'left: 0;',
+ 'right: 0;',
+ 'bottom: 0;',
+ 'overflow: visible !important;',
+ 'z-index: 5000;', /* Always display on top */
+ '-webkit-backface-visibility: hidden;',
+ 'backface-visibility: hidden;',
+ '-webkit-perspective: 1000;',
+ 'perspective: 1000;',
+ '}',
+
+ '.blocklyDropDownDiv {',
+ 'position: absolute;',
+ 'left: 0;',
+ 'top: 0;',
+ 'z-index: 1000;',
+ 'display: none;',
+ 'border: 1px solid;',
+ 'border-radius: 4px;',
+ 'box-shadow: 0px 0px 8px 1px ' + Blockly.Colours.dropDownShadow + ';',
+ 'padding: 4px;',
+ '-webkit-user-select: none;',
+ '}',
+
+ '.blocklyDropDownArrow {',
+ 'position: absolute;',
+ 'left: 0;',
+ 'top: 0;',
+ 'width: 16px;',
+ 'height: 16px;',
+ 'z-index: -1;',
+ '}',
+
+ '.blocklyDropDownButton {',
+ 'display: inline-block;',
+ 'float: left;',
+ 'padding: 0;',
+ 'margin: 4px;',
+ 'border-radius: 4px;',
+ 'outline: none;',
+ 'border: 1px solid;',
+ 'transition: box-shadow .1s;',
+ 'cursor: pointer;',
+ '}',
+
+ '.blocklyDropDownButtonHover {',
+ 'box-shadow: 0px 0px 0px 4px ' + Blockly.Colours.fieldShadow + ';',
+ '}',
+
+ '.blocklyDropDownButton:active {',
+ 'box-shadow: 0px 0px 0px 6px ' + Blockly.Colours.fieldShadow + ';',
+ '}',
+
+ '.blocklyDropDownButton > img {',
+ 'width: 80%;',
+ 'height: 80%;',
+ 'margin-top: 5%',
+ '}',
+
+ '.blocklyDropDownPlaceholder {',
+ 'display: inline-block;',
+ 'float: left;',
+ 'padding: 0;',
+ 'margin: 4px;',
+ '}',
+
+ '.blocklyNumPadButton {',
+ 'display: inline-block;',
+ 'float: left;',
+ 'padding: 0;',
+ 'width: 48px;',
+ 'height: 48px;',
+ 'margin: 4px;',
+ 'border-radius: 4px;',
+ 'background: $colour_numPadBackground;',
+ 'color: $colour_numPadText;',
+ 'outline: none;',
+ 'border: 1px solid $colour_numPadBorder;',
+ 'cursor: pointer;',
+ 'font-weight: 600;',
+ 'font-family: sans-serif;',
+ 'font-size: 12pt;',
+ '-webkit-tap-highlight-color: rgba(0,0,0,0);',
+ '}',
+
+ '.blocklyNumPadButton > img {',
+ 'margin-top: 10%;',
+ 'width: 80%;',
+ 'height: 80%;',
+ '}',
+
+ '.blocklyNumPadButton:active {',
+ 'background: $colour_numPadActiveBackground;',
+ '-webkit-tap-highlight-color: rgba(0,0,0,0);',
+ '}',
+
+ '.arrowTop {',
+ 'border-top: 1px solid;',
+ 'border-left: 1px solid;',
+ 'border-top-left-radius: 4px;',
+ '}',
+
+ '.arrowBottom {',
+ 'border-bottom: 1px solid;',
+ 'border-right: 1px solid;',
+ 'border-bottom-right-radius: 4px;',
+ '}',
+
'.blocklyResizeSE {',
'cursor: se-resize;',
'fill: #aaa;',
@@ -178,29 +333,16 @@ Blockly.Css.CONTENT = [
'stroke-width: 4px;',
'}',
- '.blocklyPathLight {',
- 'fill: none;',
- 'stroke-linecap: round;',
- 'stroke-width: 1;',
+ '.blocklyPath {',
+ 'stroke-width: 1px;',
'}',
'.blocklySelected>.blocklyPath {',
- 'stroke: #fc3;',
- 'stroke-width: 3px;',
- '}',
-
- '.blocklySelected>.blocklyPathLight {',
- 'display: none;',
- '}',
-
- '.blocklyDragging>.blocklyPath,',
- '.blocklyDragging>.blocklyPathLight {',
- 'fill-opacity: .8;',
- 'stroke-opacity: .8;',
+ // 'stroke: #fc3;',
+ // 'stroke-width: 3px;',
'}',
- '.blocklyDragging>.blocklyPathDark {',
- 'display: none;',
+ '.blocklyDragging>.blocklyPath {',
'}',
'.blocklyDisabled>.blocklyPath {',
@@ -208,15 +350,15 @@ Blockly.Css.CONTENT = [
'stroke-opacity: .5;',
'}',
- '.blocklyDisabled>.blocklyPathLight,',
- '.blocklyDisabled>.blocklyPathDark {',
- 'display: none;',
- '}',
-
'.blocklyText {',
'cursor: default;',
'fill: #fff;',
'font-family: sans-serif;',
+ 'font-size: 12pt;',
+ 'font-weight: 600;',
+ '}',
+
+ '.blocklyTextTruncated {',
'font-size: 11pt;',
'}',
@@ -232,7 +374,7 @@ Blockly.Css.CONTENT = [
'.blocklyNonEditableText>text,',
'.blocklyEditableText>text {',
- 'fill: #000;',
+ 'fill: $colour_text;',
'}',
'.blocklyEditableText:hover>rect {',
@@ -241,7 +383,7 @@ Blockly.Css.CONTENT = [
'}',
'.blocklyBubbleText {',
- 'fill: #000;',
+ 'fill: $colour_text;',
'}',
/*
@@ -297,13 +439,17 @@ Blockly.Css.CONTENT = [
'.blocklyHtmlInput {',
'border: none;',
- 'border-radius: 4px;',
'font-family: sans-serif;',
+ 'font-size: 12pt;',
'height: 100%;',
'margin: 0;',
'outline: none;',
- 'padding: 0 1px;',
- 'width: 100%',
+ 'box-sizing: border-box;',
+ 'padding: 2px 8px 0 8px;',
+ 'width: 100%;',
+ 'text-align: center;',
+ 'color: $colour_text;',
+ 'font-weight: 600;',
'}',
'.blocklyMainBackground {',
@@ -318,7 +464,7 @@ Blockly.Css.CONTENT = [
'}',
'.blocklyFlyoutBackground {',
- 'fill: #ddd;',
+ 'fill: $colour_flyout;',
'fill-opacity: .8;',
'}',
@@ -327,12 +473,12 @@ Blockly.Css.CONTENT = [
'}',
'.blocklyScrollbarKnob {',
- 'fill: #ccc;',
+ 'fill: $colour_scrollbar;',
'}',
'.blocklyScrollbarBackground:hover+.blocklyScrollbarKnob,',
'.blocklyScrollbarKnob:hover {',
- 'fill: #bbb;',
+ 'fill: $colour_scrollbarHover;',
'}',
'.blocklyZoom>image {',
@@ -393,6 +539,10 @@ Blockly.Css.CONTENT = [
'padding: 0 !important;',
'}',
+ '.blocklyDropDownNumPad {',
+ 'background-color: $colour_numPadBackground;',
+ '}',
+
/* Override the default Closure URL. */
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {',
@@ -401,7 +551,8 @@ Blockly.Css.CONTENT = [
/* Category tree in Toolbox. */
'.blocklyToolboxDiv {',
- 'background-color: #ddd;',
+ 'background-color: $colour_toolbox;',
+ 'color: $colour_toolboxText;',
'overflow-x: visible;',
'overflow-y: auto;',
'position: absolute;',
@@ -423,6 +574,16 @@ Blockly.Css.CONTENT = [
'white-space: nowrap;',
'}',
+ '.blocklyHorizontalTree {',
+ 'float: left;',
+ 'margin: 1px 5px 8px 0px;',
+ '}',
+
+ '.blocklyHorizontalTreeRtl {',
+ 'float: right;',
+ 'margin: 1px 0px 8px 5px;',
+ '}',
+
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',
'margin-left: 8px;',
'}',
@@ -437,6 +598,13 @@ Blockly.Css.CONTENT = [
'margin: 5px 0;',
'}',
+ '.blocklyTreeSeparatorHorizontal {',
+ 'border-right: solid #e5e5e5 1px;',
+ 'width: 0px;',
+ 'padding: 5px 0;',
+ 'margin: 0 5px;',
+ '}',
+
'.blocklyTreeIcon {',
'background-image: url(<<>>/sprites.png);',
'height: 16px;',
diff --git a/core/dragsurface_svg.js b/core/dragsurface_svg.js
new file mode 100644
index 0000000000..ba979bce35
--- /dev/null
+++ b/core/dragsurface_svg.js
@@ -0,0 +1,219 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Massachusetts Institute of Technology
+ * All rights reserved.
+ *
+ * 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 An SVG that floats on top of the workspace.
+ * Blocks are moved into this SVG during a drag, improving performance.
+ * The entire SVG is translated, so the blocks are never repainted during drag.
+ * @author tmickel@mit.edu (Tim Mickel)
+ */
+
+'use strict';
+
+goog.provide('Blockly.DragSurfaceSvg');
+
+goog.require('Blockly.utils');
+goog.require('Blockly.constants');
+goog.require('Blockly.Colours');
+
+goog.require('goog.asserts');
+goog.require('goog.math.Coordinate');
+
+/**
+ * Class for a Drag Surface SVG.
+ * @param {Element} container Containing element.
+ * @constructor
+ */
+Blockly.DragSurfaceSvg = function(container) {
+ this.container_ = container;
+};
+
+/**
+ * The SVG drag surface. Set once by Blockly.DragSurfaceSvg.createDom.
+ * @type {Element}
+ * @private
+ */
+Blockly.DragSurfaceSvg.prototype.SVG_ = null;
+
+/**
+ * SVG group inside the drag surface. This is where blocks are moved to.
+ * @type {Element}
+ * @private
+ */
+Blockly.DragSurfaceSvg.prototype.dragGroup_ = null;
+
+/**
+ * Containing HTML element; parent of the workspace and the drag surface.
+ * @type {Element}
+ * @private
+ */
+Blockly.DragSurfaceSvg.prototype.container_ = null;
+
+/**
+ * Cached value for the scale of the drag surface.
+ * Used to set/get the correct translation during and after a drag.
+ * @type {Number}
+ * @private
+ */
+Blockly.DragSurfaceSvg.prototype.scale_ = 1;
+
+/**
+ * ID for the drag shadow filter, set in createDom.
+ * @type {string}
+ * @private
+ */
+Blockly.DragSurfaceSvg.prototype.dragShadowFilterId_ = '';
+
+/**
+ * Standard deviation for gaussian blur on drag shadow, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.DragSurfaceSvg.SHADOW_STD_DEVIATION = 6;
+
+/**
+ * Create the drag surface and inject it into the container.
+ */
+Blockly.DragSurfaceSvg.prototype.createDom = function() {
+ if (this.SVG_) {
+ return; // Already created.
+ }
+ this.SVG_ = Blockly.createSvgElement('svg', {
+ 'xmlns': Blockly.SVG_NS,
+ 'xmlns:html': Blockly.HTML_NS,
+ 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
+ 'version': '1.1',
+ 'class': 'blocklyDragSurface'
+ }, this.container_);
+ var defs = Blockly.createSvgElement('defs', {}, this.SVG_);
+ this.dragShadowFilterId_ = this.createDropShadowDom_(defs);
+ this.dragGroup_ = Blockly.createSvgElement('g', {}, this.SVG_);
+ this.dragGroup_.setAttribute('filter', 'url(#' + this.dragShadowFilterId_ + ')');
+};
+
+/**
+ * Create the SVG def for the drop shadow.
+ * @param {Element} defs Defs element to insert the shadow filter definition
+ * @return {string} ID for the filter element
+ */
+Blockly.DragSurfaceSvg.prototype.createDropShadowDom_ = function(defs) {
+ // Adjust these width/height, x/y properties to prevent the shadow from clipping
+ var dragShadowFilter = Blockly.createSvgElement('filter',
+ {'id': 'blocklyDragShadowFilter', 'height': '140%', 'width': '140%', y: '-20%', x: '-20%'}, defs);
+ Blockly.createSvgElement('feGaussianBlur',
+ {'in': 'SourceAlpha', 'stdDeviation': Blockly.DragSurfaceSvg.SHADOW_STD_DEVIATION}, dragShadowFilter);
+ var componentTransfer = Blockly.createSvgElement('feComponentTransfer', {'result': 'offsetBlur'}, dragShadowFilter);
+ // Shadow opacity is specified in the adjustable colour library,
+ // since the darkness of the shadow largely depends on the workspace colour.
+ Blockly.createSvgElement('feFuncA',
+ {'type': 'linear', 'slope': Blockly.Colours.dragShadowOpacity}, componentTransfer);
+ Blockly.createSvgElement('feComposite',
+ {'in': 'SourceGraphic', 'in2': 'offsetBlur', 'operator': 'over'}, dragShadowFilter);
+ return dragShadowFilter.id;
+};
+
+/**
+ * Set the SVG blocks on the drag surface's group and show the surface.
+ * Only one block should be on the drag surface at a time.
+ * @param {!Element} blocks Block or group of blocks to place on the drag surface
+ */
+Blockly.DragSurfaceSvg.prototype.setBlocksAndShow = function(blocks) {
+ goog.asserts.assert(this.dragGroup_.childNodes.length == 0, 'Already dragging a block.');
+ // appendChild removes the blocks from the previous parent
+ this.dragGroup_.appendChild(blocks);
+ this.SVG_.style.display = 'block';
+};
+
+/**
+ * Translate and scale the entire drag surface group to keep in sync with the workspace.
+ * @param {Number} x X translation
+ * @param {Number} y Y translation
+ * @param {Number} scale Scale of the group
+ */
+Blockly.DragSurfaceSvg.prototype.translateAndScaleGroup = function(x, y, scale) {
+ var transform;
+ this.scale_ = scale;
+ if (Blockly.is3dSupported()) {
+ transform = 'transform: translate3d(' + x + 'px, ' + y + 'px, 0px)' +
+ 'scale3d(' + scale + ',' + scale + ',' + scale + ')';
+ this.dragGroup_.setAttribute('style', transform);
+ } else {
+ transform = 'translate(' + x + ', ' + y + ') scale(' + scale + ')';
+ this.dragGroup_.setAttribute('transform', transform);
+ }
+};
+
+/**
+ * Translate the entire drag surface during a drag.
+ * We translate the drag surface instead of the blocks inside the surface
+ * so that the browser avoids repainting the SVG.
+ * Because of this, the drag coordinates must be adjusted by scale.
+ * @param {Number} x X translation for the entire surface
+ * @param {Number} y Y translation for the entire surface
+ */
+Blockly.DragSurfaceSvg.prototype.translateSurface = function(x, y) {
+ var transform;
+ x *= this.scale_;
+ y *= this.scale_;
+ if (Blockly.is3dSupported()) {
+ transform = 'transform: translate3d(' + x + 'px, ' + y + 'px, 0px); display: block;';
+ this.SVG_.setAttribute('style', transform);
+ } else {
+ transform = 'translate(' + x + ', ' + y + ')';
+ this.SVG_.setAttribute('transform', transform);
+ }
+};
+
+/**
+ * Reports the surface translation in scaled workspace coordinates.
+ * Use this when finishing a drag to return blocks to the correct position.
+ * @return {goog.math.Coordinate} Current translation of the surface
+ */
+Blockly.DragSurfaceSvg.prototype.getSurfaceTranslation = function() {
+ var xy = Blockly.getRelativeXY_(this.SVG_);
+ return new goog.math.Coordinate(xy.x / this.scale_, xy.y / this.scale_);
+};
+
+/**
+ * Provide a reference to the drag group (primarily for BlockSvg.getRelativeToSurfaceXY).
+ * @return {Element} Drag surface group element
+ */
+Blockly.DragSurfaceSvg.prototype.getGroup = function() {
+ return this.dragGroup_;
+};
+
+/**
+ * Get the current blocks on the drag surface, if any (primarily for BlockSvg.getRelativeToSurfaceXY).
+ * @return {Element} Drag surface block DOM element
+ */
+Blockly.DragSurfaceSvg.prototype.getCurrentBlock = function() {
+ return this.dragGroup_.childNodes[0];
+};
+
+/**
+ * Clear the group and hide the surface; move the blocks off onto the provided element.
+ * @param {!Element} newSurface Surface the dragging blocks should be moved to
+ */
+Blockly.DragSurfaceSvg.prototype.clearAndHide = function(newSurface) {
+ // appendChild removes the node from this.dragGroup_
+ newSurface.appendChild(this.getCurrentBlock());
+ this.SVG_.style.display = 'none';
+ goog.asserts.assert(this.dragGroup_.childNodes.length == 0, 'Drag group was not cleared.');
+};
diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js
new file mode 100644
index 0000000000..4ce350380d
--- /dev/null
+++ b/core/dropdowndiv.js
@@ -0,0 +1,353 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Massachusetts Institute of Technology
+ * All rights reserved.
+ *
+ * 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 A div that floats on top of the workspace, for drop-down menus.
+ * The drop-down can be kept inside the workspace, animate in/out, etc.
+ * @author tmickel@mit.edu (Tim Mickel)
+ */
+
+'use strict';
+
+goog.provide('Blockly.DropDownDiv');
+
+goog.require('goog.dom');
+goog.require('goog.style');
+
+/**
+ * Class for drop-down div.
+ * @constructor
+ */
+Blockly.DropDownDiv = function() {
+};
+
+/**
+ * The div element. Set once by Blockly.DropDownDiv.createDom.
+ * @type {Element}
+ * @private
+ */
+Blockly.DropDownDiv.DIV_ = null;
+
+/**
+ * Drop-downs will appear within the bounds of this element if possible.
+ * Set in Blockly.DropDownDiv.setBoundsElement.
+ * @type {Element}
+ * @private
+ */
+Blockly.DropDownDiv.boundsElement_ = null;
+
+/**
+ * The object currently using the drop-down.
+ * @type {Object}
+ * @private
+ */
+Blockly.DropDownDiv.owner_ = null;
+
+/**
+ * Arrow size in px. Should match the value in CSS (need to position pre-render).
+ * @type {number}
+ * @const
+ */
+Blockly.DropDownDiv.ARROW_SIZE = 16;
+
+/**
+ * Drop-down border size in px. Should match the value in CSS (need to position the arrow).
+ * @type {number}
+ * @const
+ */
+Blockly.DropDownDiv.BORDER_SIZE = 1;
+
+/**
+ * Amount the arrow must be kept away from the edges of the main drop-down div, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING = 12;
+
+/**
+ * Amount drop-downs should be padded away from the source, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.DropDownDiv.PADDING_Y = 20;
+
+/**
+ * Length of animations in seconds.
+ * @type {number}
+ * @const
+ */
+Blockly.DropDownDiv.ANIMATION_TIME = 0.25;
+
+/**
+ * Timer for animation out, to be cleared if we need to immediately hide
+ * without disrupting new shows.
+ * @type {number}
+ */
+Blockly.DropDownDiv.animateOutTimer_ = null;
+
+/**
+ * When the drop-down is opened, we save the position it animated from
+ * so that it can animate back to that position on close.
+ * Absolute X position of that position, in px.
+ * @type {number}
+ */
+Blockly.DropDownDiv.hideAnimationX_ = 0;
+
+/**
+ * When the drop-down is opened, we save the position it animated from
+ * so that it can animate back to that position on close.
+ * Absolute Y position of that position, in px.
+ * @type {number}
+ */
+Blockly.DropDownDiv.hideAnimationY_ = 0;
+
+/**
+ * Callback for when the drop-down is hidden.
+ * @type {Function}
+ */
+Blockly.DropDownDiv.onHide_ = 0;
+
+/**
+ * Create and insert the DOM element for this div.
+ * @param {Element} container Element that the div should be contained in.
+ */
+Blockly.DropDownDiv.createDom = function() {
+ if (Blockly.DropDownDiv.DIV_) {
+ return; // Already created.
+ }
+ Blockly.DropDownDiv.DIV_ = goog.dom.createDom('div', 'blocklyDropDownDiv');
+ document.body.appendChild(Blockly.DropDownDiv.DIV_);
+ Blockly.DropDownDiv.content_ = goog.dom.createDom('div', 'blocklyDropDownContent');
+ Blockly.DropDownDiv.DIV_.appendChild(Blockly.DropDownDiv.content_);
+ Blockly.DropDownDiv.arrow_ = goog.dom.createDom('div', 'blocklyDropDownArrow');
+ Blockly.DropDownDiv.DIV_.appendChild(Blockly.DropDownDiv.arrow_);
+};
+
+/**
+ * Set an element to maintain bounds within. Drop-downs will appear
+ * within the box of this element if possible.
+ * @param {Element} boundsElement Element to bound drop-down to.
+ */
+Blockly.DropDownDiv.setBoundsElement = function(boundsElement) {
+ Blockly.DropDownDiv.boundsElement_ = boundsElement;
+};
+
+/**
+ * Provide the div for inserting content into the drop-down.
+ * @return {Element} Div to populate with content
+ */
+Blockly.DropDownDiv.getContentDiv = function() {
+ return Blockly.DropDownDiv.content_;
+};
+
+/**
+ * Clear the content of the drop-down.
+ */
+Blockly.DropDownDiv.clearContent = function() {
+ Blockly.DropDownDiv.content_.innerHTML = '';
+};
+
+/**
+ * Set the colour for the drop-down.
+ * @param {string} backgroundColour Any CSS color for the background
+ * @param {string} borderColour Any CSS color for the border
+ */
+Blockly.DropDownDiv.setColour = function(backgroundColour, borderColour) {
+ Blockly.DropDownDiv.DIV_.style.backgroundColor = backgroundColour;
+ Blockly.DropDownDiv.arrow_.style.backgroundColor = backgroundColour;
+ Blockly.DropDownDiv.DIV_.style.borderColor = borderColour;
+ Blockly.DropDownDiv.arrow_.style.borderColor = borderColour;
+};
+
+/**
+ * Show and place the drop-down.
+ * The drop-down is placed with an absolute "origin point" (x, y) - i.e.,
+ * the arrow will point at this origin and box will positioned below or above it.
+ * If we can maintain the container bounds at the primary point, the arrow will
+ * point there, and the container will be positioned below it.
+ * If we can't maintain the container bounds at the primary point, fall-back to the
+ * secondary point and position above.
+ * @param {Object} owner The object showing the drop-down
+ * @param {number} primaryX Desired origin point x, in absolute px
+ * @param {number} primaryY Desired origin point y, in absolute px
+ * @param {number} secondaryX Secondary/alternative origin point x, in absolute px
+ * @param {number} secondaryY Secondary/alternative origin point y, in absolute px
+ * @param {Function=} opt_onHide Optional callback for when the drop-down is hidden
+ * @return {boolean} True if the menu rendered at the primary origin point.
+ */
+Blockly.DropDownDiv.show = function(owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) {
+ Blockly.DropDownDiv.owner_ = owner;
+ Blockly.DropDownDiv.onHide_ = opt_onHide;
+ var div = Blockly.DropDownDiv.DIV_;
+ var metrics = Blockly.DropDownDiv.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);
+ // Update arrow CSS
+ Blockly.DropDownDiv.arrow_.style.transform = 'translate(' +
+ metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)';
+ Blockly.DropDownDiv.arrow_.setAttribute('class',
+ metrics.arrowAtTop ? 'blocklyDropDownArrow arrowTop' : 'blocklyDropDownArrow arrowBottom');
+ // First apply initial translation
+ div.style.transform = 'translate(' + metrics.finalX + 'px,' + metrics.finalY + 'px)';
+ // Save for animate out
+ Blockly.DropDownDiv.hideAnimationX_ = metrics.initialX;
+ Blockly.DropDownDiv.hideAnimationY_ = metrics.initialY;
+ // Show the div
+ div.style.display = 'block';
+ div.style.opacity = 1;
+ return metrics.arrowAtTop;
+};
+
+/**
+ * Helper to position the drop-down and the arrow, maintaining bounds.
+ * See explanation of origin points in Blockly.DropDownDiv.show.
+ * @param {number} primaryX Desired origin point x, in absolute px
+ * @param {number} primaryY Desired origin point y, in absolute px
+ * @param {number} secondaryX Secondary/alternative origin point x, in absolute px
+ * @param {number} secondaryY Secondary/alternative origin point y, in absolute px
+ * @returns {Object} Various final metrics, including rendered positions for drop-down and arrow.
+ */
+Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX, secondaryY) {
+ var div = Blockly.DropDownDiv.DIV_;
+ var boundPosition = goog.style.getPageOffset(Blockly.DropDownDiv.boundsElement_);
+ var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_);
+ var divSize = goog.style.getSize(div);
+
+ // First decide if we will render at primary or secondary position
+ // i.e., above or below
+ // renderX, renderY will eventually be the final rendered position of the box.
+ var renderX, renderY, renderedSecondary;
+ // Can the div fit inside the bounds if we render below the primary point?
+ if (primaryY + divSize.height > boundPosition.y + boundSize.height) {
+ // We can't fit below in terms of y. Can we fit above?
+ if (secondaryY - divSize.height < boundPosition.y) {
+ // We also can't fit above, so just render below anyway.
+ renderX = primaryX;
+ renderY = primaryY + Blockly.DropDownDiv.PADDING_Y;
+ renderedSecondary = false;
+ } else {
+ // We can fit above, render secondary
+ renderX = secondaryX;
+ renderY = secondaryY - divSize.height - Blockly.DropDownDiv.PADDING_Y;
+ renderedSecondary = true;
+ }
+ } else {
+ // We can fit below, render primary
+ renderX = primaryX;
+ renderY = primaryY + Blockly.DropDownDiv.PADDING_Y;
+ renderedSecondary = false;
+ }
+ // First calculate the absolute arrow X
+ // This needs to be done before positioning the div, since the arrow
+ // wants to be as close to the origin point as possible.
+ var arrowX = renderX - Blockly.DropDownDiv.ARROW_SIZE / 2;
+ // Keep in overall bounds
+ arrowX = Math.max(boundPosition.x, Math.min(arrowX, boundPosition.x + boundSize.width));
+
+ // Adjust the x-position of the drop-down so that the div is centered and within bounds.
+ var centerX = divSize.width / 2;
+ renderX -= centerX;
+ // Fit horizontally in the bounds.
+ renderX = Math.max(
+ boundPosition.x,
+ Math.min(renderX, boundPosition.x + boundSize.width - divSize.width)
+ );
+ // After we've finished caclulating renderX, adjust the arrow to be relative to it.
+ arrowX -= renderX;
+
+ // Pad the arrow by some pixels, primarily so that it doesn't render on top of a rounded border.
+ arrowX = Math.max(
+ Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING,
+ Math.min(arrowX, divSize.width - Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING - Blockly.DropDownDiv.ARROW_SIZE)
+ );
+
+ // Calculate arrow Y. If we rendered secondary, add on bottom.
+ // Extra pixels are added so that it covers the border of the div.
+ var arrowY = (renderedSecondary) ? divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0;
+ arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) + Blockly.DropDownDiv.BORDER_SIZE;
+
+ // Initial position calculated without any padding to provide an animation point.
+ var initialX = renderX; // X position remains constant during animation.
+ var initialY;
+ if (renderedSecondary) {
+ initialY = secondaryY - divSize.height; // No padding on Y
+ } else {
+ initialY = primaryY; // No padding on Y
+ }
+
+ return {
+ initialX: initialX,
+ initialY : initialY,
+ finalX: renderX,
+ finalY: renderY,
+ arrowX: arrowX,
+ arrowY: arrowY,
+ arrowAtTop: !renderedSecondary
+ };
+};
+
+/**
+ * Hide the menu only if it is owned by the provided object.
+ * @param {Object} owner Object which must be owning the drop-down to hide
+ * @return {Boolean} True if hidden
+ */
+Blockly.DropDownDiv.hideIfOwner = function(owner) {
+ if (Blockly.DropDownDiv.owner_ === owner) {
+ Blockly.DropDownDiv.hide();
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Hide the menu, triggering animation.
+ */
+Blockly.DropDownDiv.hide = function() {
+ // Start the animation by setting the translation and fading out.
+ var div = Blockly.DropDownDiv.DIV_;
+ div.style.transition = 'transform ' + Blockly.DropDownDiv.ANIMATION_TIME + 's, ' +
+ 'opacity ' + Blockly.DropDownDiv.ANIMATION_TIME + 's';
+ div.style.transform = 'translate(' + Blockly.DropDownDiv.hideAnimationX_ +
+ 'px,' + Blockly.DropDownDiv.hideAnimationY_ + 'px)';
+ div.style.opacity = 0;
+ Blockly.DropDownDiv.animateOutTimer_ = setTimeout(function() {
+ // Finish animation - reset all values to default.
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ }, Blockly.DropDownDiv.ANIMATION_TIME * 1000);
+ if (Blockly.DropDownDiv.onHide_) {
+ Blockly.DropDownDiv.onHide_();
+ Blockly.DropDownDiv.onHide_ = null;
+ }
+};
+
+/**
+ * Hide the menu, without animation.
+ */
+Blockly.DropDownDiv.hideWithoutAnimation = function() {
+ var div = Blockly.DropDownDiv.DIV_;
+ Blockly.DropDownDiv.animateOutTimer_ && window.clearTimeout(Blockly.DropDownDiv.animateOutTimer_);
+ div.style.transition = '';
+ div.style.transform = '';
+ div.style.display = 'none';
+ Blockly.DropDownDiv.clearContent();
+ Blockly.DropDownDiv.owner_ = null;
+ if (Blockly.DropDownDiv.onHide_) {
+ Blockly.DropDownDiv.onHide_();
+ Blockly.DropDownDiv.onHide_ = null;
+ }
+};
diff --git a/core/events.js b/core/events.js
new file mode 100644
index 0000000000..bc4299e550
--- /dev/null
+++ b/core/events.js
@@ -0,0 +1,782 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 Events fired as a result of actions in Blockly's editor.
+ * @author fraser@google.com (Neil Fraser)
+ */
+'use strict';
+
+goog.provide('Blockly.Events');
+
+goog.require('goog.math.Coordinate');
+
+
+/**
+ * Group ID for new events. Grouped events are indivisible.
+ * @type {string}
+ * @private
+ */
+Blockly.Events.group_ = '';
+
+/**
+ * Sets whether events should be added to the undo stack.
+ * @type {boolean}
+ */
+Blockly.Events.recordUndo = true;
+
+/**
+ * Allow change events to be created and fired.
+ * @type {number}
+ * @private
+ */
+Blockly.Events.disabled_ = 0;
+
+/**
+ * Name of event that creates a block.
+ * @const
+ */
+Blockly.Events.CREATE = 'create';
+
+/**
+ * Name of event that deletes a block.
+ * @const
+ */
+Blockly.Events.DELETE = 'delete';
+
+/**
+ * Name of event that changes a block.
+ * @const
+ */
+Blockly.Events.CHANGE = 'change';
+
+/**
+ * Name of event that moves a block.
+ * @const
+ */
+Blockly.Events.MOVE = 'move';
+
+/**
+ * Name of event that records a UI change.
+ * @const
+ */
+Blockly.Events.UI = 'ui';
+
+/**
+ * List of events queued for firing.
+ * @private
+ */
+Blockly.Events.FIRE_QUEUE_ = [];
+
+/**
+ * Create a custom event and fire it.
+ * @param {!Blockly.Events.Abstract} event Custom data for event.
+ */
+Blockly.Events.fire = function(event) {
+ if (!Blockly.Events.isEnabled()) {
+ return;
+ }
+ if (!Blockly.Events.FIRE_QUEUE_.length) {
+ // First event added; schedule a firing of the event queue.
+ setTimeout(Blockly.Events.fireNow_, 0);
+ }
+ Blockly.Events.FIRE_QUEUE_.push(event);
+};
+
+/**
+ * Fire all queued events.
+ * @private
+ */
+Blockly.Events.fireNow_ = function() {
+ var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true);
+ Blockly.Events.FIRE_QUEUE_.length = 0;
+ for (var i = 0, event; event = queue[i]; i++) {
+ var workspace = Blockly.Workspace.getById(event.workspaceId);
+ if (workspace) {
+ workspace.fireChangeListener(event);
+ }
+ }
+};
+
+/**
+ * Filter the queued events and merge duplicates.
+ * @param {!Array.} queueIn Array of events.
+ * @param {boolean} forward True if forward (redo), false if backward (undo).
+ * @return {!Array.} Array of filtered events.
+ */
+Blockly.Events.filter = function(queueIn, forward) {
+ var queue = goog.array.clone(queueIn);
+ if (!forward) {
+ // Undo is merged in reverse order.
+ queue.reverse();
+ }
+ // Merge duplicates. O(n^2), but n should be very small.
+ for (var i = 0, event1; event1 = queue[i]; i++) {
+ for (var j = i + 1, event2; event2 = queue[j]; j++) {
+ if (event1.type == event2.type &&
+ event1.blockId == event2.blockId &&
+ event1.workspaceId == event2.workspaceId) {
+ if (event1.type == Blockly.Events.MOVE) {
+ // Merge move events.
+ event1.newParentId = event2.newParentId;
+ event1.newInputName = event2.newInputName;
+ event1.newCoordinate = event2.newCoordinate;
+ queue.splice(j, 1);
+ j--;
+ } else if (event1.type == Blockly.Events.CHANGE &&
+ event1.element == event2.element &&
+ event1.name == event2.name) {
+ // Merge change events.
+ event1.newValue = event2.newValue;
+ queue.splice(j, 1);
+ j--;
+ } else if (event1.type == Blockly.Events.UI &&
+ event2.element == 'click' &&
+ (event1.element == 'commentOpen' ||
+ event1.element == 'mutatorOpen' ||
+ event1.element == 'warningOpen')) {
+ // Merge change events.
+ event1.newValue = event2.newValue;
+ queue.splice(j, 1);
+ j--;
+ }
+ }
+ }
+ }
+ // Remove null events.
+ for (var i = queue.length - 1; i >= 0; i--) {
+ if (queue[i].isNull()) {
+ queue.splice(i, 1);
+ }
+ }
+ if (!forward) {
+ // Restore undo order.
+ queue.reverse();
+ }
+ // Move mutation events to the top of the queue.
+ // Intentionally skip first event.
+ for (var i = 1, event; event = queue[i]; i++) {
+ if (event.type == Blockly.Events.CHANGE &&
+ event.element == 'mutation') {
+ queue.unshift(queue.splice(i, 1)[0]);
+ }
+ }
+ return queue;
+};
+
+/**
+ * Modify pending undo events so that when they are fired they don't land
+ * in the undo stack. Called by Blockly.Workspace.clearUndo.
+ */
+Blockly.Events.clearPendingUndo = function() {
+ for (var i = 0, event; event = Blockly.Events.FIRE_QUEUE_[i]; i++) {
+ event.recordUndo = false;
+ }
+};
+
+/**
+ * Stop sending events. Every call to this function MUST also call enable.
+ */
+Blockly.Events.disable = function() {
+ Blockly.Events.disabled_++;
+};
+
+/**
+ * Start sending events. Unless events were already disabled when the
+ * corresponding call to disable was made.
+ */
+Blockly.Events.enable = function() {
+ Blockly.Events.disabled_--;
+};
+
+/**
+ * Returns whether events may be fired or not.
+ * @return {boolean} True if enabled.
+ */
+Blockly.Events.isEnabled = function() {
+ return Blockly.Events.disabled_ == 0;
+};
+
+/**
+ * Current group.
+ * @return {string} ID string.
+ */
+Blockly.Events.getGroup = function() {
+ return Blockly.Events.group_;
+};
+
+/**
+ * Start or stop a group.
+ * @param {boolean|string} state True to start new group, false to end group.
+ * String to set group explicitly.
+ */
+Blockly.Events.setGroup = function(state) {
+ if (typeof state == 'boolean') {
+ Blockly.Events.group_ = state ? Blockly.genUid() : '';
+ } else {
+ Blockly.Events.group_ = state;
+ }
+};
+
+/**
+ * Compute a list of the IDs of the specified block and all its descendants.
+ * @param {!Blockly.Block} block The root block.
+ * @return {!Array.} List of block IDs.
+ * @private
+ */
+Blockly.Events.getDescendantIds_ = function(block) {
+ var ids = [];
+ var descendants = block.getDescendants();
+ for (var i = 0, descendant; descendant = descendants[i]; i++) {
+ ids[i] = descendant.id;
+ }
+ return ids;
+};
+
+/**
+ * Decode the JSON into an event.
+ * @param {!Object} json JSON representation.
+ * @param {!Blockly.Workspace} workspace Target workspace for event.
+ * @return {!Blockly.Events.Abstract} The event represented by the JSON.
+ */
+Blockly.Events.fromJson = function(json, workspace) {
+ var event;
+ switch (json.type) {
+ case Blockly.Events.CREATE:
+ event = new Blockly.Events.Create(null);
+ break;
+ case Blockly.Events.DELETE:
+ event = new Blockly.Events.Delete(null);
+ break;
+ case Blockly.Events.CHANGE:
+ event = new Blockly.Events.Change(null);
+ break;
+ case Blockly.Events.MOVE:
+ event = new Blockly.Events.Move(null);
+ break;
+ case Blockly.Events.UI:
+ event = new Blockly.Events.Ui(null);
+ break;
+ default:
+ throw 'Unknown event type.';
+ }
+ event.fromJson(json);
+ event.workspaceId = workspace.id;
+ return event;
+};
+
+/**
+ * Abstract class for an event.
+ * @param {Blockly.Block} block The block.
+ * @constructor
+ */
+Blockly.Events.Abstract = function(block) {
+ if (block) {
+ this.blockId = block.id;
+ this.workspaceId = block.workspace.id;
+ }
+ this.group = Blockly.Events.group_;
+ this.recordUndo = Blockly.Events.recordUndo;
+};
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Abstract.prototype.toJson = function() {
+ var json = {
+ 'type': this.type,
+ 'blockId': this.blockId
+ };
+ if (this.group) {
+ json['group'] = this.group;
+ }
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Abstract.prototype.fromJson = function(json) {
+ this.blockId = json['blockId'];
+ this.group = json['group'];
+};
+
+/**
+ * Does this event record any change of state?
+ * @return {boolean} True if null, false if something changed.
+ */
+Blockly.Events.Abstract.prototype.isNull = function() {
+ return false;
+};
+
+/**
+ * Run an event.
+ * @param {boolean} forward True if run forward, false if run backward (undo).
+ */
+Blockly.Events.Abstract.prototype.run = function(forward) {
+ // Defined by subclasses.
+};
+
+/**
+ * Class for a block creation event.
+ * @param {Blockly.Block} block The created block. Null for a blank event.
+ * @extends {Blockly.Events.Abstract}
+ * @constructor
+ */
+Blockly.Events.Create = function(block) {
+ if (!block) {
+ return; // Blank event to be populated by fromJson.
+ }
+ Blockly.Events.Create.superClass_.constructor.call(this, block);
+ this.xml = Blockly.Xml.blockToDomWithXY(block);
+ this.ids = Blockly.Events.getDescendantIds_(block);
+};
+goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract);
+
+/**
+ * Type of this event.
+ * @type {string}
+ */
+Blockly.Events.Create.prototype.type = Blockly.Events.CREATE;
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Create.prototype.toJson = function() {
+ var json = Blockly.Events.Create.superClass_.toJson.call(this);
+ json['xml'] = Blockly.Xml.domToText(this.xml);
+ json['ids'] = this.ids;
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Create.prototype.fromJson = function(json) {
+ Blockly.Events.Create.superClass_.fromJson.call(this, json);
+ this.xml = Blockly.Xml.textToDom('' + json['xml'] + ' ').firstChild;
+ this.ids = json['ids'];
+};
+
+/**
+ * Run a creation event.
+ * @param {boolean} forward True if run forward, false if run backward (undo).
+ */
+Blockly.Events.Create.prototype.run = function(forward) {
+ var workspace = Blockly.Workspace.getById(this.workspaceId);
+ if (forward) {
+ var xml = goog.dom.createDom('xml');
+ xml.appendChild(this.xml);
+ Blockly.Xml.domToWorkspace(xml, workspace);
+ } else {
+ for (var i = 0, id; id = this.ids[i]; i++) {
+ var block = workspace.getBlockById(id);
+ if (block) {
+ block.dispose(false, true);
+ } else if (id == this.blockId) {
+ // Only complain about root-level block.
+ console.warn("Can't uncreate non-existant block: " + id);
+ }
+ }
+ }
+};
+
+/**
+ * Class for a block deletion event.
+ * @param {Blockly.Block} block The deleted block. Null for a blank event.
+ * @extends {Blockly.Events.Abstract}
+ * @constructor
+ */
+Blockly.Events.Delete = function(block) {
+ if (!block) {
+ return; // Blank event to be populated by fromJson.
+ }
+ if (block.getParent()) {
+ throw 'Connected blocks cannot be deleted.';
+ }
+ Blockly.Events.Delete.superClass_.constructor.call(this, block);
+ this.oldXml = Blockly.Xml.blockToDomWithXY(block);
+ this.ids = Blockly.Events.getDescendantIds_(block);
+};
+goog.inherits(Blockly.Events.Delete, Blockly.Events.Abstract);
+
+/**
+ * Type of this event.
+ * @type {string}
+ */
+Blockly.Events.Delete.prototype.type = Blockly.Events.DELETE;
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Delete.prototype.toJson = function() {
+ var json = Blockly.Events.Delete.superClass_.toJson.call(this);
+ json['ids'] = this.ids;
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Delete.prototype.fromJson = function(json) {
+ Blockly.Events.Delete.superClass_.fromJson.call(this, json);
+ this.ids = json['ids'];
+};
+
+/**
+ * Run a deletion event.
+ * @param {boolean} forward True if run forward, false if run backward (undo).
+ */
+Blockly.Events.Delete.prototype.run = function(forward) {
+ var workspace = Blockly.Workspace.getById(this.workspaceId);
+ if (forward) {
+ for (var i = 0, id; id = this.ids[i]; i++) {
+ var block = workspace.getBlockById(id);
+ if (block) {
+ block.dispose(false, true);
+ } else if (id == this.blockId) {
+ // Only complain about root-level block.
+ console.warn("Can't delete non-existant block: " + id);
+ }
+ }
+ } else {
+ var xml = goog.dom.createDom('xml');
+ xml.appendChild(this.oldXml);
+ Blockly.Xml.domToWorkspace(xml, workspace);
+ }
+};
+
+/**
+ * Class for a block change event.
+ * @param {Blockly.Block} block The changed block. Null for a blank event.
+ * @param {string} element One of 'field', 'comment', 'disabled', etc.
+ * @param {?string} name Name of input or field affected, or null.
+ * @param {string} oldValue Previous value of element.
+ * @param {string} newValue New value of element.
+ * @extends {Blockly.Events.Abstract}
+ * @constructor
+ */
+Blockly.Events.Change = function(block, element, name, oldValue, newValue) {
+ if (!block) {
+ return; // Blank event to be populated by fromJson.
+ }
+ Blockly.Events.Change.superClass_.constructor.call(this, block);
+ this.element = element;
+ this.name = name;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+};
+goog.inherits(Blockly.Events.Change, Blockly.Events.Abstract);
+
+/**
+ * Type of this event.
+ * @type {string}
+ */
+Blockly.Events.Change.prototype.type = Blockly.Events.CHANGE;
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Change.prototype.toJson = function() {
+ var json = Blockly.Events.Change.superClass_.toJson.call(this);
+ json['element'] = this.element;
+ if (this.name) {
+ json['name'] = this.name;
+ }
+ json['newValue'] = this.newValue;
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Change.prototype.fromJson = function(json) {
+ Blockly.Events.Change.superClass_.fromJson.call(this, json);
+ this.element = json['element'];
+ this.name = json['name'];
+ this.newValue = json['newValue'];
+};
+
+/**
+ * Does this event record any change of state?
+ * @return {boolean} True if something changed.
+ */
+Blockly.Events.Change.prototype.isNull = function() {
+ return this.oldValue == this.newValue;
+};
+
+/**
+ * Run a change event.
+ * @param {boolean} forward True if run forward, false if run backward (undo).
+ */
+Blockly.Events.Change.prototype.run = function(forward) {
+ var workspace = Blockly.Workspace.getById(this.workspaceId);
+ var block = workspace.getBlockById(this.blockId);
+ if (!block) {
+ console.warn("Can't change non-existant block: " + this.blockId);
+ return;
+ }
+ if (block.mutator) {
+ // Close the mutator (if open) since we don't want to update it.
+ block.mutator.setVisible(false);
+ }
+ var value = forward ? this.newValue : this.oldValue;
+ switch (this.element) {
+ case 'field':
+ var field = block.getField(this.name);
+ if (field) {
+ field.setValue(value);
+ } else {
+ console.warn("Can't set non-existant field: " + this.name);
+ }
+ break;
+ case 'comment':
+ block.setCommentText(value || null);
+ break;
+ case 'collapsed':
+ block.setCollapsed(value);
+ break;
+ case 'disabled':
+ block.setDisabled(value);
+ break;
+ case 'inline':
+ block.setInputsInline(value);
+ break;
+ case 'mutation':
+ var oldMutation = '';
+ if (block.mutationToDom) {
+ var oldMutationDom = block.mutationToDom();
+ oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
+ }
+ if (block.domToMutation) {
+ value = value || ' ';
+ var dom = Blockly.Xml.textToDom('' + value + ' ');
+ block.domToMutation(dom.firstChild);
+ }
+ Blockly.Events.fire(new Blockly.Events.Change(
+ block, 'mutation', null, oldMutation, value));
+ break;
+ default:
+ console.warn('Unknown change type: ' + this.element);
+ }
+};
+
+/**
+ * Class for a block move event. Created before the move.
+ * @param {Blockly.Block} block The moved block. Null for a blank event.
+ * @extends {Blockly.Events.Abstract}
+ * @constructor
+ */
+Blockly.Events.Move = function(block) {
+ if (!block) {
+ return; // Blank event to be populated by fromJson.
+ }
+ Blockly.Events.Move.superClass_.constructor.call(this, block);
+ var location = this.currentLocation_();
+ this.oldParentId = location.parentId;
+ this.oldInputName = location.inputName;
+ this.oldCoordinate = location.coordinate;
+};
+goog.inherits(Blockly.Events.Move, Blockly.Events.Abstract);
+
+/**
+ * Type of this event.
+ * @type {string}
+ */
+Blockly.Events.Move.prototype.type = Blockly.Events.MOVE;
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Move.prototype.toJson = function() {
+ var json = Blockly.Events.Move.superClass_.toJson.call(this);
+ if (this.newParentId) {
+ json['newParentId'] = this.newParentId;
+ }
+ if (this.newInputName) {
+ json['newInputName'] = this.newInputName;
+ }
+ if (this.newCoordinate) {
+ json['newCoordinate'] = Math.round(this.newCoordinate.x) + ',' +
+ Math.round(this.newCoordinate.y);
+ }
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Move.prototype.fromJson = function(json) {
+ Blockly.Events.Move.superClass_.fromJson.call(this, json);
+ this.newParentId = json['newParentId'];
+ this.newInputName = json['newInputName'];
+ if (json['newCoordinate']) {
+ var xy = json['newCoordinate'].split(',');
+ this.newCoordinate =
+ new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1]));
+ }
+};
+
+/**
+ * Record the block's new location. Called after the move.
+ */
+Blockly.Events.Move.prototype.recordNew = function() {
+ var location = this.currentLocation_();
+ this.newParentId = location.parentId;
+ this.newInputName = location.inputName;
+ this.newCoordinate = location.coordinate;
+};
+
+/**
+ * Returns the parentId and input if the block is connected,
+ * or the XY location if disconnected.
+ * @return {!Object} Collection of location info.
+ * @private
+ */
+Blockly.Events.Move.prototype.currentLocation_ = function() {
+ var workspace = Blockly.Workspace.getById(this.workspaceId);
+ var block = workspace.getBlockById(this.blockId);
+ var location = {};
+ var parent = block.getParent();
+ if (parent) {
+ location.parentId = parent.id;
+ var input = parent.getInputWithBlock(block);
+ if (input) {
+ location.inputName = input.name;
+ }
+ } else {
+ location.coordinate = block.getRelativeToSurfaceXY();
+ }
+ return location;
+};
+
+/**
+ * Does this event record any change of state?
+ * @return {boolean} True if something changed.
+ */
+Blockly.Events.Move.prototype.isNull = function() {
+ return this.oldParentId == this.newParentId &&
+ this.oldInputName == this.newInputName &&
+ goog.math.Coordinate.equals(this.oldCoordinate, this.newCoordinate);
+};
+
+/**
+ * Run a move event.
+ * @param {boolean} forward True if run forward, false if run backward (undo).
+ */
+Blockly.Events.Move.prototype.run = function(forward) {
+ var workspace = Blockly.Workspace.getById(this.workspaceId);
+ var block = workspace.getBlockById(this.blockId);
+ if (!block) {
+ console.warn("Can't move non-existant block: " + this.blockId);
+ return;
+ }
+ var parentId = forward ? this.newParentId : this.oldParentId;
+ var inputName = forward ? this.newInputName : this.oldInputName;
+ var coordinate = forward ? this.newCoordinate : this.oldCoordinate;
+ var parentBlock = null;
+ if (parentId) {
+ parentBlock = workspace.getBlockById(parentId);
+ if (!parentBlock) {
+ console.warn("Can't connect to non-existant block: " + parentId);
+ return;
+ }
+ }
+ if (block.getParent()) {
+ block.unplug();
+ }
+ if (coordinate) {
+ var xy = block.getRelativeToSurfaceXY();
+ block.moveBy(coordinate.x - xy.x, coordinate.y - xy.y);
+ } else {
+ var blockConnection = block.outputConnection || block.previousConnection;
+ var parentConnection;
+ if (inputName) {
+ var input = parentBlock.getInput(inputName);
+ if (input) {
+ parentConnection = input.connection;
+ }
+ } else if (blockConnection.type == Blockly.PREVIOUS_STATEMENT) {
+ parentConnection = parentBlock.nextConnection;
+ }
+ if (parentConnection) {
+ blockConnection.connect(parentConnection);
+ } else {
+ console.warn("Can't connect to non-existant input: " + inputName);
+ }
+ }
+};
+
+/**
+ * Class for a UI event.
+ * @param {Blockly.Block} block The affected block.
+ * @param {string} element One of 'selected', 'comment', 'mutator', etc.
+ * @param {string} oldValue Previous value of element.
+ * @param {string} newValue New value of element.
+ * @extends {Blockly.Events.Abstract}
+ * @constructor
+ */
+Blockly.Events.Ui = function(block, element, oldValue, newValue) {
+ Blockly.Events.Ui.superClass_.constructor.call(this, block);
+ this.element = element;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ this.recordUndo = false;
+};
+goog.inherits(Blockly.Events.Ui, Blockly.Events.Abstract);
+
+/**
+ * Type of this event.
+ * @type {string}
+ */
+Blockly.Events.Ui.prototype.type = Blockly.Events.UI;
+
+/**
+ * Encode the event as JSON.
+ * @return {!Object} JSON representation.
+ */
+Blockly.Events.Ui.prototype.toJson = function() {
+ var json = Blockly.Events.Ui.superClass_.toJson.call(this);
+ json['element'] = this.element;
+ if (this.newValue !== undefined) {
+ json['newValue'] = this.newValue;
+ }
+ return json;
+};
+
+/**
+ * Decode the JSON event.
+ * @param {!Object} json JSON representation.
+ */
+Blockly.Events.Ui.prototype.fromJson = function(json) {
+ Blockly.Events.Ui.superClass_.fromJson.call(this, json);
+ this.element = json['element'];
+ this.newValue = json['newValue'];
+};
diff --git a/core/field.js b/core/field.js
index 9dfed24553..558f5c71f8 100644
--- a/core/field.js
+++ b/core/field.js
@@ -19,7 +19,7 @@
*/
/**
- * @fileoverview Input field. Used for editable titles, variables, etc.
+ * @fileoverview Field. Used for editable titles, variables, etc.
* This is an abstract class that defines the UI on the block. Actual
* instances would be Blockly.FieldTextInput, Blockly.FieldDropdown, etc.
* @author fraser@google.com (Neil Fraser)
@@ -36,13 +36,27 @@ goog.require('goog.userAgent');
/**
- * Class for an editable field.
+ * Abstract class for an editable field.
* @param {string} text The initial content of the field.
+ * @param {Function=} opt_validator An optional function that is called
+ * to validate any constraints on what the user entered. Takes the new
+ * text as an argument and returns either the accepted text, a replacement
+ * text, or null to abort the change.
* @constructor
*/
-Blockly.Field = function(text) {
- this.size_ = new goog.math.Size(0, 25);
- this.setText(text);
+Blockly.Field = function(text, opt_validator) {
+ this.size_ = new goog.math.Size(
+ Blockly.BlockSvg.FIELD_WIDTH,
+ Blockly.BlockSvg.FIELD_HEIGHT);
+ this.setValue(text);
+ this.setValidator(opt_validator);
+
+ /**
+ * Maximum characters of text to display before adding an ellipsis.
+ * Same for strings and numbers.
+ * @type {number}
+ */
+ this.maxDisplayLength = Blockly.BlockSvg.MAX_DISPLAY_LENGTH;
};
/**
@@ -59,31 +73,45 @@ Blockly.Field.cacheWidths_ = null;
*/
Blockly.Field.cacheReference_ = 0;
+
/**
- * Maximum characters of text to display before adding an ellipsis.
+ * Name of field. Unique within each block.
+ * Static labels are usually unnamed.
+ * @type {string=}
+ */
+Blockly.Field.prototype.name = undefined;
+
+/**
+ * Visible text to display.
+ * @type {string}
+ * @private
*/
-Blockly.Field.prototype.maxDisplayLength = 50;
+Blockly.Field.prototype.text_ = '';
/**
* Block this field is attached to. Starts as null, then in set in init.
+ * @type {Blockly.Block}
* @private
*/
Blockly.Field.prototype.sourceBlock_ = null;
/**
* Is the field visible, or hidden due to the block being collapsed?
+ * @type {boolean}
* @private
*/
Blockly.Field.prototype.visible_ = true;
/**
- * Change handler called when user edits an editable field.
+ * Validation function called when user edits an editable field.
+ * @type {Function}
* @private
*/
-Blockly.Field.prototype.changeHandler_ = null;
+Blockly.Field.prototype.validator_ = null;
/**
* Non-breaking space.
+ * @const
*/
Blockly.Field.NBSP = '\u00A0';
@@ -93,37 +121,48 @@ Blockly.Field.NBSP = '\u00A0';
Blockly.Field.prototype.EDITABLE = true;
/**
- * Install this field on a block.
+ * Attach this field to a block.
* @param {!Blockly.Block} block The block containing this field.
*/
-Blockly.Field.prototype.init = function(block) {
- if (this.sourceBlock_) {
+Blockly.Field.prototype.setSourceBlock = function(block) {
+ goog.asserts.assert(!this.sourceBlock_, 'Field already bound to a block.');
+ this.sourceBlock_ = block;
+};
+
+/**
+ * Install this field on a block.
+ */
+Blockly.Field.prototype.init = function() {
+ if (this.fieldGroup_) {
// Field has already been initialized once.
return;
}
- this.sourceBlock_ = block;
// Build the DOM.
this.fieldGroup_ = Blockly.createSvgElement('g', {}, null);
if (!this.visible_) {
this.fieldGroup_.style.display = 'none';
}
- this.borderRect_ = Blockly.createSvgElement('rect',
- {'rx': 4,
- 'ry': 4,
- 'x': -Blockly.BlockSvg.SEP_SPACE_X / 2,
- 'y': 0,
- 'height': 16}, this.fieldGroup_, this.sourceBlock_.workspace);
+ // Adjust X to be flipped for RTL. Position is relative to horizontal start of source block.
+ var fieldX = (this.sourceBlock_.RTL) ? -this.size_.width / 2 : this.size_.width / 2;
/** @type {!Element} */
this.textElement_ = Blockly.createSvgElement('text',
- {'class': 'blocklyText', 'y': this.size_.height - 12.5},
+ {'class': 'blocklyText',
+ 'x': fieldX,
+ 'y': this.size_.height / 2 + Blockly.BlockSvg.FIELD_TOP_PADDING,
+ 'text-anchor': 'middle'},
this.fieldGroup_);
this.updateEditable();
- block.getSvgRoot().appendChild(this.fieldGroup_);
+ this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
this.mouseUpWrapper_ =
- Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_);
+ Blockly.bindEvent_(this.getClickTarget_(), 'mouseup', this,
+ this.onMouseUp_);
// Force a render.
this.updateTextNode_();
+ if (Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, '', this.getValue()));
+ }
};
/**
@@ -138,8 +177,7 @@ Blockly.Field.prototype.dispose = function() {
goog.dom.removeNode(this.fieldGroup_);
this.fieldGroup_ = null;
this.textElement_ = null;
- this.borderRect_ = null;
- this.changeHandler_ = null;
+ this.validator_ = null;
};
/**
@@ -154,13 +192,13 @@ Blockly.Field.prototype.updateEditable = function() {
'blocklyEditableText');
Blockly.removeClass_(/** @type {!Element} */ (this.fieldGroup_),
'blocklyNoNEditableText');
- this.fieldGroup_.style.cursor = this.CURSOR;
+ this.getClickTarget_().style.cursor = this.CURSOR;
} else {
Blockly.addClass_(/** @type {!Element} */ (this.fieldGroup_),
'blocklyNonEditableText');
Blockly.removeClass_(/** @type {!Element} */ (this.fieldGroup_),
'blocklyEditableText');
- this.fieldGroup_.style.cursor = '';
+ this.getClickTarget_().style.cursor = '';
}
};
@@ -189,11 +227,11 @@ Blockly.Field.prototype.setVisible = function(visible) {
};
/**
- * Sets a new change handler for editable fields.
- * @param {Function} handler New change handler, or null.
+ * Sets a new validation function for editable fields.
+ * @param {Function} handler New validation function, or null.
*/
-Blockly.Field.prototype.setChangeHandler = function(handler) {
- this.changeHandler_ = handler;
+Blockly.Field.prototype.setValidator = function(handler) {
+ this.validator_ = handler;
};
/**
@@ -228,10 +266,6 @@ Blockly.Field.prototype.render_ = function() {
Blockly.Field.cacheWidths_[key] = width;
}
}
- if (this.borderRect_) {
- this.borderRect_.setAttribute('width',
- width + Blockly.BlockSvg.SEP_SPACE_X);
- }
} else {
var width = 0;
}
@@ -241,8 +275,6 @@ Blockly.Field.prototype.render_ = function() {
/**
* Start caching field widths. Every call to this function MUST also call
* stopCache. Caches must not survive between execution threads.
- * @type {Object}
- * @private
*/
Blockly.Field.startCache = function() {
Blockly.Field.cacheReference_++;
@@ -254,8 +286,6 @@ Blockly.Field.startCache = function() {
/**
* Stop caching field widths. Unless caching was already on when the
* corresponding call to startCache was made.
- * @type {number}
- * @private
*/
Blockly.Field.stopCache = function() {
Blockly.Field.cacheReference_--;
@@ -279,12 +309,13 @@ Blockly.Field.prototype.getSize = function() {
* Returns the height and width of the field,
* accounting for the workspace scaling.
* @return {!goog.math.Size} Height and width.
+ * @private
*/
Blockly.Field.prototype.getScaledBBox_ = function() {
- var bBox = this.borderRect_.getBBox();
- // Create new object, as getBBox can return an uneditable SVGRect in IE.
- return new goog.math.Size(bBox.width * this.sourceBlock_.workspace.scale,
- bBox.height * this.sourceBlock_.workspace.scale);
+ var size = this.getSize();
+ // Create new object, so as to not return an uneditable SVGRect in IE.
+ return new goog.math.Size(size.width * this.sourceBlock_.workspace.scale,
+ size.height * this.sourceBlock_.workspace.scale);
};
/**
@@ -315,7 +346,6 @@ Blockly.Field.prototype.setText = function(text) {
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
this.sourceBlock_.render();
this.sourceBlock_.bumpNeighbours_();
- this.sourceBlock_.workspace.fireChangeEvent();
}
};
@@ -332,6 +362,10 @@ Blockly.Field.prototype.updateTextNode_ = function() {
if (text.length > this.maxDisplayLength) {
// Truncate displayed string and add an ellipsis ('...').
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
+ // Add special class for sizing font when truncated
+ this.textElement_.setAttribute('class', 'blocklyText blocklyTextTruncated');
+ } else {
+ this.textElement_.setAttribute('class', 'blocklyText');
}
// Empty the text element.
goog.dom.removeChildren(/** @type {!Element} */ (this.textElement_));
@@ -364,10 +398,22 @@ Blockly.Field.prototype.getValue = function() {
/**
* By default there is no difference between the human-readable text and
* the language-neutral values. Subclasses (such as dropdown) may define this.
- * @param {string} text New text.
+ * @param {string} newText New text.
*/
-Blockly.Field.prototype.setValue = function(text) {
- this.setText(text);
+Blockly.Field.prototype.setValue = function(newText) {
+ if (newText === null) {
+ // No change if null.
+ return;
+ }
+ var oldText = this.getValue();
+ if (oldText == newText) {
+ return;
+ }
+ if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, oldText, newText));
+ }
+ this.setText(newText);
};
/**
@@ -385,7 +431,7 @@ Blockly.Field.prototype.onMouseUp_ = function(e) {
} else if (Blockly.isRightButton(e)) {
// Right-click.
return;
- } else if (Blockly.dragMode_ == 2) {
+ } else if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
// Drag operation is concluding. Don't open the editor.
return;
} else if (this.sourceBlock_.isEditable()) {
@@ -403,12 +449,37 @@ Blockly.Field.prototype.setTooltip = function(newTip) {
// Non-abstract sub-classes may wish to implement this. See FieldLabel.
};
+/**
+ * Select the element to bind the click handler to. When this element is
+ * clicked on an editable field, the editor will open.
+ *
+ * If the block has multiple fields, this is just the group containing the
+ * field. If the block has only one field, we handle clicks over the whole
+ * block.
+ *
+ * @return {!Element} Element to bind click handler to.
+ * @private
+ */
+Blockly.Field.prototype.getClickTarget_ = function() {
+ var nFields = 0;
+
+ for (var i = 0, input; input = this.sourceBlock_.inputList[i]; i++) {
+ nFields += input.fieldRow.length;
+ }
+
+ if (nFields <= 1) {
+ return this.sourceBlock_.getSvgRoot();
+ } else {
+ return this.getSvgRoot();
+ }
+};
+
/**
* Return the absolute coordinates of the top-left corner of this field.
* The origin (0,0) is the top-left corner of the page body.
- * @return {{!goog.math.Coordinate}} Object with .x and .y properties.
+ * @return {!goog.math.Coordinate} Object with .x and .y properties.
* @private
*/
Blockly.Field.prototype.getAbsoluteXY_ = function() {
- return goog.style.getPageOffset(this.borderRect_);
+ return goog.style.getPageOffset(this.getClickTarget_());
};
diff --git a/core/field_angle.js b/core/field_angle.js
index 8fb311fcc0..b5ca196cee 100644
--- a/core/field_angle.js
+++ b/core/field_angle.js
@@ -34,20 +34,19 @@ goog.require('goog.userAgent');
/**
* Class for an editable angle field.
* @param {string} text The initial content of the field.
- * @param {Function=} opt_changeHandler An optional function that is called
+ * @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
-Blockly.FieldAngle = function(text, opt_changeHandler) {
+Blockly.FieldAngle = function(text, opt_validator) {
// Add degree symbol: "360°" (LTR) or "°360" (RTL)
this.symbol_ = Blockly.createSvgElement('tspan', {}, null);
this.symbol_.appendChild(document.createTextNode('\u00B0'));
- Blockly.FieldAngle.superClass_.constructor.call(this, text, null);
- this.setChangeHandler(opt_changeHandler);
+ Blockly.FieldAngle.superClass_.constructor.call(this, text, opt_validator);
};
goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput);
@@ -55,7 +54,7 @@ goog.inherits(Blockly.FieldAngle, Blockly.FieldTextInput);
* Sets a new change handler for angle field.
* @param {Function} handler New change handler, or null.
*/
-Blockly.FieldAngle.prototype.setChangeHandler = function(handler) {
+Blockly.FieldAngle.prototype.setValidator = function(handler) {
var wrappedHandler;
if (handler) {
// Wrap the user's change handler together with the angle validator.
@@ -68,7 +67,7 @@ Blockly.FieldAngle.prototype.setChangeHandler = function(handler) {
v1 = value;
}
var v2 = Blockly.FieldAngle.angleValidator.call(this, v1);
- if (v2 !== undefined) {
+ if (v2 === undefined) {
v2 = v1;
}
}
@@ -77,7 +76,7 @@ Blockly.FieldAngle.prototype.setChangeHandler = function(handler) {
} else {
wrappedHandler = Blockly.FieldAngle.angleValidator;
}
- Blockly.FieldAngle.superClass_.setChangeHandler.call(this, wrappedHandler);
+ Blockly.FieldAngle.superClass_.setValidator.call(this, wrappedHandler);
};
/**
@@ -91,6 +90,36 @@ Blockly.FieldAngle.ROUND = 15;
*/
Blockly.FieldAngle.HALF = 100 / 2;
+/* The following two settings work together to set the behaviour of the angle
+ * picker. While many combinations are possible, two modes are typical:
+ * Math mode.
+ * 0 deg is right, 90 is up. This is the style used by protractors.
+ * Blockly.FieldAngle.CLOCKWISE = false;
+ * Blockly.FieldAngle.OFFSET = 0;
+ * Compass mode.
+ * 0 deg is up, 90 is right. This is the style used by maps.
+ * Blockly.FieldAngle.CLOCKWISE = true;
+ * Blockly.FieldAngle.OFFSET = 90;
+ */
+
+/**
+ * Angle increases clockwise (true) or counterclockwise (false).
+ */
+Blockly.FieldAngle.CLOCKWISE = false;
+
+/**
+ * Offset the location of 0 degrees (and all angles) by a constant.
+ * Usually either 0 (0 = right) or 90 (0 = up).
+ */
+Blockly.FieldAngle.OFFSET = 0;
+
+/**
+ * Maximum allowed angle before wrapping.
+ * Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
+ */
+Blockly.FieldAngle.WRAP = 360;
+
+
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
@@ -196,18 +225,20 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) {
} else if (dy > 0) {
angle += 360;
}
+ if (Blockly.FieldAngle.CLOCKWISE) {
+ angle = Blockly.FieldAngle.OFFSET + 360 - angle;
+ } else {
+ angle -= Blockly.FieldAngle.OFFSET;
+ }
if (Blockly.FieldAngle.ROUND) {
angle = Math.round(angle / Blockly.FieldAngle.ROUND) *
Blockly.FieldAngle.ROUND;
}
- if (angle >= 360) {
- // Rounding may have rounded up to 360.
- angle -= 360;
- }
- angle = String(angle);
+ angle = Blockly.FieldAngle.angleValidator(angle);
Blockly.FieldTextInput.htmlInput_.value = angle;
- this.setText(angle);
+ this.setValue(angle);
this.validate_();
+ this.resizeEditor_();
};
/**
@@ -239,26 +270,33 @@ Blockly.FieldAngle.prototype.updateGraph_ = function() {
if (!this.gauge_) {
return;
}
- var angleRadians = goog.math.toRadians(Number(this.getText()));
- if (isNaN(angleRadians)) {
- this.gauge_.setAttribute('d',
- 'M ' + Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF);
- this.line_.setAttribute('x2', Blockly.FieldAngle.HALF);
- this.line_.setAttribute('y2', Blockly.FieldAngle.HALF);
- } else {
- var x = Blockly.FieldAngle.HALF + Math.cos(angleRadians) *
- Blockly.FieldAngle.RADIUS;
- var y = Blockly.FieldAngle.HALF + Math.sin(angleRadians) *
- -Blockly.FieldAngle.RADIUS;
- var largeFlag = (angleRadians > Math.PI) ? 1 : 0;
- this.gauge_.setAttribute('d',
- 'M ' + Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF +
- ' h ' + Blockly.FieldAngle.RADIUS +
- ' A ' + Blockly.FieldAngle.RADIUS + ',' + Blockly.FieldAngle.RADIUS +
- ' 0 ' + largeFlag + ' 0 ' + x + ',' + y + ' z');
- this.line_.setAttribute('x2', x);
- this.line_.setAttribute('y2', y);
+ var angleDegrees = Number(this.getText()) + Blockly.FieldAngle.OFFSET;
+ var angleRadians = goog.math.toRadians(angleDegrees);
+ var path = ['M ', Blockly.FieldAngle.HALF, ',', Blockly.FieldAngle.HALF];
+ var x2 = Blockly.FieldAngle.HALF;
+ var y2 = Blockly.FieldAngle.HALF;
+ if (!isNaN(angleRadians)) {
+ var angle1 = goog.math.toRadians(Blockly.FieldAngle.OFFSET);
+ var x1 = Math.cos(angle1) * Blockly.FieldAngle.RADIUS;
+ var y1 = Math.sin(angle1) * -Blockly.FieldAngle.RADIUS;
+ if (Blockly.FieldAngle.CLOCKWISE) {
+ angleRadians = 2 * angle1 - angleRadians;
+ }
+ x2 += Math.cos(angleRadians) * Blockly.FieldAngle.RADIUS;
+ y2 -= Math.sin(angleRadians) * Blockly.FieldAngle.RADIUS;
+ // Don't ask how the flag calculations work. They just do.
+ var largeFlag = Math.abs(Math.floor((angleRadians - angle1) / Math.PI) % 2);
+ if (Blockly.FieldAngle.CLOCKWISE) {
+ largeFlag = 1 - largeFlag;
+ }
+ var sweepFlag = Number(Blockly.FieldAngle.CLOCKWISE);
+ path.push(' l ', x1, ',', y1,
+ ' A ', Blockly.FieldAngle.RADIUS, ',', Blockly.FieldAngle.RADIUS,
+ ' 0 ', largeFlag, ' ', sweepFlag, ' ', x2, ',', y2, ' z');
}
+ this.gauge_.setAttribute('d', path.join(''));
+ this.line_.setAttribute('x2', x2);
+ this.line_.setAttribute('y2', y2);
};
/**
@@ -273,7 +311,10 @@ Blockly.FieldAngle.angleValidator = function(text) {
if (n < 0) {
n += 360;
}
+ if (n > Blockly.FieldAngle.WRAP) {
+ n -= 360;
+ }
n = String(n);
- }
+ }
return n;
};
diff --git a/core/field_checkbox.js b/core/field_checkbox.js
index c56b2e70ea..183e226c9e 100644
--- a/core/field_checkbox.js
+++ b/core/field_checkbox.js
@@ -32,22 +32,25 @@ goog.require('Blockly.Field');
/**
* Class for a checkbox field.
* @param {string} state The initial state of the field ('TRUE' or 'FALSE').
- * @param {Function=} opt_changeHandler A function that is executed when a new
+ * @param {Function=} opt_validator A function that is executed when a new
* option is selected. Its sole argument is the new checkbox state. If
* it returns a value, this becomes the new checkbox state, unless the
* value is null, in which case the change is aborted.
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldCheckbox = function(state, opt_changeHandler) {
- Blockly.FieldCheckbox.superClass_.constructor.call(this, '');
-
- this.setChangeHandler(opt_changeHandler);
+Blockly.FieldCheckbox = function(state, opt_validator) {
+ Blockly.FieldCheckbox.superClass_.constructor.call(this, '', opt_validator);
// Set the initial state.
this.setValue(state);
};
goog.inherits(Blockly.FieldCheckbox, Blockly.Field);
+/**
+ * Character for the checkmark.
+ */
+Blockly.FieldCheckbox.CHECK_CHAR = '\u2713';
+
/**
* Mouse cursor style when over the hotspot that initiates editability.
*/
@@ -58,7 +61,7 @@ Blockly.FieldCheckbox.prototype.CURSOR = 'default';
* @param {!Blockly.Block} block The block containing this text.
*/
Blockly.FieldCheckbox.prototype.init = function(block) {
- if (this.sourceBlock_) {
+ if (this.fieldGroup_) {
// Checkbox has already been initialized once.
return;
}
@@ -66,8 +69,9 @@ Blockly.FieldCheckbox.prototype.init = function(block) {
// The checkbox doesn't use the inherited text element.
// Instead it uses a custom checkmark element that is either visible or not.
this.checkElement_ = Blockly.createSvgElement('text',
- {'class': 'blocklyText', 'x': -3, 'y': 14}, this.fieldGroup_);
- var textNode = document.createTextNode('\u2713');
+ {'class': 'blocklyText blocklyCheckbox', 'x': -3, 'y': 14},
+ this.fieldGroup_);
+ var textNode = document.createTextNode(Blockly.FieldCheckbox.CHECK_CHAR);
this.checkElement_.appendChild(textNode);
this.checkElement_.style.display = this.state_ ? 'block' : 'none';
};
@@ -87,13 +91,14 @@ Blockly.FieldCheckbox.prototype.getValue = function() {
Blockly.FieldCheckbox.prototype.setValue = function(strBool) {
var newState = (strBool == 'TRUE');
if (this.state_ !== newState) {
+ if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, this.state_, newState));
+ }
this.state_ = newState;
if (this.checkElement_) {
this.checkElement_.style.display = newState ? 'block' : 'none';
}
- if (this.sourceBlock_ && this.sourceBlock_.rendered) {
- this.sourceBlock_.workspace.fireChangeEvent();
- }
}
};
@@ -103,15 +108,14 @@ Blockly.FieldCheckbox.prototype.setValue = function(strBool) {
*/
Blockly.FieldCheckbox.prototype.showEditor_ = function() {
var newState = !this.state_;
- if (this.sourceBlock_ && this.changeHandler_) {
- // Call any change handler, and allow it to override.
- var override = this.changeHandler_(newState);
+ if (this.sourceBlock_ && this.validator_) {
+ // Call any validation function, and allow it to override.
+ var override = this.validator_(newState);
if (override !== undefined) {
newState = override;
}
}
if (newState !== null) {
- this.sourceBlock_.setShadow(false);
this.setValue(String(newState).toUpperCase());
}
};
diff --git a/core/field_colour.js b/core/field_colour.js
index 69fdb93d46..96318b9eec 100644
--- a/core/field_colour.js
+++ b/core/field_colour.js
@@ -36,7 +36,7 @@ goog.require('goog.ui.ColorPicker');
/**
* Class for a colour input field.
* @param {string} colour The initial colour in '#rrggbb' format.
- * @param {Function=} opt_changeHandler A function that is executed when a new
+ * @param {Function=} opt_validator A function that is executed when a new
* colour is selected. Its sole argument is the new colour value. Its
* return value becomes the selected colour, unless it is undefined, in
* which case the new colour stands, or it is null, in which case the change
@@ -44,12 +44,9 @@ goog.require('goog.ui.ColorPicker');
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldColour = function(colour, opt_changeHandler) {
- Blockly.FieldColour.superClass_.constructor.call(this, '\u00A0\u00A0\u00A0');
-
- this.setChangeHandler(opt_changeHandler);
- // Set the initial state.
- this.setValue(colour);
+Blockly.FieldColour = function(colour, opt_validator) {
+ Blockly.FieldColour.superClass_.constructor.call(this, colour, opt_validator);
+ this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP);
};
goog.inherits(Blockly.FieldColour, Blockly.Field);
@@ -73,7 +70,12 @@ Blockly.FieldColour.prototype.columns_ = 0;
*/
Blockly.FieldColour.prototype.init = function(block) {
Blockly.FieldColour.superClass_.init.call(this, block);
- this.borderRect_.style['fillOpacity'] = 1;
+ // TODO(#163): borderRect_ has been removed from the field.
+ // When fixing field_colour, we should re-color the shadow block instead,
+ // or re-implement a rectangle in the field.
+ if (this.borderRect_) {
+ this.borderRect_.style['fillOpacity'] = 1;
+ }
this.setValue(this.getValue());
};
@@ -103,13 +105,15 @@ Blockly.FieldColour.prototype.getValue = function() {
* @param {string} colour The new colour in '#rrggbb' format.
*/
Blockly.FieldColour.prototype.setValue = function(colour) {
+ if (this.sourceBlock_ && Blockly.Events.isEnabled() &&
+ this.colour_ != colour) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, this.colour_, colour));
+ }
this.colour_ = colour;
if (this.borderRect_) {
this.borderRect_.style.fill = colour;
}
- if (this.sourceBlock_ && this.sourceBlock_.rendered) {
- this.sourceBlock_.workspace.fireChangeEvent();
- }
};
/**
@@ -118,6 +122,7 @@ Blockly.FieldColour.prototype.setValue = function(colour) {
*/
Blockly.FieldColour.prototype.getText = function() {
var colour = this.colour_;
+ // Try to use #rgb format if possible, rather than #rrggbb.
var m = colour.match(/^#(.)\1(.)\2(.)\3$/);
if (m) {
colour = '#' + m[1] + m[2] + m[3];
@@ -214,15 +219,14 @@ Blockly.FieldColour.prototype.showEditor_ = function() {
function(event) {
var colour = event.target.getSelectedColor() || '#000000';
Blockly.WidgetDiv.hide();
- if (thisField.sourceBlock_ && thisField.changeHandler_) {
- // Call any change handler, and allow it to override.
- var override = thisField.changeHandler_(colour);
+ if (thisField.sourceBlock_ && thisField.validator_) {
+ // Call any validation function, and allow it to override.
+ var override = thisField.validator_(colour);
if (override !== undefined) {
colour = override;
}
}
if (colour !== null) {
- thisField.sourceBlock_.setShadow(false);
thisField.setValue(colour);
}
});
diff --git a/core/field_date.js b/core/field_date.js
index 11f47d9764..e2a203a2f8 100644
--- a/core/field_date.js
+++ b/core/field_date.js
@@ -39,7 +39,7 @@ goog.require('goog.ui.DatePicker');
/**
* Class for a date input field.
* @param {string} date The initial date.
- * @param {Function=} opt_changeHandler A function that is executed when a new
+ * @param {Function=} opt_validator A function that is executed when a new
* date is selected. Its sole argument is the new date value. Its
* return value becomes the selected date, unless it is undefined, in
* which case the new date stands, or it is null, in which case the change
@@ -47,13 +47,12 @@ goog.require('goog.ui.DatePicker');
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldDate = function(date, opt_changeHandler) {
+Blockly.FieldDate = function(date, opt_validator) {
if (!date) {
date = new goog.date.Date().toIsoString(true);
}
- Blockly.FieldDate.superClass_.constructor.call(this, date);
+ Blockly.FieldDate.superClass_.constructor.call(this, date, opt_validator);
this.setValue(date);
- this.setChangeHandler(opt_changeHandler);
};
goog.inherits(Blockly.FieldDate, Blockly.Field);
@@ -83,8 +82,8 @@ Blockly.FieldDate.prototype.getValue = function() {
* @param {string} date The new date.
*/
Blockly.FieldDate.prototype.setValue = function(date) {
- if (this.sourceBlock_ && this.changeHandler_) {
- var validated = this.changeHandler_(date);
+ if (this.sourceBlock_ && this.validator_) {
+ var validated = this.validator_(date);
// If the new date is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null && validated !== undefined) {
@@ -150,9 +149,9 @@ Blockly.FieldDate.prototype.showEditor_ = function() {
function(event) {
var date = event.date ? event.date.toIsoString(true) : '';
Blockly.WidgetDiv.hide();
- if (thisField.sourceBlock_ && thisField.changeHandler_) {
- // Call any change handler, and allow it to override.
- var override = thisField.changeHandler_(date);
+ if (thisField.sourceBlock_ && thisField.validator_) {
+ // Call any validation function, and allow it to override.
+ var override = thisField.validator_(date);
if (override !== undefined) {
date = override;
}
diff --git a/core/field_dropdown.js b/core/field_dropdown.js
index b773d12f77..d715c8c725 100644
--- a/core/field_dropdown.js
+++ b/core/field_dropdown.js
@@ -39,9 +39,9 @@ goog.require('goog.userAgent');
/**
* Class for an editable dropdown field.
- * @param {(!Array.>|!Function)} menuGenerator An array of options
- * for a dropdown list, or a function which generates these options.
- * @param {Function=} opt_changeHandler A function that is executed when a new
+ * @param {(!Array.>|!Function)} menuGenerator An array of
+ * options for a dropdown list, or a function which generates these options.
+ * @param {Function=} opt_validator A function that is executed when a new
* option is selected, with the newly selected value as its sole argument.
* If it returns a value, that value (which must be one of the options) will
* become selected in place of the newly selected option, unless the return
@@ -49,15 +49,14 @@ goog.require('goog.userAgent');
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldDropdown = function(menuGenerator, opt_changeHandler) {
+Blockly.FieldDropdown = function(menuGenerator, opt_validator) {
this.menuGenerator_ = menuGenerator;
- this.setChangeHandler(opt_changeHandler);
this.trimOptions_();
var firstTuple = this.getOptions_()[0];
- this.value_ = firstTuple[1];
// Call parent's constructor.
- Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[0]);
+ Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1],
+ opt_validator);
};
goog.inherits(Blockly.FieldDropdown, Blockly.Field);
@@ -81,11 +80,10 @@ Blockly.FieldDropdown.prototype.CURSOR = 'default';
* @param {!Blockly.Block} block The block containing this text.
*/
Blockly.FieldDropdown.prototype.init = function(block) {
- if (this.sourceBlock_) {
+ if (this.fieldGroup_) {
// Dropdown has already been initialized once.
return;
}
-
// Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL)
this.arrow_ = Blockly.createSvgElement('tspan', {}, null);
this.arrow_.appendChild(document.createTextNode(
@@ -111,15 +109,14 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() {
var menuItem = e.target;
if (menuItem) {
var value = menuItem.getValue();
- if (thisField.sourceBlock_ && thisField.changeHandler_) {
- // Call any change handler, and allow it to override.
- var override = thisField.changeHandler_(value);
+ if (thisField.sourceBlock_ && thisField.validator_) {
+ // Call any validation function, and allow it to override.
+ var override = thisField.validator_(value);
if (override !== undefined) {
value = override;
}
}
if (value !== null) {
- thisField.sourceBlock_.setShadow(false);
thisField.setValue(value);
}
}
@@ -265,6 +262,13 @@ Blockly.FieldDropdown.prototype.getValue = function() {
* @param {string} newValue New value to set.
*/
Blockly.FieldDropdown.prototype.setValue = function(newValue) {
+ if (newValue === null || newValue === this.value_) {
+ return; // No change if null.
+ }
+ if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, this.value_, newValue));
+ }
this.value_ = newValue;
// Look up and display the human-readable text.
var options = this.getOptions_();
@@ -308,7 +312,6 @@ Blockly.FieldDropdown.prototype.setText = function(text) {
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
this.sourceBlock_.render();
this.sourceBlock_.bumpNeighbours_();
- this.sourceBlock_.workspace.fireChangeEvent();
}
};
diff --git a/core/field_iconmenu.js b/core/field_iconmenu.js
new file mode 100644
index 0000000000..76d2e10987
--- /dev/null
+++ b/core/field_iconmenu.js
@@ -0,0 +1,297 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Massachusetts Institute of Technology
+ * All rights reserved.
+ *
+ * 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 Icon picker input field.
+ * This is primarily for use in Scratch Horizontal blocks.
+ * Pops open a drop-down with icons; when an icon is selected, it replaces
+ * the icon (image field) in the original block.
+ * @author tmickel@mit.edu (Tim Mickel)
+ */
+'use strict';
+
+goog.provide('Blockly.FieldIconMenu');
+
+goog.require('Blockly.DropDownDiv');
+
+/**
+ * Class for an icon menu field.
+ * @param {Object} icons List of icons. These take the same options as an Image Field.
+ * @extends {Blockly.Field}
+ * @constructor
+ */
+Blockly.FieldIconMenu = function(icons) {
+ /** @type {object} */
+ this.icons_ = icons;
+ // Example:
+ // [{src: '...', width: 20, height: 20, alt: '...', value: 'machine_value'}, ...]
+ // First icon provides the default values.
+ var defaultValue = icons[0].value;
+ Blockly.FieldIconMenu.superClass_.constructor.call(this, defaultValue);
+};
+goog.inherits(Blockly.FieldIconMenu, Blockly.Field);
+
+
+/**
+ * Fixed width of the drop-down, in px. Icon buttons will flow inside this width.
+ * @type {number}
+ * @const
+ */
+Blockly.FieldIconMenu.DROPDOWN_WIDTH = 168;
+
+/**
+ * Save the primary colour of the source block while the menu is open, for reset.
+ * @type {number|string}
+ * @private
+ */
+Blockly.FieldIconMenu.savedPrimary_ = null;
+
+/**
+ * Called when the field is placed on a block.
+ * @param {Block} block The owning block.
+ */
+Blockly.FieldIconMenu.prototype.init = function(block) {
+ // Render the arrow icon
+ // Fixed sizes in px. Saved for creating the flip transform of the menu renders above the button.
+ var arrowSize = 12;
+ /** @type {Number} */
+ this.arrowX_ = 18;
+ /** @type {Number} */
+ this.arrowY_ = 10;
+ if (block.RTL) {
+ // In RTL, the icon position is flipped and rendered from the right (offset by width)
+ this.arrowX_ = -this.arrowX_ - arrowSize;
+ }
+ /** @type {Element} */
+ this.arrowIcon_ = Blockly.createSvgElement('image', {
+ 'height': arrowSize + 'px',
+ 'width': arrowSize + 'px',
+ 'transform': 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')'
+ });
+ this.arrowIcon_.setAttributeNS('http://www.w3.org/1999/xlink',
+ 'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow.svg');
+ block.getSvgRoot().appendChild(this.arrowIcon_);
+ Blockly.FieldIconMenu.superClass_.init.call(this, block);
+};
+
+/**
+ * Mouse cursor style when over the hotspot that initiates the editor.
+ * @const
+ */
+Blockly.FieldIconMenu.prototype.CURSOR = 'default';
+
+/**
+* Set the language-neutral value for this icon drop-down menu.
+ * @param {?string} newValue New value.
+ * @override
+ */
+Blockly.FieldIconMenu.prototype.setValue = function(newValue) {
+ if (newValue === null || newValue === this.value_) {
+ return; // No change
+ }
+ if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, this.value_, newValue));
+ }
+ this.value_ = newValue;
+ // Find the relevant icon in this.icons_ to get the image src.
+ this.setParentFieldImage(this.getSrcForValue(this.value_));
+};
+
+/**
+* Find the parent block's FieldImage and set its src.
+ * @param {?string} src New src for the parent block FieldImage.
+ * @private
+ */
+Blockly.FieldIconMenu.prototype.setParentFieldImage = function(src) {
+ // Only attempt if we have a set sourceBlock_ and parentBlock_
+ // It's possible that this function could be called before
+ // a parent block is set; in that case, fail silently.
+ if (this.sourceBlock_ && this.sourceBlock_.parentBlock_) {
+ var parentBlock = this.sourceBlock_.parentBlock_;
+ // Loop through all inputs' fields to find the first FieldImage
+ for (var i = 0, input; input = parentBlock.inputList[i]; i++) {
+ for (var j = 0, field; field = input.fieldRow[j]; j++) {
+ if (field instanceof Blockly.FieldImage) {
+ // Src for a FieldImage is stored in its value.
+ field.setValue(src);
+ return;
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Get the language-neutral value from this drop-down menu.
+ * @return {string} Current language-neutral value.
+ */
+Blockly.FieldIconMenu.prototype.getValue = function() {
+ return this.value_;
+};
+
+/**
+ * For a language-neutral value, get the src for the image that represents it.
+ * @param {string} value Language-neutral value to look up.
+ * @return {string} Src to image representing value
+ */
+Blockly.FieldIconMenu.prototype.getSrcForValue = function(value) {
+ for (var i = 0, icon; icon = this.icons_[i]; i++) {
+ if (icon.value === value) {
+ return icon.src;
+ }
+ }
+};
+
+/**
+ * Show the drop-down menu for editing this field.
+ * @private
+ */
+Blockly.FieldIconMenu.prototype.showEditor_ = function() {
+ // If there is an existing drop-down we own, this is a request to hide the drop-down.
+ if (Blockly.DropDownDiv.hideIfOwner(this)) {
+ return;
+ }
+ // If there is an existing drop-down someone else owns, hide it immediately and clear it.
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ Blockly.DropDownDiv.clearContent();
+ // Populate the drop-down with the icons for this field.
+ var contentDiv = Blockly.DropDownDiv.getContentDiv();
+ // Accessibility properties
+ contentDiv.setAttribute('role', 'menu');
+ contentDiv.setAttribute('aria-haspopup', 'true');
+ for (var i = 0, icon; icon = this.icons_[i]; i++) {
+ // Icons with the type property placeholder take up space but don't have any functionality
+ // Use for special-case layouts
+ if (icon.type == 'placeholder') {
+ var placeholder = document.createElement('span');
+ placeholder.setAttribute('class', 'blocklyDropDownPlaceholder');
+ placeholder.style.width = icon.width + 'px';
+ placeholder.style.height = icon.height + 'px';
+ contentDiv.appendChild(placeholder);
+ continue;
+ }
+ var button = document.createElement('button');
+ button.setAttribute('id', ':' + i); // For aria-activedescendant
+ button.setAttribute('role', 'menuitem');
+ button.setAttribute('class', 'blocklyDropDownButton');
+ button.title = icon.alt;
+ button.style.width = icon.width + 'px';
+ button.style.height = icon.height + 'px';
+ var backgroundColor = this.sourceBlock_.getColour();
+ if (icon.value == this.getValue()) {
+ // This icon is selected, show it in a different colour
+ backgroundColor = this.sourceBlock_.getColourTertiary();
+ button.setAttribute('aria-selected', 'true');
+ }
+ button.style.backgroundColor = backgroundColor;
+ button.style.borderColor = this.sourceBlock_.getColourTertiary();
+ button.onclick = this.buttonClick_.bind(this);
+ button.ontouchend = this.buttonClick_.bind(this);
+ // These are applied manually instead of using the :hover pseudoclass
+ // because Android has a bad long press "helper" menu and green highlight
+ // that we must prevent with ontouchstart preventDefault
+ button.ontouchstart = function(e) {
+ this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
+ e.preventDefault();
+ };
+ button.onmouseover = function() {
+ this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
+ contentDiv.setAttribute('aria-activedescendant', this.id);
+ };
+ button.onmouseout = function() {
+ this.setAttribute('class', 'blocklyDropDownButton');
+ contentDiv.removeAttribute('aria-activedescendant');
+ };
+ var buttonImg = document.createElement('img');
+ buttonImg.src = icon.src;
+ //buttonImg.alt = icon.alt;
+ // Upon click/touch, we will be able to get the clicked element as e.target
+ // Store a data attribute on all possible click targets so we can match it to the icon.
+ button.setAttribute('data-value', icon.value);
+ buttonImg.setAttribute('data-value', icon.value);
+ button.appendChild(buttonImg);
+ contentDiv.appendChild(button);
+ }
+ contentDiv.style.width = Blockly.FieldIconMenu.DROPDOWN_WIDTH + 'px';
+ // Calculate positioning for the drop-down
+ // sourceBlock_ is the rendered shadow field button
+ var scale = this.sourceBlock_.workspace.scale;
+ var bBox = this.sourceBlock_.getHeightWidth();
+ bBox.width *= scale;
+ bBox.height *= scale;
+ var position = this.getAbsoluteXY_();
+ // If we can fit it, render below the shadow block
+ var primaryX = position.x + bBox.width / 2;
+ var primaryY = position.y + bBox.height;
+ // If we can't fit it, render above the entire parent block
+ var secondaryX = primaryX;
+ var secondaryY = position.y - (Blockly.BlockSvg.MIN_BLOCK_Y * scale) - (Blockly.BlockSvg.FIELD_Y_OFFSET * scale);
+
+ Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), this.sourceBlock_.getColourTertiary());
+
+ // Update source block colour to look selected
+ this.savedPrimary_ = this.sourceBlock_.getColour();
+ this.sourceBlock_.setColour(this.sourceBlock_.getColourSecondary(),
+ this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
+
+ Blockly.DropDownDiv.setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);
+ var renderedPrimary = Blockly.DropDownDiv.show(this, primaryX, primaryY,
+ secondaryX, secondaryY, this.onHide_.bind(this));
+ if (!renderedPrimary) {
+ // Adjust for rotation
+ var arrowX = this.arrowX_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5 + 1;
+ var arrowY = this.arrowY_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5;
+ // Flip the arrow on the button
+ this.arrowIcon_.setAttribute('transform',
+ 'translate(' + arrowX + ',' + arrowY + ') rotate(180)');
+ }
+};
+
+/**
+ * Callback for when a button is clicked inside the drop-down.
+ * Should be bound to the FieldIconMenu.
+ * @param {Event} e DOM event for the click/touch
+ * @private
+ */
+Blockly.FieldIconMenu.prototype.buttonClick_ = function(e) {
+ var value = e.target.getAttribute('data-value');
+ this.setValue(value);
+ Blockly.DropDownDiv.hide();
+};
+
+/**
+ * Callback for when the drop-down is hidden.
+ */
+Blockly.FieldIconMenu.prototype.onHide_ = function() {
+ // Reset the button colour and clear accessibility properties
+ // Only attempt to do this reset if sourceBlock_ is not disposed.
+ // It could become disposed before an onHide_, for example,
+ // when a block is dragged from the flyout.
+ if (this.sourceBlock_) {
+ this.sourceBlock_.setColour(this.savedPrimary_,
+ this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
+ }
+ Blockly.DropDownDiv.content_.removeAttribute('role');
+ Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
+ Blockly.DropDownDiv.content_.removeAttribute('aria-activedescendant');
+ // Unflip the arrow if appropriate
+ this.arrowIcon_.setAttribute('transform', 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')');
+};
diff --git a/core/field_image.js b/core/field_image.js
index b24e75c542..49e750a2b6 100644
--- a/core/field_image.js
+++ b/core/field_image.js
@@ -38,17 +38,18 @@ goog.require('goog.userAgent');
* @param {number} width Width of the image.
* @param {number} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
+ * @param {boolean} flip_rtl Whether to flip the icon in RTL
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldImage = function(src, width, height, opt_alt) {
+Blockly.FieldImage = function(src, width, height, opt_alt, flip_rtl) {
this.sourceBlock_ = null;
// Ensure height and width are numbers. Strings are bad at math.
this.height_ = Number(height);
this.width_ = Number(width);
- this.size_ = new goog.math.Size(this.width_,
- this.height_ + 2 * Blockly.BlockSvg.INLINE_PADDING_Y);
+ this.size_ = new goog.math.Size(this.width_, this.height_);
this.text_ = opt_alt || '';
+ this.flipRTL_ = flip_rtl;
this.setValue(src);
};
goog.inherits(Blockly.FieldImage, Blockly.Field);
@@ -67,14 +68,12 @@ Blockly.FieldImage.prototype.EDITABLE = false;
/**
* Install this image on a block.
- * @param {!Blockly.Block} block The block containing this text.
*/
-Blockly.FieldImage.prototype.init = function(block) {
- if (this.sourceBlock_) {
+Blockly.FieldImage.prototype.init = function() {
+ if (this.fieldGroup_) {
// Image has already been initialized once.
return;
}
- this.sourceBlock_ = block;
// Build the DOM.
/** @type {SVGElement} */
this.fieldGroup_ = Blockly.createSvgElement('g', {}, null);
@@ -97,7 +96,7 @@ Blockly.FieldImage.prototype.init = function(block) {
'width': this.width_ + 'px',
'fill-opacity': 0}, this.fieldGroup_);
}
- block.getSvgRoot().appendChild(this.fieldGroup_);
+ this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
// Configure the field to be transparent with respect to tooltips.
var topElement = this.rectElement_ || this.imageElement_;
@@ -151,6 +150,14 @@ Blockly.FieldImage.prototype.setValue = function(src) {
}
};
+/**
+ * Get whether to flip this image in RTL
+ * @return {boolean} True if we should flip in RTL.
+ */
+Blockly.FieldImage.prototype.getFlipRTL = function() {
+ return this.flipRTL_;
+};
+
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
diff --git a/core/field_label.js b/core/field_label.js
index da52f8d09b..cb5fa7d398 100644
--- a/core/field_label.js
+++ b/core/field_label.js
@@ -42,7 +42,7 @@ goog.require('goog.math.Size');
Blockly.FieldLabel = function(text, opt_class) {
this.size_ = new goog.math.Size(0, 17.5);
this.class_ = opt_class;
- this.setText(text);
+ this.setValue(text);
};
goog.inherits(Blockly.FieldLabel, Blockly.Field);
@@ -53,15 +53,12 @@ Blockly.FieldLabel.prototype.EDITABLE = false;
/**
* Install this text on a block.
- * @param {!Blockly.Block} block The block containing this text.
*/
-Blockly.FieldLabel.prototype.init = function(block) {
- if (this.sourceBlock_) {
+Blockly.FieldLabel.prototype.init = function() {
+ if (this.textElement_) {
// Text has already been initialized once.
return;
}
- this.sourceBlock_ = block;
-
// Build the DOM.
this.textElement_ = Blockly.createSvgElement('text',
{'class': 'blocklyText', 'y': this.size_.height - 5}, null);
@@ -71,7 +68,7 @@ Blockly.FieldLabel.prototype.init = function(block) {
if (!this.visible_) {
this.textElement_.style.display = 'none';
}
- block.getSvgRoot().appendChild(this.textElement_);
+ this.sourceBlock_.getSvgRoot().appendChild(this.textElement_);
// Configure the field to be transparent with respect to tooltips.
this.textElement_.tooltip = this.sourceBlock_;
diff --git a/core/field_number.js b/core/field_number.js
new file mode 100644
index 0000000000..e66ecf31fe
--- /dev/null
+++ b/core/field_number.js
@@ -0,0 +1,287 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 Massachusetts Institute of Technology
+ * All rights reserved.
+ *
+ * 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 Field for numbers. Includes validator and numpad on touch.
+ * @author tmickel@mit.edu (Tim Mickel)
+ */
+'use strict';
+
+goog.provide('Blockly.FieldNumber');
+
+goog.require('Blockly.FieldTextInput');
+goog.require('goog.math');
+goog.require('goog.userAgent');
+
+
+/**
+ * Class for an editable number field.
+ * @param {string} text The initial content of the field.
+ * @param {Function=} opt_validator An optional function that is called
+ * to validate any constraints on what the user entered. Takes the new
+ * text as an argument and returns the accepted text or null to abort
+ * the change.
+ * @param {number} precision Precision of the decimal value (negative power of 10).
+ * @param {number} min Minimum value of the number.
+ * @param {number} max Maximum value of the number.
+ * @extends {Blockly.FieldTextInput}
+ * @constructor
+ */
+Blockly.FieldNumber = function(text, opt_validator, precision, min, max) {
+ this.precision_ = precision;
+ this.min_ = min;
+ this.max_ = max;
+ Blockly.FieldNumber.superClass_.constructor.call(this, text, opt_validator);
+};
+goog.inherits(Blockly.FieldNumber, Blockly.FieldTextInput);
+
+/**
+ * Fixed width of the num-pad drop-down, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.FieldNumber.DROPDOWN_WIDTH = 168;
+
+/**
+ * Extra padding to add between the block and the num-pad drop-down, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.FieldNumber.DROPDOWN_Y_PADDING = 8;
+
+/**
+ * Buttons for the num-pad, in order from the top left.
+ * Values are strings of the number or symbol will be added to the field text
+ * when the button is pressed.
+ * @type {Array.}
+ * @const
+ */
+ // Calculator order
+Blockly.FieldNumber.NUMPAD_BUTTONS = ['7', '8', '9', '4', '5', '6', '1', '2', '3', '.', '0'];
+
+/**
+ * Src for the delete icon to be shown on the num-pad.
+ * @type {string}
+ * @const
+ */
+Blockly.FieldNumber.NUMPAD_DELETE_ICON = 'data:image/svg+xml;utf8,' +
+ '' +
+ ' ';
+
+/**
+ * Currently active field during an edit.
+ * Used to give a reference to the num-pad button callbacks.
+ * @type {?FieldNumber}
+ * @private
+ */
+Blockly.FieldNumber.activeField_ = null;
+
+/**
+ * Sets a new change handler for angle field.
+ * @param {Function} handler New change handler, or null.
+ */
+Blockly.FieldNumber.prototype.setValidator = function(handler) {
+ var wrappedHandler;
+ if (handler) {
+ // Wrap the user's change handler together with the number validator.
+ // This is copied entirely from FieldAngle.
+ wrappedHandler = function(value) {
+ var v1 = handler.call(this, value);
+ var v2;
+ if (v1 === null) {
+ v2 = v1;
+ } else {
+ if (v1 === undefined) {
+ v1 = value;
+ }
+ v2 = Blockly.FieldNumber.numberValidator.call(this, v1);
+ if (v2 === undefined) {
+ v2 = v1;
+ }
+ }
+ return v2 === value ? undefined : v2;
+ };
+ } else {
+ wrappedHandler = Blockly.FieldNumber.numberValidator;
+ }
+ Blockly.FieldNumber.superClass_.setValidator.call(this, wrappedHandler);
+};
+
+/**
+ * Show the inline free-text editor on top of the text and the num-pad if appropriate.
+ * @private
+ */
+Blockly.FieldNumber.prototype.showEditor_ = function() {
+ Blockly.FieldNumber.activeField_ = this;
+ // Do not focus on mobile devices so we can show the num-pad
+ var showNumPad =
+ goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD;
+ Blockly.FieldNumber.superClass_.showEditor_.call(this, false, showNumPad);
+
+ // Show a numeric keypad in the drop-down on touch
+ if (showNumPad) {
+ this.showNumPad_();
+ }
+};
+
+Blockly.FieldNumber.prototype.showNumPad_ = function() {
+ // If there is an existing drop-down someone else owns, hide it immediately and clear it.
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ Blockly.DropDownDiv.clearContent();
+
+ var contentDiv = Blockly.DropDownDiv.getContentDiv();
+
+ // Accessibility properties
+ contentDiv.setAttribute('role', 'menu');
+ contentDiv.setAttribute('aria-haspopup', 'true');
+
+ // Add numeric keypad buttons
+ var buttons = Blockly.FieldNumber.NUMPAD_BUTTONS;
+ for (var i = 0, buttonText; buttonText = buttons[i]; i++) {
+ var button = document.createElement('button');
+ button.setAttribute('role', 'menuitem');
+ button.setAttribute('class', 'blocklyNumPadButton');
+ button.title = buttonText;
+ button.innerHTML = buttonText;
+ // Num-pad only reacts on touch devices
+ button.ontouchstart = Blockly.FieldNumber.numPadButtonTouch_;
+ if (buttonText == '.' && this.precision_ == 0) {
+ // Don't show the decimal point for inputs that must be round numbers
+ button.setAttribute('style', 'visibility: hidden');
+ }
+ contentDiv.appendChild(button);
+ }
+ // Add erase button to the end
+ var eraseButton = document.createElement('button');
+ eraseButton.setAttribute('role', 'menuitem');
+ eraseButton.setAttribute('class', 'blocklyNumPadButton');
+ eraseButton.title = 'Delete';
+ var eraseImage = document.createElement('img');
+ eraseImage.src = Blockly.FieldNumber.NUMPAD_DELETE_ICON;
+ eraseButton.appendChild(eraseImage);
+ // Num-pad only reacts on touch devices
+ eraseButton.ontouchstart = Blockly.FieldNumber.numPadEraseButtonTouch_;
+ contentDiv.appendChild(eraseButton);
+
+ // Set colour and size of drop-down
+ Blockly.DropDownDiv.setColour(Blockly.Colours.numPadBackground, Blockly.Colours.numPadBorder);
+ contentDiv.style.width = Blockly.FieldNumber.DROPDOWN_WIDTH + 'px';
+
+ // Calculate positioning for the drop-down
+ // sourceBlock_ is the rendered shadow field input box
+ var scale = this.sourceBlock_.workspace.scale;
+ var bBox = this.sourceBlock_.getHeightWidth();
+ bBox.width *= scale;
+ bBox.height *= scale;
+ var position = this.getAbsoluteXY_();
+ // If we can fit it, render below the shadow block
+ var primaryX = position.x + bBox.width / 2;
+ var primaryY = position.y + bBox.height + Blockly.FieldNumber.DROPDOWN_Y_PADDING;
+ // If we can't fit it, render above the entire parent block
+ var secondaryX = primaryX;
+ var secondaryY = position.y - (Blockly.BlockSvg.MIN_BLOCK_Y * scale) - (Blockly.BlockSvg.FIELD_Y_OFFSET * scale);
+
+ Blockly.DropDownDiv.setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);
+ Blockly.DropDownDiv.show(this, primaryX, primaryY, secondaryX, secondaryY, this.onHide_.bind(this));
+};
+
+/**
+ * Call for when a num-pad button is touched.
+ * Determine what the user is inputting, and update the text field appropriately.
+ */
+Blockly.FieldNumber.numPadButtonTouch_ = function() {
+ // String of the button (e.g., '7')
+ var spliceValue = this.innerHTML;
+ // Old value of the text field
+ var oldValue = Blockly.FieldTextInput.htmlInput_.value;
+ // Determine the selected portion of the text field
+ var selectionStart = Blockly.FieldTextInput.htmlInput_.selectionStart;
+ var selectionEnd = Blockly.FieldTextInput.htmlInput_.selectionEnd;
+ // Splice in the new value
+ var newValue = oldValue.slice(0, selectionStart) + spliceValue + oldValue.slice(selectionEnd);
+ // Updates the display. The actual setValue occurs when the field is stopped editing.
+ Blockly.FieldTextInput.htmlInput_.value = Blockly.FieldTextInput.htmlInput_.defaultValue = newValue;
+ // Resize and scroll the text field appropriately
+ Blockly.FieldNumber.superClass_.resizeEditor_.call(Blockly.FieldNumber.activeField_);
+ Blockly.FieldTextInput.htmlInput_.setSelectionRange(newValue.length, newValue.length);
+ Blockly.FieldTextInput.htmlInput_.scrollLeft = Blockly.FieldTextInput.htmlInput_.scrollWidth;
+};
+
+/**
+ * Call for when the num-pad erase button is touched.
+ * Determine what the user is asking to erase, and erase it.
+ */
+Blockly.FieldNumber.numPadEraseButtonTouch_ = function() {
+ // Old value of the text field
+ var oldValue = Blockly.FieldTextInput.htmlInput_.value;
+ // Determine what is selected to erase (if anything)
+ var selectionStart = Blockly.FieldTextInput.htmlInput_.selectionStart;
+ var selectionEnd = Blockly.FieldTextInput.htmlInput_.selectionEnd;
+ // Cut out anything that was previously selected
+ var newValue = oldValue.slice(0, selectionStart) + oldValue.slice(selectionEnd);
+ if (selectionEnd - selectionStart == 0) { // Length of selection == 0
+ // Delete the last character if nothing was selected
+ newValue = oldValue.slice(0, selectionStart - 1) + oldValue.slice(selectionStart);
+ }
+ // Update the display to show erased value.
+ Blockly.FieldTextInput.htmlInput_.value = Blockly.FieldTextInput.htmlInput_.defaultValue = newValue;
+ // Resize and scroll the text field appropriately
+ Blockly.FieldNumber.superClass_.resizeEditor_.call(Blockly.FieldNumber.activeField_);
+ Blockly.FieldTextInput.htmlInput_.setSelectionRange(newValue.length, newValue.length);
+ Blockly.FieldTextInput.htmlInput_.scrollLeft = Blockly.FieldTextInput.htmlInput_.scrollWidth;
+};
+
+/**
+ * Callback for when the drop-down is hidden.
+ */
+Blockly.FieldNumber.prototype.onHide_ = function() {
+ // Clear accessibility properties
+ Blockly.DropDownDiv.content_.removeAttribute('role');
+ Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
+};
+
+/**
+ * Ensure that only a number may be entered with the properties of this field.
+ * @param {string} text The user's text.
+ * @return {?string} A string representing a valid angle, or null if invalid.
+ */
+Blockly.FieldNumber.numberValidator = function(text) {
+ var n = Blockly.FieldTextInput.numberValidator(text);
+ if (n !== null) {
+ // string -> float
+ n = parseFloat(n);
+ // Keep within min and max
+ n = Math.min(Math.max(n, this.min_), this.max_);
+ // Update float precision (returns a string)
+ n = n.toFixed(this.precision_);
+ // Parse to a float and back to string to remove trailing decimals
+ n = parseFloat(n);
+ n = String(n);
+ }
+ return n;
+};
diff --git a/core/field_textinput.js b/core/field_textinput.js
index a5cac7ebb8..e89f80c710 100644
--- a/core/field_textinput.js
+++ b/core/field_textinput.js
@@ -26,8 +26,11 @@
goog.provide('Blockly.FieldTextInput');
+goog.require('Blockly.BlockSvg.render');
+goog.require('Blockly.Colours');
goog.require('Blockly.Field');
goog.require('Blockly.Msg');
+goog.require('Blockly.utils');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.userAgent');
@@ -36,23 +39,44 @@ goog.require('goog.userAgent');
/**
* Class for an editable text field.
* @param {string} text The initial content of the field.
- * @param {Function=} opt_changeHandler An optional function that is called
+ * @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @extends {Blockly.Field}
* @constructor
*/
-Blockly.FieldTextInput = function(text, opt_changeHandler) {
- Blockly.FieldTextInput.superClass_.constructor.call(this, text);
- this.setChangeHandler(opt_changeHandler);
+Blockly.FieldTextInput = function(text, opt_validator) {
+ Blockly.FieldTextInput.superClass_.constructor.call(this, text,
+ opt_validator);
};
goog.inherits(Blockly.FieldTextInput, Blockly.Field);
/**
- * Point size of text. Should match blocklyText's font-size in CSS.
+ * Point size of text before animation. Must match size in CSS.
*/
-Blockly.FieldTextInput.FONTSIZE = 11;
+Blockly.FieldTextInput.FONTSIZE_INITIAL = 12;
+
+/**
+ * Point size of text after animation.
+ */
+Blockly.FieldTextInput.FONTSIZE_FINAL = 14;
+
+/**
+ * Length of animations in seconds.
+ */
+Blockly.FieldTextInput.ANIMATION_TIME = 0.25;
+
+/**
+ * Padding to use for text measurement for the field during editing, in px.
+ */
+Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC = 45;
+
+/**
+ * Numeric field types.
+ * Scratch-specific property to ensure the border radius is set correctly for these types.
+ */
+Blockly.FieldTextInput.NUMERIC_FIELD_TYPES = ['math_number', 'math_positive_number', 'math_whole_number'];
/**
* Mouse cursor style when over the hotspot that initiates the editor.
@@ -78,20 +102,19 @@ Blockly.FieldTextInput.prototype.dispose = function() {
* @param {?string} text New text.
* @override
*/
-Blockly.FieldTextInput.prototype.setText = function(text) {
+Blockly.FieldTextInput.prototype.setValue = function(text) {
if (text === null) {
- // No change if null.
- return;
+ return; // No change if null.
}
- if (this.sourceBlock_ && this.changeHandler_) {
- var validated = this.changeHandler_(text);
+ if (this.sourceBlock_ && this.validator_) {
+ var validated = this.validator_(text);
// If the new text is invalid, validation returns null.
// In this case we still want to display the illegal result.
if (validated !== null && validated !== undefined) {
text = validated;
}
}
- Blockly.Field.prototype.setText.call(this, text);
+ Blockly.Field.prototype.setValue.call(this, text);
};
/**
@@ -106,35 +129,26 @@ Blockly.FieldTextInput.prototype.setSpellcheck = function(check) {
* Show the inline free-text editor on top of the text.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
+ * @param {boolean=} opt_readOnly True if editor should be created with HTML
+ * input set to read-only, to prevent virtual keyboards.
* @private
*/
-Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
+Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput, opt_readOnly) {
+ this.workspace_ = this.sourceBlock_.workspace;
var quietInput = opt_quietInput || false;
- if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID ||
- goog.userAgent.IPAD)) {
- // Mobile browsers have issues with in-line textareas (focus & keyboards).
- var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_);
- if (this.sourceBlock_ && this.changeHandler_) {
- var override = this.changeHandler_(newValue);
- if (override !== undefined) {
- newValue = override;
- }
- }
- if (newValue !== null) {
- this.setText(newValue);
- }
- return;
- }
-
- Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_());
+ var readOnly = opt_readOnly || false;
+ Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL,
+ this.widgetDispose_(), this.widgetDisposeAnimationFinished_(),
+ Blockly.FieldTextInput.ANIMATION_TIME);
var div = Blockly.WidgetDiv.DIV;
+ // Apply text-input-specific fixed CSS
+ div.className += ' fieldTextInput';
// Create the input.
var htmlInput = goog.dom.createDom('input', 'blocklyHtmlInput');
htmlInput.setAttribute('spellcheck', this.spellcheck_);
- var fontSize = (Blockly.FieldTextInput.FONTSIZE *
- this.sourceBlock_.workspace.scale) + 'pt';
- div.style.fontSize = fontSize;
- htmlInput.style.fontSize = fontSize;
+ if (readOnly) {
+ htmlInput.setAttribute('readonly', 'true');
+ }
/** @type {!HTMLInputElement} */
Blockly.FieldTextInput.htmlInput_ = htmlInput;
div.appendChild(htmlInput);
@@ -146,6 +160,8 @@ Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
if (!quietInput) {
htmlInput.focus();
htmlInput.select();
+ // For iOS only
+ htmlInput.setSelectionRange(0, 99999);
}
// Bind to keydown -- trap Enter without IME and Esc to hide.
@@ -157,10 +173,19 @@ Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) {
// Bind to keyPress -- repeatedly resize when holding down a key.
htmlInput.onKeyPressWrapper_ =
Blockly.bindEvent_(htmlInput, 'keypress', this, this.onHtmlInputChange_);
- var workspaceSvg = this.sourceBlock_.workspace.getCanvas();
- htmlInput.onWorkspaceChangeWrapper_ =
- Blockly.bindEvent_(workspaceSvg, 'blocklyWorkspaceChange', this,
- this.resizeEditor_);
+ htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this);
+ this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_);
+
+ // Add animation transition properties
+ div.style.transition = 'padding ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
+ 'width ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
+ 'height ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
+ 'margin-left ' + Blockly.FieldTextInput.ANIMATION_TIME + 's,' +
+ 'box-shadow ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
+ htmlInput.style.transition = 'font-size ' + Blockly.FieldTextInput.ANIMATION_TIME + 's';
+ // The animated properties themselves
+ htmlInput.style.fontSize = Blockly.FieldTextInput.FONTSIZE_FINAL + 'pt';
+ div.style.boxShadow = '0px 0px 0px 4px ' + Blockly.Colours.fieldShadow;
};
/**
@@ -174,7 +199,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
if (e.keyCode == enterKey) {
Blockly.WidgetDiv.hide();
} else if (e.keyCode == escKey) {
- this.setText(htmlInput.defaultValue);
+ htmlInput.value = htmlInput.defaultValue;
Blockly.WidgetDiv.hide();
} else if (e.keyCode == tabKey) {
Blockly.WidgetDiv.hide();
@@ -190,21 +215,18 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) {
*/
Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) {
var htmlInput = Blockly.FieldTextInput.htmlInput_;
- var escKey = 27;
- if (e.keyCode != escKey) {
- // Update source block.
- var text = htmlInput.value;
- if (text !== htmlInput.oldValue_) {
- this.sourceBlock_.setShadow(false);
- htmlInput.oldValue_ = text;
- this.setText(text);
- this.validate_();
- } else if (goog.userAgent.WEBKIT) {
- // Cursor key. Render the source block to show the caret moving.
- // Chrome only (version 26, OS X).
- this.sourceBlock_.render();
- }
+ // Update source block.
+ var text = htmlInput.value;
+ if (text !== htmlInput.oldValue_) {
+ htmlInput.oldValue_ = text;
+ this.setValue(text);
+ this.validate_();
+ } else if (goog.userAgent.WEBKIT) {
+ // Cursor key. Render the source block to show the caret moving.
+ // Chrome only (version 26, OS X).
+ this.sourceBlock_.render();
}
+ this.resizeEditor_();
};
/**
@@ -216,8 +238,8 @@ Blockly.FieldTextInput.prototype.validate_ = function() {
var valid = true;
goog.asserts.assertObject(Blockly.FieldTextInput.htmlInput_);
var htmlInput = Blockly.FieldTextInput.htmlInput_;
- if (this.sourceBlock_ && this.changeHandler_) {
- valid = this.changeHandler_(htmlInput.value);
+ if (this.sourceBlock_ && this.validator_) {
+ valid = this.validator_(htmlInput.value);
}
if (valid === null) {
Blockly.addClass_(htmlInput, 'blocklyInvalidInput');
@@ -231,67 +253,136 @@ Blockly.FieldTextInput.prototype.validate_ = function() {
* @private
*/
Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
+ var scale = this.sourceBlock_.workspace.scale;
var div = Blockly.WidgetDiv.DIV;
- var bBox = this.fieldGroup_.getBBox();
- div.style.width = bBox.width * this.sourceBlock_.workspace.scale + 'px';
- div.style.height = bBox.height * this.sourceBlock_.workspace.scale + 'px';
+ // Resize the box based on the measured width of the text, pre-truncation
+ var textWidth = Blockly.measureText(
+ Blockly.FieldTextInput.htmlInput_.style.fontSize,
+ Blockly.FieldTextInput.htmlInput_.style.fontFamily,
+ Blockly.FieldTextInput.htmlInput_.style.fontWeight,
+ Blockly.FieldTextInput.htmlInput_.value
+ );
+ // Size drawn in the canvas needs padding and scaling
+ textWidth += Blockly.FieldTextInput.TEXT_MEASURE_PADDING_MAGIC;
+ textWidth *= scale;
+ // The width must be at least FIELD_WIDTH and at most FIELD_WIDTH_MAX_EDIT
+ var width = Math.max(textWidth, Blockly.BlockSvg.FIELD_WIDTH_MIN_EDIT * scale);
+ width = Math.min(width, Blockly.BlockSvg.FIELD_WIDTH_MAX_EDIT * scale);
+ // Add 1px to width and height to account for border (pre-scale)
+ div.style.width = (width / scale + 1) + 'px';
+ div.style.height = (Blockly.BlockSvg.FIELD_HEIGHT_MAX_EDIT + 1) + 'px';
+ div.style.transform = 'scale(' + scale + ')';
+
+ // Use margin-left to animate repositioning of the box (value is unscaled).
+ // This is the difference between the default position and the positioning
+ // after growing the box.
+ var initialWidth = Blockly.BlockSvg.FIELD_WIDTH * scale;
+ var finalWidth = width;
+ div.style.marginLeft = -0.5 * (finalWidth - initialWidth) + 'px';
+
+ // Add 0.5px to account for slight difference between SVG and CSS border
+ var borderRadius = this.getBorderRadius() + 0.5;
+ div.style.borderRadius = borderRadius + 'px';
+ // Pull stroke colour from the existing shadow block
+ var strokeColour = this.sourceBlock_.getColourTertiary();
+ div.style.borderColor = strokeColour;
+
var xy = this.getAbsoluteXY_();
+ // Account for border width, post-scale
+ xy.x -= scale / 2;
+ xy.y -= scale / 2;
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
if (this.sourceBlock_.RTL) {
- var borderBBox = this.getScaledBBox_();
- xy.x += borderBBox.width;
+ xy.x += width;
xy.x -= div.offsetWidth;
}
// Shift by a few pixels to line up exactly.
- xy.y += 1;
+ xy.y += 1 * scale;
if (goog.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) {
// Firefox mis-reports the location of the border by a pixel
// once the WidgetDiv is moved into position.
- xy.x -= 1;
- xy.y -= 1;
+ xy.x += 2 * scale;
+ xy.y += 1 * scale;
}
if (goog.userAgent.WEBKIT) {
- xy.y -= 3;
+ xy.x += 0.5;
+ xy.y -= 1 * scale;
}
+ // Finally, set the actual style
div.style.left = xy.x + 'px';
div.style.top = xy.y + 'px';
};
/**
- * Close the editor, save the results, and dispose of the editable
- * text field's elements.
+ * Determine border radius based on the type of the owning shadow block.
+ * @return {Number} Border radius in px.
+*/
+Blockly.Field.prototype.getBorderRadius = function() {
+ if (Blockly.FieldTextInput.NUMERIC_FIELD_TYPES.indexOf(this.sourceBlock_.type) > -1) {
+ return Blockly.BlockSvg.NUMBER_FIELD_CORNER_RADIUS;
+ } else {
+ return Blockly.BlockSvg.TEXT_FIELD_CORNER_RADIUS;
+ }
+};
+
+/**
+ * Close the editor, save the results, and start animating the disposal of elements.
* @return {!Function} Closure to call on destruction of the WidgetDiv.
* @private
*/
Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
var thisField = this;
return function() {
+ var div = Blockly.WidgetDiv.DIV;
var htmlInput = Blockly.FieldTextInput.htmlInput_;
// Save the edit (if it validates).
var text = htmlInput.value;
- if (thisField.sourceBlock_ && thisField.changeHandler_) {
- var text1 = thisField.changeHandler_(text);
+ if (thisField.sourceBlock_ && thisField.validator_) {
+ var text1 = thisField.validator_(text);
if (text1 === null) {
// Invalid edit.
text = htmlInput.defaultValue;
} else if (text1 !== undefined) {
- // Change handler has changed the text.
+ // Validation function has changed the text.
text = text1;
}
}
- thisField.setText(text);
+ thisField.setValue(text);
thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
- Blockly.unbindEvent_(htmlInput.onWorkspaceChangeWrapper_);
- Blockly.FieldTextInput.htmlInput_ = null;
+ thisField.workspace_.removeChangeListener(
+ htmlInput.onWorkspaceChangeWrapper_);
+
+ // Animation of disposal
+ htmlInput.style.fontSize = Blockly.FieldTextInput.FONTSIZE_INITIAL + 'pt';
+ div.style.boxShadow = '';
+ div.style.width = Blockly.BlockSvg.FIELD_WIDTH + 'px';
+ div.style.height = Blockly.BlockSvg.FIELD_HEIGHT + 'px';
+ div.style.marginLeft = 0;
+ };
+};
+
+/**
+ * Final disposal of the text field's elements and properties.
+ * @return {!Function} Closure to call on finish animation of the WidgetDiv.
+ * @private
+ */
+Blockly.FieldTextInput.prototype.widgetDisposeAnimationFinished_ = function() {
+ return function() {
// Delete style properties.
var style = Blockly.WidgetDiv.DIV.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
+ // Reset class
+ Blockly.WidgetDiv.DIV.className = 'blocklyWidgetDiv';
+ // Reset transitions
+ Blockly.WidgetDiv.DIV.style.transition = '';
+ Blockly.FieldTextInput.htmlInput_.style.transition = '';
+ Blockly.FieldTextInput.htmlInput_ = null;
};
};
diff --git a/core/field_variable.js b/core/field_variable.js
index a03cfe6ec3..8e5bac9664 100644
--- a/core/field_variable.js
+++ b/core/field_variable.js
@@ -36,15 +36,14 @@ goog.require('goog.string');
* Class for a variable's dropdown field.
* @param {?string} varname The default name for the variable. If null,
* a unique variable name will be generated.
- * @param {Function=} opt_changeHandler A function that is executed when a new
+ * @param {Function=} opt_validator A function that is executed when a new
* option is selected. Its sole argument is the new option value.
* @extends {Blockly.FieldDropdown}
* @constructor
*/
-Blockly.FieldVariable = function(varname, opt_changeHandler) {
+Blockly.FieldVariable = function(varname, opt_validator) {
Blockly.FieldVariable.superClass_.constructor.call(this,
- Blockly.FieldVariable.dropdownCreate, null);
- this.setChangeHandler(opt_changeHandler);
+ Blockly.FieldVariable.dropdownCreate, opt_validator);
this.setValue(varname || '');
};
goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);
@@ -53,7 +52,7 @@ goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);
* Sets a new change handler for angle field.
* @param {Function} handler New change handler, or null.
*/
-Blockly.FieldVariable.prototype.setChangeHandler = function(handler) {
+Blockly.FieldVariable.prototype.setValidator = function(handler) {
var wrappedHandler;
if (handler) {
// Wrap the user's change handler together with the variable rename handler.
@@ -66,7 +65,7 @@ Blockly.FieldVariable.prototype.setChangeHandler = function(handler) {
v1 = value;
}
var v2 = Blockly.FieldVariable.dropdownChange.call(this, v1);
- if (v2 !== undefined) {
+ if (v2 === undefined) {
v2 = v1;
}
}
@@ -75,7 +74,7 @@ Blockly.FieldVariable.prototype.setChangeHandler = function(handler) {
} else {
wrappedHandler = Blockly.FieldVariable.dropdownChange;
}
- Blockly.FieldVariable.superClass_.setChangeHandler.call(this, wrappedHandler);
+ Blockly.FieldVariable.superClass_.setValidator.call(this, wrappedHandler);
};
/**
@@ -83,21 +82,17 @@ Blockly.FieldVariable.prototype.setChangeHandler = function(handler) {
* @param {!Blockly.Block} block The block containing this text.
*/
Blockly.FieldVariable.prototype.init = function(block) {
- if (this.sourceBlock_) {
+ if (this.fieldGroup_) {
// Dropdown has already been initialized once.
return;
}
-
+ Blockly.FieldVariable.superClass_.init.call(this, block);
if (!this.getValue()) {
// Variables without names get uniquely named for this workspace.
- if (block.isInFlyout) {
- var workspace = block.workspace.targetWorkspace;
- } else {
- var workspace = block.workspace;
- }
+ var workspace =
+ block.isInFlyout ? block.workspace.targetWorkspace : block.workspace;
this.setValue(Blockly.Variables.generateUniqueName(workspace));
}
- Blockly.FieldVariable.superClass_.init.call(this, block);
};
/**
@@ -111,11 +106,15 @@ Blockly.FieldVariable.prototype.getValue = function() {
/**
* Set the variable name.
- * @param {string} text New text.
+ * @param {string} newValue New text.
*/
-Blockly.FieldVariable.prototype.setValue = function(text) {
- this.value_ = text;
- this.setText(text);
+Blockly.FieldVariable.prototype.setValue = function(newValue) {
+ if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ this.sourceBlock_, 'field', this.name, this.value_, newValue));
+ }
+ this.value_ = newValue;
+ this.setText(newValue);
};
/**
diff --git a/core/flyout.js b/core/flyout.js
index a3b81de1c6..300d863eaf 100644
--- a/core/flyout.js
+++ b/core/flyout.js
@@ -28,6 +28,7 @@ goog.provide('Blockly.Flyout');
goog.require('Blockly.Block');
goog.require('Blockly.Comment');
+goog.require('Blockly.Events');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
goog.require('goog.events');
@@ -43,12 +44,6 @@ goog.require('goog.userAgent');
Blockly.Flyout = function(workspaceOptions) {
workspaceOptions.getMetrics = this.getMetrics_.bind(this);
workspaceOptions.setMetrics = this.setMetrics_.bind(this);
- /**
- * @type {!Blockly.Workspace}
- * @private
- */
- this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
- this.workspace_.isFlyout = true;
/**
* Is RTL vs LTR.
@@ -56,6 +51,25 @@ Blockly.Flyout = function(workspaceOptions) {
*/
this.RTL = !!workspaceOptions.RTL;
+ /**
+ * Flyout should be laid out horizontally vs vertically.
+ * @type {boolean}
+ */
+ this.horizontalLayout_ = workspaceOptions.horizontalLayout;
+
+ /**
+ * Position of the toolbox and flyout relative to the workspace.
+ * @type {number}
+ */
+ this.toolboxPosition_ = workspaceOptions.toolboxPosition;
+
+ /**
+ * @type {!Blockly.Workspace}
+ * @private
+ */
+ this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
+ this.workspace_.isFlyout = true;
+
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {!Array.}
@@ -77,6 +91,13 @@ Blockly.Flyout = function(workspaceOptions) {
* @private
*/
this.listeners_ = [];
+
+ /**
+ * List of blocks that should always be disabled.
+ * @type {!Array.}
+ * @private
+ */
+ this.permanentlyDisabled_ = [];
};
/**
@@ -90,7 +111,14 @@ Blockly.Flyout.prototype.autoClose = true;
* @type {number}
* @const
*/
-Blockly.Flyout.prototype.CORNER_RADIUS = 8;
+Blockly.Flyout.prototype.CORNER_RADIUS = 0;
+
+/**
+ * Margin of blocks in the flyout.
+ * @type {number}
+ * @const
+ */
+Blockly.Flyout.prototype.BLOCK_MARGIN = 8;
/**
* Top/bottom padding between scrollbar and edge of flyout background.
@@ -113,6 +141,52 @@ Blockly.Flyout.prototype.width_ = 0;
*/
Blockly.Flyout.prototype.height_ = 0;
+/**
+ * Width of flyout contents.
+ * @type {number}
+ * @private
+ */
+Blockly.Flyout.prototype.contentWidth_ = 0;
+
+/**
+ * Height of flyout contents.
+ * @type {number}
+ * @private
+ */
+Blockly.Flyout.prototype.contentHeight_ = 0;
+
+/**
+ * Vertical offset of flyout.
+ * @type {number}
+ * @private
+ */
+Blockly.Flyout.prototype.verticalOffset_ = 0;
+
+/**
+ * Range of a drag angle from a fixed flyout considered "dragging toward workspace."
+ * Drags that are within the bounds of this many degrees from the orthogonal
+ * line to the flyout edge are considered to be "drags toward the workspace."
+ * Example:
+ * Flyout Edge Workspace
+ * [block] / <-within this angle, drags "toward workspace" |
+ * [block] ---- orthogonal to flyout boundary ---- |
+ * [block] \ |
+ * The angle is given in degrees from the orthogonal.
+ * @type {number}
+ * @private
+*/
+Blockly.Flyout.prototype.dragAngleRange_ = 70;
+
+/**
+ * Is the flyout dragging (scrolling)?
+ * 0 - DRAG_NONE - no drag is ongoing or state is undetermined
+ * 1 - DRAG_STICKY - still within the sticky drag radius
+ * 2 - DRAG_FREE - in scroll mode (never create a new block)
+ * @private
+ */
+Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE;
+
+
/**
* Creates the flyout's DOM. Only needs to be called once.
* @return {!Element} The flyout's SVG group.
@@ -141,18 +215,17 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.targetWorkspace_ = targetWorkspace;
this.workspace_.targetWorkspace = targetWorkspace;
// Add scrollbar.
- this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, false, false);
+ this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, this.horizontalLayout_, false);
this.hide();
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.wheel_));
if (!this.autoClose) {
- Array.prototype.push.apply(this.eventWrappers_,
- Blockly.bindEvent_(this.targetWorkspace_.getCanvas(),
- 'blocklyWorkspaceChange', this, this.filterForCapacity_));
+ this.filterWrapper_ = this.filterForCapacity_.bind(this);
+ this.targetWorkspace_.addChangeListener(this.filterWrapper_);
}
- // Dragging the flyout up and down.
+ // Dragging the flyout up and down (or left and right).
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_));
};
@@ -164,6 +237,10 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
Blockly.Flyout.prototype.dispose = function() {
this.hide();
Blockly.unbindEvent_(this.eventWrappers_);
+ if (this.filterWrapper_) {
+ this.targetWorkspace_.removeChangeListener(this.filterWrapper_);
+ this.filterWrapper_ = null;
+ }
if (this.scrollbar_) {
this.scrollbar_.dispose();
this.scrollbar_ = null;
@@ -187,9 +264,12 @@ Blockly.Flyout.prototype.dispose = function() {
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
+ * .contentWidth: Width of the contents,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .absoluteTop: Top-edge of view.
+ * .viewLeft: Offset of the left edge of visible rectangle from parent,
+ * .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteLeft: Left-edge of view.
* @return {Object} Contains size and position metrics of the flyout.
* @private
@@ -199,42 +279,75 @@ Blockly.Flyout.prototype.getMetrics_ = function() {
// Flyout is hidden.
return null;
}
- var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
- var viewWidth = this.width_;
+
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
var optionBox = {height: 0, y: 0};
}
- return {
+
+ var absoluteTop = this.verticalOffset_ + this.SCROLLBAR_PADDING
+ if (this.horizontalLayout_) {
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
+ absoluteTop = 0;
+ }
+ var viewHeight = this.height_;
+ var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
+ } else {
+ var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
+ var viewWidth = this.width_;
+ }
+
+ var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
- contentHeight: (optionBox.height + optionBox.y) * this.workspace_.scale,
+ contentHeight: this.contentHeight_ * this.workspace_.scale,
+ contentWidth: this.contentWidth_ * this.workspace_.scale,
viewTop: -this.workspace_.scrollY,
- contentTop: 0,
- absoluteTop: this.SCROLLBAR_PADDING,
- absoluteLeft: 0
+ viewLeft: -this.workspace_.scrollX,
+ contentTop: optionBox.y,
+ contentLeft: 0,
+ absoluteTop: absoluteTop,
+ absoluteLeft: this.SCROLLBAR_PADDING
};
+ return metrics;
};
/**
- * Sets the Y translation of the flyout to match the scrollbars.
- * @param {!Object} yRatio Contains a y property which is a float
- * between 0 and 1 specifying the degree of scrolling.
+ * Sets the translation of the flyout to match the scrollbars.
+ * @param {!Object} xyRatio Contains a y property which is a float
+ * between 0 and 1 specifying the degree of scrolling and a
+ * similar x property.
* @private
*/
-Blockly.Flyout.prototype.setMetrics_ = function(yRatio) {
+Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
- if (goog.isNumber(yRatio.y)) {
+ if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) {
this.workspace_.scrollY =
- -metrics.contentHeight * yRatio.y - metrics.contentTop;
+ -metrics.contentHeight * xyRatio.y - metrics.contentTop;
+ } else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) {
+ if (this.RTL) {
+ this.workspace_.scrollX =
+ -metrics.contentWidth * xyRatio.x + metrics.contentLeft;
+ } else {
+ this.workspace_.scrollX =
+ -metrics.contentWidth * xyRatio.x - metrics.contentLeft;
+ }
}
- this.workspace_.translate(0, this.workspace_.scrollY + metrics.absoluteTop);
+ var translateX = this.horizontalLayout_ && this.RTL ?
+ metrics.absoluteLeft + metrics.viewWidth - this.workspace_.scrollX :
+ this.workspace_.scrollX + metrics.absoluteLeft;
+ this.workspace_.translate(translateX,
+ this.workspace_.scrollY + metrics.absoluteTop);
+};
+
+Blockly.Flyout.prototype.setVerticalOffset = function(verticalOffset) {
+ this.verticalOffset_ = verticalOffset;
};
/**
@@ -249,47 +362,145 @@ Blockly.Flyout.prototype.position = function() {
// Hidden components will return null.
return;
}
- var edgeWidth = this.width_ - this.CORNER_RADIUS;
- if (this.RTL) {
+ var edgeWidth = this.horizontalLayout_ ? metrics.viewWidth : this.width_;
+ edgeWidth -= this.CORNER_RADIUS;
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
edgeWidth *= -1;
}
- var path = ['M ' + (this.RTL ? this.width_ : 0) + ',0'];
- path.push('h', edgeWidth);
- path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
- this.RTL ? 0 : 1,
- this.RTL ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
- this.CORNER_RADIUS);
- path.push('v', Math.max(0, metrics.viewHeight - this.CORNER_RADIUS * 2));
- path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
- this.RTL ? 0 : 1,
- this.RTL ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
- this.CORNER_RADIUS);
- path.push('h', -edgeWidth);
- path.push('z');
- this.svgBackground_.setAttribute('d', path.join(' '));
+
+ this.setBackgroundPath_(edgeWidth,
+ this.horizontalLayout_ ? this.height_ + this.verticalOffset_ : metrics.viewHeight);
var x = metrics.absoluteLeft;
- if (this.RTL) {
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
x += metrics.viewWidth;
x -= this.width_;
}
+
+ var y = metrics.absoluteTop;
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
+ y += metrics.viewHeight;
+ y -= this.height_;
+ }
+
this.svgGroup_.setAttribute('transform',
- 'translate(' + x + ',' + metrics.absoluteTop + ')');
+ 'translate(' + x + ',' + y + ')');
- // Record the height for Blockly.Flyout.getMetrics_.
- this.height_ = metrics.viewHeight;
+ // Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
+ // horizontal.
+ if (this.horizontalLayout_) {
+ this.width_ = metrics.viewWidth;
+ } else {
+ this.height_ = metrics.viewHeight;
+ }
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
this.scrollbar_.resize();
}
+ // The blocks need to be visible in order to be laid out and measured correctly, but we don't
+ // want the flyout to show up until it's properly sized.
+ // Opacity is set to zero in show().
+ this.svgGroup_.style.opacity = 1;
+};
+
+/**
+ * Create and set the path for the visible boundaries of the flyout.
+ * @param {number} width The width of the flyout, not including the
+ * rounded corners.
+ * @param {number} height The height of the flyout, not including
+ * rounded corners.
+ * @private
+ */
+Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) {
+ if (this.horizontalLayout_) {
+ this.setBackgroundPathHorizontal_(width, height);
+ } else {
+ this.setBackgroundPathVertical_(width, height);
+ }
};
+/**
+ * Create and set the path for the visible boundaries of the toolbox in vertical mode.
+ * @param {number} width The width of the toolbox, not including the
+ * rounded corners.
+ * @param {number} height The height of the toolbox, not including
+ * rounded corners.
+ * @private
+ */
+Blockly.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) {
+ var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT;
+ // Decide whether to start on the left or right.
+ var path = ['M ' + (atRight ? this.width_ : 0) + ',0'];
+ // Top.
+ path.push('h', width);
+ // Rounded corner.
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
+ atRight ? 0 : 1,
+ atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
+ this.CORNER_RADIUS);
+ // Side closest to workspace.
+ path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2));
+ // Rounded corner.
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
+ atRight ? 0 : 1,
+ atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
+ this.CORNER_RADIUS);
+ // Bottom.
+ path.push('h', -width);
+ path.push('z');
+ this.svgBackground_.setAttribute('d', path.join(' '));
+};
+
+/**
+ * Create and set the path for the visible boundaries of the toolbox in horizontal mode.
+ * @param {number} width The width of the toolbox, not including the
+ * rounded corners.
+ * @param {number} height The height of the toolbox, not including
+ * rounded corners.
+ * @private
+ */
+Blockly.Flyout.prototype.setBackgroundPathHorizontal_ = function(width, height) {
+ var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP;
+ // start at top left.
+ var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
+
+ if (atTop) {
+ // top
+ path.push('h', width + this.CORNER_RADIUS);
+ // right
+ path.push('v', height);
+ // bottom
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
+ -this.CORNER_RADIUS, this.CORNER_RADIUS);
+ path.push('h', -1 * (width - this.CORNER_RADIUS));
+ // left
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
+ -this.CORNER_RADIUS, -this.CORNER_RADIUS);
+ path.push('z');
+ } else {
+ // top
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
+ this.CORNER_RADIUS, -this.CORNER_RADIUS);
+ path.push('h', width - this.CORNER_RADIUS);
+ // right
+ path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
+ this.CORNER_RADIUS, this.CORNER_RADIUS);
+ path.push('v', height - this.CORNER_RADIUS);
+ // bottom
+ path.push('h', -width - this.CORNER_RADIUS);
+ // left
+ path.push('z');
+ }
+ this.svgBackground_.setAttribute('d', path.join(' '));
+};
+
+
/**
* Scroll the flyout to the top.
*/
-Blockly.Flyout.prototype.scrollToTop = function() {
- this.scrollbar_.set(0);
+Blockly.Flyout.prototype.scrollToStart = function() {
+ this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? 1000000000 : 0);
};
/**
@@ -298,6 +509,10 @@ Blockly.Flyout.prototype.scrollToTop = function() {
* @private
*/
Blockly.Flyout.prototype.wheel_ = function(e) {
+ // Don't scroll sideways.
+ if (this.horizontalLayout_) {
+ return;
+ }
var delta = e.deltaY;
if (delta) {
if (goog.userAgent.GECKO) {
@@ -338,7 +553,7 @@ Blockly.Flyout.prototype.hide = function() {
}
this.listeners_.length = 0;
if (this.reflowWrapper_) {
- Blockly.unbindEvent_(this.reflowWrapper_);
+ this.workspace_.removeChangeListener(this.reflowWrapper_);
this.reflowWrapper_ = null;
}
// Do NOT delete the blocks here. Wait until Flyout.show.
@@ -352,18 +567,7 @@ Blockly.Flyout.prototype.hide = function() {
*/
Blockly.Flyout.prototype.show = function(xmlList) {
this.hide();
- // Delete any blocks from a previous showing.
- var blocks = this.workspace_.getTopBlocks(false);
- for (var i = 0, block; block = blocks[i]; i++) {
- if (block.workspace == this.workspace_) {
- block.dispose(false, false);
- }
- }
- // Delete any background buttons from a previous showing.
- for (var i = 0, rect; rect = this.buttons_[i]; i++) {
- goog.dom.removeNode(rect);
- }
- this.buttons_.length = 0;
+ this.clearOldBlocks_();
if (xmlList == Blockly.Variables.NAME_TYPE) {
// Special category for variables.
@@ -375,23 +579,73 @@ Blockly.Flyout.prototype.show = function(xmlList) {
Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace);
}
- var margin = this.CORNER_RADIUS;
- this.svgGroup_.style.display = 'block';
+ var margin = this.BLOCK_MARGIN;
// Create the blocks to be shown in this flyout.
var blocks = [];
var gaps = [];
+ this.permanentlyDisabled_.length = 0;
for (var i = 0, xml; xml = xmlList[i]; i++) {
if (xml.tagName && xml.tagName.toUpperCase() == 'BLOCK') {
- var block = Blockly.Xml.domToBlock(
- /** @type {!Blockly.Workspace} */ (this.workspace_), xml);
+ var block = Blockly.Xml.domToBlock(xml, this.workspace_);
+ if (block.disabled) {
+ // Record blocks that were initially disabled.
+ // Do not enable these blocks as a result of capacity filtering.
+ this.permanentlyDisabled_.push(block);
+ }
blocks.push(block);
var gap = parseInt(xml.getAttribute('gap'), 10);
- gaps.push(gap || margin * 3);
+ gaps.push(isNaN(gap) ? margin * 3 : gap);
+ }
+ }
+ // The blocks need to be visible in order to be laid out and measured correctly, but we don't
+ // want the flyout to show up until it's properly sized.
+ // Opacity is reset at the end of position().
+ this.svgGroup_.style.opacity = 0;
+ this.svgGroup_.style.display = 'block';
+ this.layoutBlocks_(blocks, gaps, margin);
+
+ // IE 11 is an incompetant browser that fails to fire mouseout events.
+ // When the mouse is over the background, deselect all blocks.
+ var deselectAll = function(e) {
+ var blocks = this.workspace_.getTopBlocks(false);
+ for (var i = 0, block; block = blocks[i]; i++) {
+ block.removeSelect();
}
+ };
+ this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
+ this, deselectAll));
+
+ if (this.horizontalLayout_) {
+ this.height_ = 0;
+ } else {
+ this.width_ = 0;
}
+ this.reflow();
+
+ this.filterForCapacity_();
+
+ // Fire a resize event to update the flyout's scrollbar.
+ Blockly.svgResize(this.workspace_);
+ this.reflowWrapper_ = this.reflow.bind(this);
+ this.workspace_.addChangeListener(this.reflowWrapper_);
+
+};
- // Lay out the blocks vertically.
- var cursorY = margin;
+/**
+ * Lay out the blocks in the flyout.
+ * @param {!Array.} blocks The blocks to lay out.
+ * @param {!Array.} gaps The visible gaps between blocks.
+ * @param {number} margin The margin around the edges of the flyout.
+ * @private
+ */
+Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps, margin) {
+ // Lay out the blocks.
+ var marginLR = margin / this.workspace_.scale + Blockly.BlockSvg.TAB_WIDTH;
+ var marginTB = margin;
+ var contentWidth = 0;
+ var contentHeight = 0;
+ var cursorX = marginLR
+ var cursorY = marginTB;
for (var i = 0, block; block = blocks[i]; i++) {
var allBlocks = block.getDescendants();
for (var j = 0, child; child = allBlocks[j]; j++) {
@@ -400,110 +654,88 @@ Blockly.Flyout.prototype.show = function(xmlList) {
// block.
child.isInFlyout = true;
}
- block.render();
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
- var x = this.RTL ? 0 : margin / this.workspace_.scale +
- Blockly.BlockSvg.TAB_WIDTH;
- block.moveBy(x, cursorY);
- cursorY += blockHW.height + gaps[i];
+ Blockly.Events.disable();
+ block.moveBy((this.horizontalLayout_ && this.RTL) ? -cursorX : cursorX, cursorY);
+ Blockly.Events.enable();
+
+ // Assuming this is the last block, work out the size of the content
+ // (including margins)
+ contentWidth = cursorX + blockHW.width + marginLR;
+ contentHeight = cursorY + blockHW.height + marginTB;
+
+ if (this.horizontalLayout_) {
+ cursorX += blockHW.width + gaps[i];
+ } else {
+ cursorY += blockHW.height + gaps[i];
+ }
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null);
+ rect.tooltip = block;
+ Blockly.Tooltip.bindMouseEvents(rect);
// Add the rectangles under the blocks, so that the blocks' tooltips work.
this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
block.flyoutRect_ = rect;
this.buttons_[i] = rect;
- if (this.autoClose) {
- this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
- this.createBlockFunc_(block)));
- } else {
- this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
- this.blockMouseDown_(block)));
- }
- this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block,
- block.addSelect));
- this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block,
- block.removeSelect));
- this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null,
- this.createBlockFunc_(block)));
- this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block,
- block.addSelect));
- this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block,
- block.removeSelect));
+ this.addBlockListeners_(root, block, rect);
}
- // IE 11 is an incompetant browser that fails to fire mouseout events.
- // When the mouse is over the background, deselect all blocks.
- var deselectAll = function(e) {
- var blocks = this.workspace_.getTopBlocks(false);
- for (var i = 0, block; block = blocks[i]; i++) {
- block.removeSelect();
- }
- };
- this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
- this, deselectAll));
-
- this.width_ = 0;
- this.reflow();
-
- this.filterForCapacity_();
-
- // Fire a resize event to update the flyout's scrollbar.
- Blockly.fireUiEventNow(window, 'resize');
- this.reflowWrapper_ = Blockly.bindEvent_(this.workspace_.getCanvas(),
- 'blocklyWorkspaceChange', this, this.reflow);
- this.workspace_.fireChangeEvent();
+ // Record calculated content size for getMetrics_
+ this.contentWidth_ = contentWidth;
+ this.contentHeight_ = contentHeight;
};
/**
- * Compute width of flyout. Position button under each block.
- * For RTL: Lay out the blocks right-aligned.
+ * Delete blocks and background buttons from a previous showing of the flyout.
+ * @private
*/
-Blockly.Flyout.prototype.reflow = function() {
- this.workspace_.scale = this.targetWorkspace_.scale;
- var flyoutWidth = 0;
- var margin = this.CORNER_RADIUS;
+Blockly.Flyout.prototype.clearOldBlocks_ = function() {
+ // Delete any blocks from a previous showing.
var blocks = this.workspace_.getTopBlocks(false);
- for (var x = 0, block; block = blocks[x]; x++) {
- var width = block.getHeightWidth().width;
- if (block.outputConnection) {
- width -= Blockly.BlockSvg.TAB_WIDTH;
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.workspace == this.workspace_) {
+ block.dispose(false, false);
}
- flyoutWidth = Math.max(flyoutWidth, width);
}
- flyoutWidth += Blockly.BlockSvg.TAB_WIDTH;
- flyoutWidth *= this.workspace_.scale;
- flyoutWidth += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
- if (this.width_ != flyoutWidth) {
- for (var x = 0, block; block = blocks[x]; x++) {
- var blockHW = block.getHeightWidth();
- if (this.RTL) {
- // With the flyoutWidth known, right-align the blocks.
- var oldX = block.getRelativeToSurfaceXY().x;
- var dx = flyoutWidth - margin;
- dx /= this.workspace_.scale;
- dx -= Blockly.BlockSvg.TAB_WIDTH;
- block.moveBy(dx - oldX, 0);
- }
- if (block.flyoutRect_) {
- block.flyoutRect_.setAttribute('width', blockHW.width);
- block.flyoutRect_.setAttribute('height', blockHW.height);
- // Blocks with output tabs are shifted a bit.
- var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
- var blockXY = block.getRelativeToSurfaceXY();
- block.flyoutRect_.setAttribute('x',
- this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
- block.flyoutRect_.setAttribute('y', blockXY.y);
- }
- }
- // Record the width for .getMetrics_ and .position.
- this.width_ = flyoutWidth;
- // Fire a resize event to update the flyout's scrollbar.
- Blockly.fireUiEvent(window, 'resize');
+ // Delete any background buttons from a previous showing.
+ for (var j = 0, rect; rect = this.buttons_[j]; j++) {
+ goog.dom.removeNode(rect);
+ }
+ this.buttons_.length = 0;
+};
+
+/**
+ * Add listeners to a block that has been added to the flyout.
+ * @param {Element} root The root node of the SVG group the block is in.
+ * @param {!Blockly.Block} block The block to add listeners for.
+ * @param {!Element} rect The invisible rectangle under the block that acts as
+ * a button for that block.
+ * @private
+ */
+Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
+ if (this.autoClose) {
+ this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
+ this.createBlockFunc_(block)));
+ this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null,
+ this.createBlockFunc_(block)));
+ } else {
+ this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
+ this.blockMouseDown_(block)));
+ this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null,
+ this.blockMouseDown_(block)));
}
+ this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block,
+ block.addSelect));
+ this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block,
+ block.removeSelect));
+ this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block,
+ block.addSelect));
+ this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block,
+ block.removeSelect));
};
/**
@@ -515,21 +747,25 @@ Blockly.Flyout.prototype.reflow = function() {
Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
var flyout = this;
return function(e) {
+ flyout.dragMode_ = Blockly.DRAG_NONE;
Blockly.terminateDrag_();
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.hideChaff();
if (Blockly.isRightButton(e)) {
// Right-click.
block.showContextMenu_(e);
} else {
// Left-click (or middle click)
- Blockly.removeAllRanges();
Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
// Record the current mouse position.
+ flyout.startDragMouseY_ = e.clientY;
+ flyout.startDragMouseX_ = e.clientX;
Blockly.Flyout.startDownEvent_ = e;
Blockly.Flyout.startBlock_ = block;
Blockly.Flyout.startFlyout_ = flyout;
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document,
- 'mouseup', this, Blockly.terminateDrag_);
+ 'mouseup', this, flyout.onMouseUp_);
Blockly.Flyout.onMouseMoveBlockWrapper_ = Blockly.bindEvent_(document,
'mousemove', this, flyout.onMouseMoveBlock_);
}
@@ -539,17 +775,21 @@ Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
};
/**
- * Mouse down on the flyout background. Start a vertical scroll drag.
+ * Mouse down on the flyout background. Start a scroll drag.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.Flyout.prototype.onMouseDown_ = function(e) {
+ this.dragMode_ = Blockly.DRAG_FREE;
if (Blockly.isRightButton(e)) {
return;
}
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.hideChaff(true);
Blockly.Flyout.terminateDrag_();
this.startDragMouseY_ = e.clientY;
+ this.startDragMouseX_ = e.clientX;
Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove',
this, this.onMouseMove_);
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup',
@@ -559,19 +799,46 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
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.Flyout.prototype.onMouseUp_ = function(e) {
+ if (Blockly.dragMode_ != Blockly.DRAG_FREE &&
+ !Blockly.WidgetDiv.isVisible()) {
+ Blockly.Events.fire(
+ new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'click',
+ undefined, undefined));
+ }
+ Blockly.terminateDrag_();
+};
+
/**
* Handle a mouse-move to vertically drag the flyout.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.Flyout.prototype.onMouseMove_ = function(e) {
- var dy = e.clientY - this.startDragMouseY_;
- this.startDragMouseY_ = e.clientY;
- var metrics = this.getMetrics_();
- var y = metrics.viewTop - dy;
- y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
- y = Math.max(y, 0);
- this.scrollbar_.set(y);
+ if (this.horizontalLayout_) {
+ var dx = e.clientX - this.startDragMouseX_;
+ this.startDragMouseX_ = e.clientX;
+ var metrics = this.getMetrics_();
+ var x = metrics.viewLeft - dx;
+ x = Math.min(x, metrics.contentWidth - metrics.viewWidth);
+ x = Math.max(x, 0);
+ this.scrollbar_.set(x);
+ } else {
+ var dy = e.clientY - this.startDragMouseY_;
+ this.startDragMouseY_ = e.clientY;
+ var metrics = this.getMetrics_();
+ var y = metrics.viewTop - dy;
+ y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
+ y = Math.max(y, 0);
+ this.scrollbar_.set(y);
+ }
};
/**
@@ -592,15 +859,91 @@ Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) {
e.stopPropagation();
return;
}
- Blockly.removeAllRanges();
var dx = e.clientX - Blockly.Flyout.startDownEvent_.clientX;
var dy = e.clientY - Blockly.Flyout.startDownEvent_.clientY;
- // Still dragging within the sticky DRAG_RADIUS.
- if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
- // Create the block.
+
+ var createBlock = Blockly.Flyout.startFlyout_.determineDragIntention_(dx, dy);
+ if (createBlock) {
Blockly.Flyout.startFlyout_.createBlockFunc_(Blockly.Flyout.startBlock_)(
Blockly.Flyout.startDownEvent_);
+ } else if (Blockly.Flyout.startFlyout_.dragMode_ == Blockly.DRAG_FREE) {
+ // Do a scroll
+ Blockly.Flyout.startFlyout_.onMouseMove_(e);
+ }
+ e.stopPropagation();
+};
+
+/**
+ * Determine the intention of a drag.
+ * Updates dragMode_ based on a drag delta and the current mode,
+ * and returns true if we should create a new block.
+ * @param {number} dx X delta of the drag
+ * @param {number} dy Y delta of the drag
+ * @return {boolean} True if a new block should be created.
+ */
+Blockly.Flyout.prototype.determineDragIntention_ = function(dx, dy) {
+ if (this.dragMode_ == Blockly.DRAG_FREE) {
+ // Once in free mode, always stay in free mode and never create a block.
+ return false;
+ }
+ var dragDistance = Math.sqrt(dx * dx + dy * dy);
+ if (dragDistance < Blockly.DRAG_RADIUS) {
+ // Still within the sticky drag radius
+ this.dragMode_ = Blockly.DRAG_STICKY;
+ return false;
+ } else {
+ if (this.isDragTowardWorkspace_(dx, dy)) {
+ // Immediately create a block
+ return true;
+ } else {
+ // Immediately move to free mode - the drag is away from the workspace.
+ this.dragMode_ = Blockly.DRAG_FREE;
+ return false;
+ }
+ }
+};
+
+/**
+ * Determine if a drag delta is toward the workspace, based on the position
+ * and orientation of the flyout. This is used in onMouseMoveBlock_ to determine
+ * if a new block should be created or if the flyout should scroll.
+ * @param {number} dx X delta of the drag
+ * @param {number} dy Y delta of the drag
+ * @return {boolean} true if the drag is toward the workspace.
+ * @private
+ */
+Blockly.Flyout.prototype.isDragTowardWorkspace_ = function(dx, dy) {
+ // Direction goes from -180 to 180, with 0 toward the right and 90 on top.
+ var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
+
+ var draggingTowardWorkspace = false;
+ var range = Blockly.Flyout.startFlyout_.dragAngleRange_;
+ if (Blockly.Flyout.startFlyout_.horizontalLayout_) {
+ if (Blockly.Flyout.startFlyout_.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
+ // Horizontal at top
+ if (dragDirection < 90 + range && dragDirection > 90 - range ) {
+ draggingTowardWorkspace = true;
+ }
+ } else {
+ // Horizontal at bottom
+ if (dragDirection > -90 - range && dragDirection < -90 + range) {
+ draggingTowardWorkspace = true;
+ }
+ }
+ } else {
+ if (Blockly.Flyout.startFlyout_.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
+ // Vertical at left
+ if (dragDirection < range && dragDirection > -range) {
+ draggingTowardWorkspace = true;
+ }
+ } else {
+ // Vertical at right
+ if (dragDirection < -180 + range || dragDirection > 180 - range) {
+ draggingTowardWorkspace = true;
+ }
+ }
}
+ return draggingTowardWorkspace;
};
/**
@@ -613,6 +956,9 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
var flyout = this;
var workspace = this.targetWorkspace_;
return function(e) {
+ // Hide drop-downs and animating WidgetDiv immediately
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
if (Blockly.isRightButton(e)) {
// Right-click. Don't create a block, let the context menu show.
return;
@@ -621,37 +967,13 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
// Beyond capacity.
return;
}
- // Create the new block by cloning the block in the flyout (via XML).
- var xml = Blockly.Xml.blockToDom_(originBlock);
- var block = Blockly.Xml.domToBlock(workspace, xml);
- // Place it in the same spot as the flyout copy.
- var svgRootOld = originBlock.getSvgRoot();
- if (!svgRootOld) {
- throw 'originBlock is not rendered.';
- }
- var xyOld = Blockly.getSvgXY_(svgRootOld, workspace);
- // Scale the scroll (getSvgXY_ did not do this).
- if (flyout.RTL) {
- var width = workspace.getMetrics().viewWidth - flyout.width_;
- xyOld.x += width / workspace.scale - width;
- } else {
- xyOld.x += flyout.workspace_.scrollX / flyout.workspace_.scale -
- flyout.workspace_.scrollX;
+ Blockly.Events.disable();
+ var block = flyout.placeNewBlock_(originBlock, workspace);
+ Blockly.Events.enable();
+ if (Blockly.Events.isEnabled()) {
+ Blockly.Events.setGroup(true);
+ Blockly.Events.fire(new Blockly.Events.Create(block));
}
- xyOld.y += flyout.workspace_.scrollY / flyout.workspace_.scale -
- flyout.workspace_.scrollY;
- var svgRootNew = block.getSvgRoot();
- if (!svgRootNew) {
- throw 'block is not rendered.';
- }
- var xyNew = Blockly.getSvgXY_(svgRootNew, workspace);
- // Scale the scroll (getSvgXY_ did not do this).
- xyNew.x += workspace.scrollX / workspace.scale - workspace.scrollX;
- xyNew.y += workspace.scrollY / workspace.scale - workspace.scrollY;
- if (workspace.toolbox_ && !workspace.scrollbar) {
- xyNew.x += workspace.toolbox_.width / workspace.scale;
- }
- block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y);
if (flyout.autoClose) {
flyout.hide();
} else {
@@ -659,43 +981,135 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
}
// Start a dragging operation on the new block.
block.onMouseDown_(e);
+ Blockly.dragMode_ = Blockly.DRAG_FREE;
+ block.setDragging_(true);
+ block.moveToDragSurface_();
};
};
+/**
+ * Copy a block from the flyout to the workspace and position it correctly.
+ * @param {!Blockly.Block} originBlock The flyout block to copy.
+ * @param {!Blockly.Workspace} workspace The main workspace.
+ * @return {!Blockly.Block} The new block in the main workspace.
+ * @private
+ */
+Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock, workspace) {
+ var blockSvgOld = originBlock.getSvgRoot();
+ if (!blockSvgOld) {
+ throw 'originBlock is not rendered.';
+ }
+ // Figure out where the original block is on the screen, relative to the upper
+ // left corner of the main workspace.
+ var xyOld = Blockly.getSvgXY_(blockSvgOld, workspace);
+ // Take into account that the flyout might have been scrolled horizontally
+ // (separately from the main workspace).
+ // Generally a no-op in vertical mode but likely to happen in horizontal
+ // mode.
+ var scrollX = this.workspace_.scrollX;
+ var scale = this.workspace_.scale;
+ xyOld.x += scrollX / scale - scrollX;
+ // If the flyout is on the right side, (0, 0) in the flyout is offset to
+ // the right of (0, 0) in the main workspace. Offset to take that into
+ // account.
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
+ scrollX = workspace.getMetrics().viewWidth - this.width_;
+ scale = workspace.scale;
+ // Scale the scroll (getSvgXY_ did not do this).
+ xyOld.x += scrollX / scale - scrollX;
+ }
+
+ // Take into account that the flyout might have been scrolled vertically
+ // (separately from the main workspace).
+ // Generally a no-op in horizontal mode but likely to happen in vertical
+ // mode.
+ var scrollY = this.workspace_.scrollY;
+ scale = this.workspace_.scale;
+ xyOld.y += scrollY / scale - scrollY;
+ // If the flyout is on the bottom, (0, 0) in the flyout is offset to be below
+ // (0, 0) in the main workspace. Offset to take that into account.
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
+ scrollY = workspace.getMetrics().viewHeight - this.height_;
+ scale = workspace.scale;
+ xyOld.y += scrollY / scale - scrollY;
+ }
+
+ // Create the new block by cloning the block in the flyout (via XML).
+ var xml = Blockly.Xml.blockToDom(originBlock);
+ var block = Blockly.Xml.domToBlock(xml, workspace);
+ var blockSvgNew = block.getSvgRoot();
+ if (!blockSvgNew) {
+ throw 'block is not rendered.';
+ }
+ // Figure out where the new block got placed on the screen, relative to the
+ // upper left corner of the workspace. This may not be the same as the
+ // original block because the flyout's origin may not be the same as the
+ // main workspace's origin.
+ var xyNew = Blockly.getSvgXY_(blockSvgNew, workspace);
+ // Scale the scroll (getSvgXY_ did not do this).
+ xyNew.x += workspace.scrollX / workspace.scale - workspace.scrollX;
+ xyNew.y += workspace.scrollY / workspace.scale - workspace.scrollY;
+ // If the flyout is collapsible and the workspace can't be scrolled.
+ if (workspace.toolbox_ && !workspace.scrollbar) {
+ xyNew.x += workspace.toolbox_.width / workspace.scale;
+ xyNew.y += workspace.toolbox_.height / workspace.scale;
+ }
+
+ // Move the new block to where the old block is.
+ block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y);
+ return block;
+};
+
/**
* Filter the blocks on the flyout to disable the ones that are above the
* capacity limit.
* @private
*/
Blockly.Flyout.prototype.filterForCapacity_ = function() {
+ var filtered = false;
var remainingCapacity = this.targetWorkspace_.remainingCapacity();
var blocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
- var allBlocks = block.getDescendants();
- if (allBlocks.length > remainingCapacity) {
- block.setDisabled(true);
+ if (this.permanentlyDisabled_.indexOf(block) == -1) {
+ var allBlocks = block.getDescendants();
+ block.setDisabled(allBlocks.length > remainingCapacity);
+ filtered = true;
}
}
+ if (filtered) {
+ // Top-most block. Fire an event to allow scrollbars to resize.
+ Blockly.asyncSvgResize(this.workspace);
+ }
};
/**
- * Return the deletion rectangle for this flyout.
+ * Return the deletion rectangle for this flyout in viewport coordinates.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
-Blockly.Flyout.prototype.getRect = function() {
+Blockly.Flyout.prototype.getClientRect = function() {
+ var flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
- var mainWorkspace = Blockly.mainWorkspace;
- var x = Blockly.getSvgXY_(this.svgGroup_, mainWorkspace).x;
- if (!this.RTL) {
- x -= BIG_NUM;
+ var x = flyoutRect.left;
+ var y = flyoutRect.top;
+ var width = flyoutRect.width;
+ var height = flyoutRect.height;
+
+ if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
+ return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2,
+ BIG_NUM + height);
+ } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
+ return new goog.math.Rect(-BIG_NUM, y + this.verticalOffset_, BIG_NUM * 2,
+ BIG_NUM + height);
+ } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
+ return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width,
+ BIG_NUM * 2);
+ } else { // Right
+ return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width,
+ BIG_NUM * 2);
}
- // Fix scale if nested in zoomed workspace.
- var scale = this.targetWorkspace_ == mainWorkspace ? 1 : mainWorkspace.scale;
- return new goog.math.Rect(x, -BIG_NUM,
- BIG_NUM + this.width_ * scale, BIG_NUM * 2);
};
/**
@@ -723,3 +1137,97 @@ Blockly.Flyout.terminateDrag_ = function() {
Blockly.Flyout.startBlock_ = null;
Blockly.Flyout.startFlyout_ = null;
};
+
+/**
+ * Compute height of flyout. Position button under each block.
+ * For RTL: Lay out the blocks right-aligned.
+ */
+Blockly.Flyout.prototype.reflowHorizontal = function() {
+ this.workspace_.scale = this.targetWorkspace_.scale;
+ var flyoutHeight = 0;
+ var margin = this.BLOCK_MARGIN;
+ var blocks = this.workspace_.getTopBlocks(false);
+ for (var x = 0, block; block = blocks[x]; x++) {
+ var height = block.getHeightWidth().height;
+ flyoutHeight = Math.max(flyoutHeight, height);
+ }
+ flyoutHeight *= this.workspace_.scale;
+ flyoutHeight += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
+ if (this.height_ != flyoutHeight) {
+ for (var x = 0, block; block = blocks[x]; x++) {
+ var blockHW = block.getHeightWidth();
+ if (block.flyoutRect_) {
+ block.flyoutRect_.setAttribute('width', blockHW.width);
+ block.flyoutRect_.setAttribute('height', blockHW.height);
+ // Blocks with output tabs are shifted a bit.
+ var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
+ var blockXY = block.getRelativeToSurfaceXY();
+ block.flyoutRect_.setAttribute('y', blockXY.y);
+ block.flyoutRect_.setAttribute('x',
+ this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
+ }
+ }
+ // Record the width for .getMetrics_ and .position.
+ this.height_ = flyoutHeight;
+ // Top-most block. Fire an event to allow scrollbars to resize.
+ Blockly.asyncSvgResize(this.workspace);
+ }
+};
+
+/**
+ * Compute width of flyout. Position button under each block.
+ * For RTL: Lay out the blocks right-aligned.
+ */
+Blockly.Flyout.prototype.reflowVertical = function() {
+ this.workspace_.scale = this.targetWorkspace_.scale;
+ var flyoutWidth = 0;
+ var margin = this.BLOCK_MARGIN;
+ var blocks = this.workspace_.getTopBlocks(false);
+ for (var x = 0, block; block = blocks[x]; x++) {
+ var width = block.getHeightWidth().width;
+ if (block.outputConnection) {
+ width -= Blockly.BlockSvg.TAB_WIDTH;
+ }
+ flyoutWidth = Math.max(flyoutWidth, width);
+ }
+ flyoutWidth += Blockly.BlockSvg.TAB_WIDTH;
+ flyoutWidth *= this.workspace_.scale;
+ flyoutWidth += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
+ if (this.width_ != flyoutWidth) {
+ for (var x = 0, block; block = blocks[x]; x++) {
+ var blockHW = block.getHeightWidth();
+ if (this.RTL) {
+ // With the flyoutWidth known, right-align the blocks.
+ var oldX = block.getRelativeToSurfaceXY().x;
+ var dx = flyoutWidth - margin;
+ dx /= this.workspace_.scale;
+ dx -= Blockly.BlockSvg.TAB_WIDTH;
+ block.moveBy(dx - oldX, 0);
+ }
+ if (block.flyoutRect_) {
+ block.flyoutRect_.setAttribute('width', blockHW.width);
+ block.flyoutRect_.setAttribute('height', blockHW.height);
+ // Blocks with output tabs are shifted a bit.
+ var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
+ var blockXY = block.getRelativeToSurfaceXY();
+ block.flyoutRect_.setAttribute('x',
+ this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
+ block.flyoutRect_.setAttribute('y', blockXY.y);
+ }
+ }
+ // Record the width for .getMetrics_ and .position.
+ this.width_ = flyoutWidth;
+ // Top-most block. Fire an event to allow scrollbars to resize.
+ Blockly.asyncSvgResize(this.workspace);
+ }
+};
+
+Blockly.Flyout.prototype.reflow = function() {
+ Blockly.Events.disable();
+ if (this.horizontalLayout_) {
+ this.reflowHorizontal();
+ } else {
+ this.reflowVertical();
+ }
+ Blockly.Events.enable();
+};
diff --git a/core/generator.js b/core/generator.js
index 082101b402..48378cd468 100644
--- a/core/generator.js
+++ b/core/generator.js
@@ -63,6 +63,13 @@ Blockly.Generator.prototype.INFINITE_LOOP_TRAP = null;
*/
Blockly.Generator.prototype.STATEMENT_PREFIX = null;
+/**
+ * The method of indenting. Defaults to two spaces, but language generators
+ * may override this to increase indent or change to tabs.
+ * @type {string}
+ */
+Blockly.Generator.prototype.INDENT = ' ';
+
/**
* Generate code for all blocks in the workspace to the specified language.
* @param {Blockly.Workspace} workspace Workspace to generate code from.
@@ -163,6 +170,8 @@ Blockly.Generator.prototype.blockToCode = function(block) {
var code = func.call(block, block);
if (goog.isArray(code)) {
// Value blocks return tuples of code and operator order.
+ goog.asserts.assert(block.outputConnection,
+ 'Expecting string from statement block "%s".', block.type);
return [this.scrub_(block, code[0]), code[1]];
} else if (goog.isString(code)) {
if (this.STATEMENT_PREFIX) {
@@ -265,13 +274,6 @@ Blockly.Generator.prototype.addLoopTrap = function(branch, id) {
return branch;
};
-/**
- * The method of indenting. Defaults to two spaces, but language generators
- * may override this to increase indent or change to tabs.
- * @type {string}
- */
-Blockly.Generator.prototype.INDENT = ' ';
-
/**
* Comma-separated list of reserved words.
* @type {string}
@@ -310,7 +312,7 @@ Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
* The code gets output when Blockly.Generator.finish() is called.
*
* @param {string} desiredName The desired name of the function (e.g., isPrime).
- * @param {!Array.} code A list of Python statements.
+ * @param {!Array.} code A list of statements. Use ' ' for indents.
* @return {string} The actual name of the new function. This may differ
* from desiredName if the former has already been taken by the user.
* @private
@@ -320,8 +322,15 @@ Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) {
var functionName =
this.variableDB_.getDistinctName(desiredName, this.NAME_TYPE);
this.functionNames_[desiredName] = functionName;
- this.definitions_[desiredName] = code.join('\n').replace(
+ var codeText = code.join('\n').replace(
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName);
+ // Change all ' ' indents into the desired indent.
+ var oldCodeText;
+ while (oldCodeText != codeText) {
+ oldCodeText = codeText;
+ codeText = codeText.replace(/^(( )*) /gm, '$1' + this.INDENT);
+ }
+ this.definitions_[desiredName] = codeText;
}
return this.functionNames_[desiredName];
};
diff --git a/core/icon.js b/core/icon.js
index d9d041dc75..9d2cbeaad9 100644
--- a/core/icon.js
+++ b/core/icon.js
@@ -27,6 +27,7 @@
goog.provide('Blockly.Icon');
goog.require('goog.dom');
+goog.require('goog.math.Coordinate');
/**
@@ -56,16 +57,11 @@ Blockly.Icon.prototype.SIZE = 17;
Blockly.Icon.prototype.bubble_ = null;
/**
- * Absolute X coordinate of icon's center.
+ * Absolute coordinate of icon's center.
+ * @type {goog.math.Coordinate}
* @private
*/
-Blockly.Icon.prototype.iconX_ = 0;
-
-/**
- * Absolute Y coordinate of icon's centre.
- * @private
- */
-Blockly.Icon.prototype.iconY_ = 0;
+Blockly.Icon.prototype.iconXY_ = null;
/**
* Create the icon on the block.
@@ -128,7 +124,7 @@ Blockly.Icon.prototype.isVisible = function() {
* @private
*/
Blockly.Icon.prototype.iconClick_ = function(e) {
- if (Blockly.dragMode_ == 2) {
+ if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
// Drag operation is concluding. Don't open the editor.
return;
}
@@ -176,14 +172,12 @@ Blockly.Icon.prototype.renderIcon = function(cursorX) {
/**
* Notification that the icon has moved. Update the arrow accordingly.
- * @param {number} x Absolute horizontal location.
- * @param {number} y Absolute vertical location.
+ * @param {!goog.math.Coordinate} xy Absolute location.
*/
-Blockly.Icon.prototype.setIconLocation = function(x, y) {
- this.iconX_ = x;
- this.iconY_ = y;
+Blockly.Icon.prototype.setIconLocation = function(xy) {
+ this.iconXY_ = xy;
if (this.isVisible()) {
- this.bubble_.setAnchorLocation(x, y);
+ this.bubble_.setAnchorLocation(xy);
}
};
@@ -195,17 +189,18 @@ Blockly.Icon.prototype.computeIconLocation = function() {
// Find coordinates for the centre of the icon and update the arrow.
var blockXY = this.block_.getRelativeToSurfaceXY();
var iconXY = Blockly.getRelativeXY_(this.iconGroup_);
- var newX = blockXY.x + iconXY.x + this.SIZE / 2;
- var newY = blockXY.y + iconXY.y + this.SIZE / 2;
- if (newX !== this.iconX_ || newY !== this.iconY_) {
- this.setIconLocation(newX, newY);
+ var newXY = new goog.math.Coordinate(
+ blockXY.x + iconXY.x + this.SIZE / 2,
+ blockXY.y + iconXY.y + this.SIZE / 2);
+ if (!goog.math.Coordinate.equals(this.getIconLocation(), newXY)) {
+ this.setIconLocation(newXY);
}
};
/**
* Returns the center of the block's icon relative to the surface.
- * @return {!Object} Object with x and y properties.
+ * @return {!goog.math.Coordinate} Object with x and y properties.
*/
Blockly.Icon.prototype.getIconLocation = function() {
- return {x: this.iconX_, y: this.iconY_};
+ return this.iconXY_;
};
diff --git a/core/inject.js b/core/inject.js
index 6c807cfff2..71fde36177 100644
--- a/core/inject.js
+++ b/core/inject.js
@@ -27,192 +27,59 @@
goog.provide('Blockly.inject');
goog.require('Blockly.Css');
+goog.require('Blockly.Options');
goog.require('Blockly.WorkspaceSvg');
+goog.require('Blockly.DragSurfaceSvg');
+goog.require('Blockly.DropDownDiv');
goog.require('goog.dom');
goog.require('goog.ui.Component');
goog.require('goog.userAgent');
+/**
+ * Radius of stack glow, in px.
+ * @type {number}
+ * @const
+ */
+Blockly.STACK_GLOW_RADIUS = 1;
/**
* Inject a Blockly editor into the specified container element (usually a div).
- * @param {!Element|string} container Containing element or its ID.
+ * @param {!Element|string} container Containing element, or its ID,
+ * or a CSS selector.
* @param {Object=} opt_options Optional dictionary of options.
* @return {!Blockly.Workspace} Newly created main workspace.
*/
Blockly.inject = function(container, opt_options) {
if (goog.isString(container)) {
- container = document.getElementById(container);
+ container = document.getElementById(container) ||
+ document.querySelector(container);
}
// Verify that the container is in document.
if (!goog.dom.contains(document, container)) {
throw 'Error: container is not in current document.';
}
- var options = Blockly.parseOptions_(opt_options || {});
- var svg = Blockly.createDom_(container, options);
- var workspace = Blockly.createMainWorkspace_(svg, options);
+ var options = new Blockly.Options(opt_options || {});
+
+ // Add the relative wrapper. This is for positioning the drag surface exactly
+ // on top of the blockly SVG. Without this, top positioning of the drag surface
+ // is always off by a few pixels.
+ var relativeWrapper = goog.dom.createDom('div', 'blocklyRelativeWrapper');
+ container.appendChild(relativeWrapper);
+
+ var svg = Blockly.createDom_(relativeWrapper, options);
+ var dragSurface = new Blockly.DragSurfaceSvg(relativeWrapper);
+ dragSurface.createDom();
+ var workspace = Blockly.createMainWorkspace_(svg, options, dragSurface);
Blockly.init_(workspace);
workspace.markFocused();
Blockly.bindEvent_(svg, 'focus', workspace, workspace.markFocused);
return workspace;
};
-/**
- * Parse the provided toolbox tree into a consistent DOM format.
- * @param {Node|string} tree DOM tree of blocks, or text representation of same.
- * @return {Node} DOM tree of blocks, or null.
- * @private
- */
-Blockly.parseToolboxTree_ = function(tree) {
- if (tree) {
- if (typeof tree != 'string') {
- if (typeof XSLTProcessor == 'undefined' && tree.outerHTML) {
- // In this case the tree will not have been properly built by the
- // browser. The HTML will be contained in the element, but it will
- // not have the proper DOM structure since the browser doesn't support
- // XSLTProcessor (XML -> HTML). This is the case in IE 9+.
- tree = tree.outerHTML;
- } else if (!(tree instanceof Element)) {
- tree = null;
- }
- }
- if (typeof tree == 'string') {
- tree = Blockly.Xml.textToDom(tree);
- }
- } else {
- tree = null;
- }
- return tree;
-};
-
-/**
- * Configure Blockly to behave according to a set of options.
- * @param {!Object} options Dictionary of options. Specification:
- * https://developers.google.com/blockly/installation/overview#configuration
- * @return {!Object} Dictionary of normalized options.
- * @private
- */
-Blockly.parseOptions_ = function(options) {
- var readOnly = !!options['readOnly'];
- if (readOnly) {
- var languageTree = null;
- var hasCategories = false;
- var hasTrashcan = false;
- var hasCollapse = false;
- var hasComments = false;
- var hasDisable = false;
- var hasSounds = false;
- } else {
- var languageTree = Blockly.parseToolboxTree_(options['toolbox']);
- var hasCategories = Boolean(languageTree &&
- languageTree.getElementsByTagName('category').length);
- var hasTrashcan = options['trashcan'];
- if (hasTrashcan === undefined) {
- hasTrashcan = hasCategories;
- }
- var hasCollapse = options['collapse'];
- if (hasCollapse === undefined) {
- hasCollapse = hasCategories;
- }
- var hasComments = options['comments'];
- if (hasComments === undefined) {
- hasComments = hasCategories;
- }
- var hasDisable = options['disable'];
- if (hasDisable === undefined) {
- hasDisable = hasCategories;
- }
- var hasSounds = options['sounds'];
- if (hasSounds === undefined) {
- hasSounds = true;
- }
- }
- var hasScrollbars = options['scrollbars'];
- if (hasScrollbars === undefined) {
- hasScrollbars = hasCategories;
- }
- var hasCss = options['css'];
- if (hasCss === undefined) {
- hasCss = true;
- }
- // See grid documentation at:
- // https://developers.google.com/blockly/installation/grid
- var grid = options['grid'] || {};
- var gridOptions = {};
- gridOptions.spacing = parseFloat(grid['spacing']) || 0;
- gridOptions.colour = grid['colour'] || '#888';
- gridOptions.length = parseFloat(grid['length']) || 1;
- gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap'];
- var pathToMedia = 'https://blockly-demo.appspot.com/static/media/';
- if (options['media']) {
- pathToMedia = options['media'];
- } else if (options['path']) {
- // 'path' is a deprecated option which has been replaced by 'media'.
- pathToMedia = options['path'] + 'media/';
- }
-
- // See zoom documentation at:
- // https://developers.google.com/blockly/installation/zoom
- var zoom = options['zoom'] || {};
- var zoomOptions = {};
- if (zoom['controls'] === undefined) {
- zoomOptions.controls = false;
- } else {
- zoomOptions.controls = !!zoom['controls'];
- }
- if (zoom['wheel'] === undefined) {
- zoomOptions.wheel = false;
- } else {
- zoomOptions.wheel = !!zoom['wheel'];
- }
- if (zoom['startScale'] === undefined) {
- zoomOptions.startScale = 1;
- } else {
- zoomOptions.startScale = parseFloat(zoom['startScale']);
- }
- if (zoom['maxScale'] === undefined) {
- zoomOptions.maxScale = 3;
- } else {
- zoomOptions.maxScale = parseFloat(zoom['maxScale']);
- }
- if (zoom['minScale'] === undefined) {
- zoomOptions.minScale = 0.3;
- } else {
- zoomOptions.minScale = parseFloat(zoom['minScale']);
- }
- if (zoom['scaleSpeed'] === undefined) {
- zoomOptions.scaleSpeed = 1.2;
- } else {
- zoomOptions.scaleSpeed = parseFloat(zoom['scaleSpeed']);
- }
-
- var enableRealtime = !!options['realtime'];
- var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined;
-
- return {
- RTL: !!options['rtl'],
- collapse: hasCollapse,
- comments: hasComments,
- disable: hasDisable,
- readOnly: readOnly,
- maxBlocks: options['maxBlocks'] || Infinity,
- pathToMedia: pathToMedia,
- hasCategories: hasCategories,
- hasScrollbars: hasScrollbars,
- hasTrashcan: hasTrashcan,
- hasSounds: hasSounds,
- hasCss: hasCss,
- languageTree: languageTree,
- gridOptions: gridOptions,
- zoomOptions: zoomOptions,
- enableRealtime: enableRealtime,
- realtimeOptions: realtimeOptions
- };
-};
-
/**
* Create the SVG image.
* @param {!Element} container Containing element.
- * @param {Object} options Dictionary of options.
+ * @param {!Blockly.Options} options Dictionary of options.
* @return {!Element} Newly created SVG image.
* @private
*/
@@ -228,16 +95,6 @@ Blockly.createDom_ = function(container, options) {
Blockly.Css.inject(options.hasCss, options.pathToMedia);
// Build the SVG DOM.
- /*
-
- ...
-
- */
var svg = Blockly.createSvgElement('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:html': 'http://www.w3.org/1999/xhtml',
@@ -245,51 +102,31 @@ Blockly.createDom_ = function(container, options) {
'version': '1.1',
'class': 'blocklySvg'
}, container);
- /*
-
- ... filters go here ...
-
- */
var defs = Blockly.createSvgElement('defs', {}, svg);
var rnd = String(Math.random()).substring(2);
- /*
-
-
-
-
-
-
-
-
- */
- var embossFilter = Blockly.createSvgElement('filter',
- {'id': 'blocklyEmbossFilter' + rnd}, defs);
+
+ // Using a dilate distorts the block shape.
+ // Instead use a gaussian blur, and then set all alpha to 1 with a transfer.
+ var stackGlowFilter = Blockly.createSvgElement('filter',
+ {'id': 'blocklyStackGlowFilter',
+ 'height': '160%', 'width': '180%', y: '-30%', x: '-40%'}, defs);
Blockly.createSvgElement('feGaussianBlur',
- {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter);
- var feSpecularLighting = Blockly.createSvgElement('feSpecularLighting',
- {'in': 'blur', 'surfaceScale': 1, 'specularConstant': 0.5,
- 'specularExponent': 10, 'lighting-color': 'white', 'result': 'specOut'},
- embossFilter);
- Blockly.createSvgElement('fePointLight',
- {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting);
+ {'in': 'SourceGraphic',
+ 'stdDeviation': Blockly.STACK_GLOW_RADIUS}, stackGlowFilter);
+ // Set all gaussian blur pixels to 1 opacity before applying flood
+ var componentTransfer = Blockly.createSvgElement('feComponentTransfer', {'result': 'outBlur'}, stackGlowFilter);
+ Blockly.createSvgElement('feFuncA',
+ {'type': 'table', 'tableValues': '0' + ' 1'.repeat(16)}, componentTransfer);
+ // Color the highlight
+ Blockly.createSvgElement('feFlood',
+ {'flood-color': Blockly.Colours.stackGlow,
+ 'flood-opacity': Blockly.Colours.stackGlowOpacity, 'result': 'outColor'}, stackGlowFilter);
Blockly.createSvgElement('feComposite',
- {'in': 'specOut', 'in2': 'SourceAlpha', 'operator': 'in',
- 'result': 'specOut'}, embossFilter);
+ {'in': 'outColor', 'in2': 'outBlur',
+ 'operator': 'in', 'result': 'outGlow'}, stackGlowFilter);
Blockly.createSvgElement('feComposite',
- {'in': 'SourceGraphic', 'in2': 'specOut', 'operator': 'arithmetic',
- 'k1': 0, 'k2': 1, 'k3': 1, 'k4': 0}, embossFilter);
- options.embossFilterId = embossFilter.id;
- /*
-
-
-
-
- */
+ {'in': 'SourceGraphic', 'in2': 'outGlow', 'operator': 'over'}, stackGlowFilter);
+
var disabledPattern = Blockly.createSvgElement('pattern',
{'id': 'blocklyDisabledPattern' + rnd,
'patternUnits': 'userSpaceOnUse',
@@ -299,12 +136,7 @@ Blockly.createDom_ = function(container, options) {
Blockly.createSvgElement('path',
{'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern);
options.disabledPatternId = disabledPattern.id;
- /*
-
-
-
-
- */
+
var gridPattern = Blockly.createSvgElement('pattern',
{'id': 'blocklyGridPattern' + rnd,
'patternUnits': 'userSpaceOnUse'}, defs);
@@ -326,15 +158,16 @@ Blockly.createDom_ = function(container, options) {
/**
* Create a main workspace and add it to the SVG.
* @param {!Element} svg SVG element with pattern defined.
- * @param {Object} options Dictionary of options.
+ * @param {!Blockly.Options} options Dictionary of options.
+ * @param {!Blockly.DragSurfaceSvg} dragSurface Drag surface SVG for the workspace.
* @return {!Blockly.Workspace} Newly created main workspace.
* @private
*/
-Blockly.createMainWorkspace_ = function(svg, options) {
+Blockly.createMainWorkspace_ = function(svg, options, dragSurface) {
options.parentWorkspace = null;
options.getMetrics = Blockly.getMainWorkspaceMetrics_;
options.setMetrics = Blockly.setMainWorkspaceMetrics_;
- var mainWorkspace = new Blockly.WorkspaceSvg(options);
+ var mainWorkspace = new Blockly.WorkspaceSvg(options, dragSurface);
mainWorkspace.scale = options.zoomOptions.startScale;
svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
// A null translation will also apply the correct initial scale.
@@ -343,7 +176,7 @@ Blockly.createMainWorkspace_ = function(svg, options) {
if (!options.readOnly && !options.hasScrollbars) {
var workspaceChanged = function() {
- if (Blockly.dragMode_ == 0) {
+ if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
var metrics = mainWorkspace.getMetrics();
var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
var edgeTop = metrics.viewTop + metrics.absoluteTop;
@@ -361,26 +194,27 @@ Blockly.createMainWorkspace_ = function(svg, options) {
var blockXY = block.getRelativeToSurfaceXY();
var blockHW = block.getHeightWidth();
// Bump any block that's above the top back inside.
- var overflow = edgeTop + MARGIN - blockHW.height - blockXY.y;
- if (overflow > 0) {
- block.moveBy(0, overflow);
+ var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y;
+ if (overflowTop > 0) {
+ block.moveBy(0, overflowTop);
}
// Bump any block that's below the bottom back inside.
- var overflow = edgeTop + metrics.viewHeight - MARGIN - blockXY.y;
- if (overflow < 0) {
- block.moveBy(0, overflow);
+ var overflowBottom =
+ edgeTop + metrics.viewHeight - MARGIN - blockXY.y;
+ if (overflowBottom < 0) {
+ block.moveBy(0, overflowBottom);
}
// Bump any block that's off the left back inside.
- var overflow = MARGIN + edgeLeft -
+ var overflowLeft = MARGIN + edgeLeft -
blockXY.x - (options.RTL ? 0 : blockHW.width);
- if (overflow > 0) {
- block.moveBy(overflow, 0);
+ if (overflowLeft > 0) {
+ block.moveBy(overflowLeft, 0);
}
// Bump any block that's off the right back inside.
- var overflow = edgeLeft + metrics.viewWidth - MARGIN -
+ var overflowRight = edgeLeft + metrics.viewWidth - MARGIN -
blockXY.x + (options.RTL ? blockHW.width : 0);
- if (overflow < 0) {
- block.moveBy(overflow, 0);
+ if (overflowRight < 0) {
+ block.moveBy(overflowRight, 0);
}
}
}
@@ -391,6 +225,7 @@ Blockly.createMainWorkspace_ = function(svg, options) {
// The SVG is now fully assembled.
Blockly.svgResize(mainWorkspace);
Blockly.WidgetDiv.createDom();
+ Blockly.DropDownDiv.createDom();
Blockly.Tooltip.createDom();
return mainWorkspace;
};
@@ -403,6 +238,7 @@ Blockly.createMainWorkspace_ = function(svg, options) {
Blockly.init_ = function(mainWorkspace) {
var options = mainWorkspace.options;
var svg = mainWorkspace.getParentSvg();
+
// Supress the browser's context menu.
Blockly.bindEvent_(svg, 'contextmenu', null,
function(e) {
@@ -410,34 +246,14 @@ Blockly.init_ = function(mainWorkspace) {
e.preventDefault();
}
});
- // Bind events for scrolling the workspace.
- // Most of these events should be bound to the SVG's surface.
- // However, 'mouseup' has to be on the whole document so that a block dragged
- // out of bounds and released will know that it has been released.
- // Also, 'keydown' has to be on the whole document since the browser doesn't
- // understand a concept of focus on the SVG image.
Blockly.bindEvent_(window, 'resize', null,
- function() {Blockly.svgResize(mainWorkspace);});
-
- if (!Blockly.documentEventsBound_) {
- // Only bind the window/document events once.
- // Destroying and reinjecting Blockly should not bind again.
- Blockly.bindEvent_(document, 'keydown', null, Blockly.onKeyDown_);
- Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_);
- Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_);
- // Don't use bindEvent_ for document's mouseup since that would create a
- // corresponding touch handler that would squeltch the ability to interact
- // with non-Blockly elements.
- document.addEventListener('mouseup', Blockly.onMouseUp_, false);
- // Some iPad versions don't fire resize after portrait to landscape change.
- if (goog.userAgent.IPAD) {
- Blockly.bindEvent_(window, 'orientationchange', document, function() {
- Blockly.fireUiEvent(window, 'resize');
+ function() {
+ Blockly.hideChaff(true);
+ Blockly.asyncSvgResize(mainWorkspace);
});
- }
- Blockly.documentEventsBound_ = true;
- }
+
+ Blockly.inject.bindDocumentEvents_();
if (options.languageTree) {
if (mainWorkspace.toolbox_) {
@@ -446,16 +262,22 @@ Blockly.init_ = function(mainWorkspace) {
// Build a fixed flyout with the root blocks.
mainWorkspace.flyout_.init(mainWorkspace);
mainWorkspace.flyout_.show(options.languageTree.childNodes);
- // Translate the workspace sideways to avoid the fixed flyout.
- mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
- if (options.RTL) {
- mainWorkspace.scrollX *= -1;
+ // Translate the workspace to avoid the fixed flyout.
+ if (options.horizontalLayout) {
+ mainWorkspace.scrollY = mainWorkspace.flyout_.height_;
+ if (options.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
+ mainWorkspace.scrollY *= -1;
+ }
+ } else {
+ mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
+ if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
+ mainWorkspace.scrollX *= -1;
+ }
}
- var translation = 'translate(' + mainWorkspace.scrollX + ',0)';
- mainWorkspace.getCanvas().setAttribute('transform', translation);
- mainWorkspace.getBubbleCanvas().setAttribute('transform', translation);
+ mainWorkspace.translate(mainWorkspace.scrollX, mainWorkspace.scrollY);
}
}
+
if (options.hasScrollbars) {
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
mainWorkspace.scrollbar.resize();
@@ -464,17 +286,9 @@ Blockly.init_ = function(mainWorkspace) {
// Load the sounds.
if (options.hasSounds) {
mainWorkspace.loadAudio_(
- [options.pathToMedia + 'click.mp3',
- options.pathToMedia + 'click.wav',
- options.pathToMedia + 'click.ogg'], 'click');
- mainWorkspace.loadAudio_(
- [options.pathToMedia + 'disconnect.wav',
- options.pathToMedia + 'disconnect.mp3',
- options.pathToMedia + 'disconnect.ogg'], 'disconnect');
+ [options.pathToMedia + 'click.wav'], 'click');
mainWorkspace.loadAudio_(
- [options.pathToMedia + 'delete.mp3',
- options.pathToMedia + 'delete.ogg',
- options.pathToMedia + 'delete.wav'], 'delete');
+ [options.pathToMedia + 'delete.wav'], 'delete');
// Bind temporary hooks that preload the sounds.
var soundBinds = [];
@@ -492,6 +306,36 @@ Blockly.init_ = function(mainWorkspace) {
}
};
+/**
+ * Bind document events, but only once. Destroying and reinjecting Blockly
+ * should not bind again.
+ * Bind events for scrolling the workspace.
+ * Most of these events should be bound to the SVG's surface.
+ * However, 'mouseup' has to be on the whole document so that a block dragged
+ * out of bounds and released will know that it has been released.
+ * Also, 'keydown' has to be on the whole document since the browser doesn't
+ * understand a concept of focus on the SVG image.
+ * @private
+ */
+Blockly.inject.bindDocumentEvents_ = function() {
+ if (!Blockly.documentEventsBound_) {
+ Blockly.bindEvent_(document, 'keydown', null, Blockly.onKeyDown_);
+ Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_);
+ Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_);
+ // Don't use bindEvent_ for document's mouseup since that would create a
+ // corresponding touch handler that would squeltch the ability to interact
+ // with non-Blockly elements.
+ document.addEventListener('mouseup', Blockly.onMouseUp_, false);
+ // Some iPad versions don't fire resize after portrait to landscape change.
+ if (goog.userAgent.IPAD) {
+ Blockly.bindEvent_(window, 'orientationchange', document, function() {
+ Blockly.asyncSvgResize();
+ });
+ }
+ }
+ Blockly.documentEventsBound_ = true;
+};
+
/**
* Modify the block tree on the existing toolbox.
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
diff --git a/core/input.js b/core/input.js
index b924337699..183d76c73c 100644
--- a/core/input.js
+++ b/core/input.js
@@ -47,7 +47,10 @@ Blockly.Input = function(type, name, block, connection) {
this.type = type;
/** @type {string} */
this.name = name;
- /** @type {!Blockly.Block} */
+ /**
+ * @type {!Blockly.Block}
+ * @private
+ */
this.sourceBlock_ = block;
/** @type {Blockly.Connection} */
this.connection = connection;
@@ -84,8 +87,9 @@ Blockly.Input.prototype.appendField = function(field, opt_name) {
if (goog.isString(field)) {
field = new Blockly.FieldLabel(/** @type {string} */ (field));
}
+ field.setSourceBlock(this.sourceBlock_);
if (this.sourceBlock_.rendered) {
- field.init(this.sourceBlock_);
+ field.init();
}
field.name = opt_name;
@@ -108,19 +112,6 @@ Blockly.Input.prototype.appendField = function(field, opt_name) {
return this;
};
-/**
- * Add an item to the end of the input's field row.
- * @param {*} field Something to add as a field.
- * @param {string=} opt_name Language-neutral identifier which may used to find
- * this field again. Should be unique to the host block.
- * @return {!Blockly.Input} The input being append to (to allow chaining).
- * @deprecated December 2013
- */
-Blockly.Input.prototype.appendTitle = function(field, opt_name) {
- console.warn('Deprecated call to appendTitle, use appendField instead.');
- return this.appendField(field, opt_name);
-};
-
/**
* Remove a field from this input.
* @param {string} name The name of the field.
diff --git a/core/mutator.js b/core/mutator.js
index 13d492a1e0..a53babfd98 100644
--- a/core/mutator.js
+++ b/core/mutator.js
@@ -198,11 +198,13 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
// No change.
return;
}
+ Blockly.Events.fire(
+ new Blockly.Events.Ui(this.block_, 'mutatorOpen', !visible, visible));
if (visible) {
// Create the bubble.
- this.bubble_ = new Blockly.Bubble(this.block_.workspace,
- this.createEditor_(), this.block_.svgPath_,
- this.iconX_, this.iconY_, null, null);
+ this.bubble_ = new Blockly.Bubble(
+ /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
+ this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null);
var tree = this.workspace_.options.languageTree;
if (tree) {
this.workspace_.flyout_.init(this.workspace_);
@@ -232,17 +234,14 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
if (this.block_.saveConnections) {
var thisMutator = this;
this.block_.saveConnections(this.rootBlock_);
- this.sourceListener_ = Blockly.bindEvent_(
- this.block_.workspace.getCanvas(),
- 'blocklyWorkspaceChange', null,
- function() {
- thisMutator.block_.saveConnections(thisMutator.rootBlock_)
- });
+ this.sourceListener_ = function() {
+ thisMutator.block_.saveConnections(thisMutator.rootBlock_);
+ };
+ this.block_.workspace.addChangeListener(this.sourceListener_);
}
this.resizeBubble_();
// When the mutator's workspace changes, update the source block.
- Blockly.bindEvent_(this.workspace_.getCanvas(), 'blocklyWorkspaceChange',
- this, this.workspaceChanged_);
+ this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));
this.updateColour();
} else {
// Dispose of the bubble.
@@ -255,7 +254,7 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
this.workspaceWidth_ = 0;
this.workspaceHeight_ = 0;
if (this.sourceListener_) {
- Blockly.unbindEvent_(this.sourceListener_);
+ this.block_.workspace.removeChangeListener(this.sourceListener_);
this.sourceListener_ = null;
}
}
@@ -268,7 +267,7 @@ Blockly.Mutator.prototype.setVisible = function(visible) {
* @private
*/
Blockly.Mutator.prototype.workspaceChanged_ = function() {
- if (Blockly.dragMode_ == 0) {
+ if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
var blocks = this.workspace_.getTopBlocks(false);
var MARGIN = 20;
for (var b = 0, block; block = blocks[b]; b++) {
@@ -283,23 +282,37 @@ Blockly.Mutator.prototype.workspaceChanged_ = function() {
// When the mutator's workspace changes, update the source block.
if (this.rootBlock_.workspace == this.workspace_) {
+ Blockly.Events.setGroup(true);
+ var block = this.block_;
+ var oldMutationDom = block.mutationToDom();
+ var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
// Switch off rendering while the source block is rebuilt.
- var savedRendered = this.block_.rendered;
- this.block_.rendered = false;
+ var savedRendered = block.rendered;
+ block.rendered = false;
// Allow the source block to rebuild itself.
- this.block_.compose(this.rootBlock_);
+ block.compose(this.rootBlock_);
// Restore rendering and show the changes.
- this.block_.rendered = savedRendered;
+ block.rendered = savedRendered;
// Mutation may have added some elements that need initalizing.
- this.block_.initSvg();
- if (this.block_.rendered) {
- this.block_.render();
+ block.initSvg();
+ var newMutationDom = block.mutationToDom();
+ var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
+ if (oldMutation != newMutation) {
+ Blockly.Events.fire(new Blockly.Events.Change(
+ block, 'mutation', null, oldMutation, newMutation));
+ // Ensure that any bump is part of this mutation's event group.
+ var group = Blockly.Events.getGroup();
+ setTimeout(function() {
+ Blockly.Events.setGroup(group);
+ block.bumpNeighbours_();
+ Blockly.Events.setGroup(false);
+ }, Blockly.BUMP_DELAY);
+ }
+ if (block.rendered) {
+ block.render();
}
this.resizeBubble_();
- // The source block may have changed, notify its workspace.
- this.block_.workspace.fireChangeEvent();
- goog.Timer.callOnce(
- this.block_.bumpNeighbours_, Blockly.BUMP_DELAY, this.block_);
+ Blockly.Events.setGroup(false);
}
};
@@ -330,3 +343,37 @@ Blockly.Mutator.prototype.dispose = function() {
this.block_.mutator = null;
Blockly.Icon.prototype.dispose.call(this);
};
+
+/**
+ * Reconnect an block to a mutated input.
+ * @param {Blockly.Connection} connectionChild Connection on child block.
+ * @param {!Blockly.Block} block Parent block.
+ * @param {string} inputName Name of input on parent block.
+ * @return {boolean} True iff a reconnection was made, false otherwise.
+ */
+Blockly.Mutator.reconnect = function(connectionChild, block, inputName) {
+ if (!connectionChild || !connectionChild.getSourceBlock().workspace) {
+ return false; // No connection or block has been deleted.
+ }
+ var connectionParent = block.getInput(inputName).connection;
+ var currentParent = connectionChild.targetBlock();
+ if ((!currentParent || currentParent == block) &&
+ connectionParent.targetConnection != connectionChild) {
+ if (connectionParent.isConnected()) {
+ // There's already something connected here. Get rid of it.
+ connectionParent.disconnect();
+ }
+ connectionParent.connect(connectionChild);
+ return true;
+ }
+ return false;
+};
+
+// Export symbols that would otherwise be renamed by Closure compiler.
+if (!goog.global['Blockly']) {
+ goog.global['Blockly'] = {};
+}
+if (!goog.global['Blockly']['Mutator']) {
+ goog.global['Blockly']['Mutator'] = {};
+}
+goog.global['Blockly']['Mutator']['reconnect'] = Blockly.Mutator.reconnect;
diff --git a/core/options.js b/core/options.js
new file mode 100644
index 0000000000..b102a0ce3e
--- /dev/null
+++ b/core/options.js
@@ -0,0 +1,251 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 Object that controls settings for the workspace.
+ * @author fenichel@google.com (Rachel Fenichel)
+ */
+'use strict';
+
+goog.provide('Blockly.Options');
+goog.require('Blockly.Colours');
+
+
+/**
+ * Parse the user-specified options, using reasonable defaults where behaviour
+ * is unspecified.
+ * @param {!Object} options Dictionary of options. Specification:
+ * https://developers.google.com/blockly/installation/overview#configuration
+ * @constructor
+ */
+Blockly.Options = function(options) {
+ var readOnly = !!options['readOnly'];
+ if (readOnly) {
+ var languageTree = null;
+ var hasCategories = false;
+ var hasTrashcan = false;
+ var hasCollapse = false;
+ var hasComments = false;
+ var hasDisable = false;
+ var hasSounds = false;
+ } else {
+ var languageTree = Blockly.Options.parseToolboxTree(options['toolbox']);
+ var hasCategories = Boolean(languageTree &&
+ languageTree.getElementsByTagName('category').length);
+ var hasTrashcan = options['trashcan'];
+ if (hasTrashcan === undefined) {
+ hasTrashcan = hasCategories;
+ }
+ var hasCollapse = options['collapse'];
+ if (hasCollapse === undefined) {
+ hasCollapse = hasCategories;
+ }
+ var hasComments = options['comments'];
+ if (hasComments === undefined) {
+ hasComments = hasCategories;
+ }
+ var hasDisable = options['disable'];
+ if (hasDisable === undefined) {
+ hasDisable = hasCategories;
+ }
+ var hasSounds = options['sounds'];
+ if (hasSounds === undefined) {
+ hasSounds = true;
+ }
+ }
+ var hasScrollbars = options['scrollbars'];
+ if (hasScrollbars === undefined) {
+ hasScrollbars = hasCategories;
+ }
+ var hasCss = options['css'];
+ if (hasCss === undefined) {
+ hasCss = true;
+ }
+ var pathToMedia = 'https://blockly-demo.appspot.com/static/media/';
+ if (options['media']) {
+ pathToMedia = options['media'];
+ } else if (options['path']) {
+ // 'path' is a deprecated option which has been replaced by 'media'.
+ pathToMedia = options['path'] + 'media/';
+ }
+
+ var horizontalLayout = options['horizontalLayout'];
+ if (horizontalLayout === undefined) {
+ horizontalLayout = false;
+ }
+ var toolboxAtStart = options['toolboxPosition'];
+ if (toolboxAtStart === 'end') {
+ toolboxAtStart = false;
+ } else {
+ toolboxAtStart = true;
+ }
+
+ if (horizontalLayout) {
+ var toolboxPosition = toolboxAtStart ? Blockly.TOOLBOX_AT_TOP : Blockly.TOOLBOX_AT_BOTTOM;
+ } else {
+ var toolboxPosition =
+ (toolboxAtStart == options['rtl']) ? Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT;
+ }
+
+ var enableRealtime = !!options['realtime'];
+ var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined;
+
+ // Colour overrides provided by the injection
+ var colours = options['colours'];
+ if (colours) {
+ for (var colourProperty in colours) {
+ if (colours.hasOwnProperty(colourProperty) &&
+ Blockly.Colours.hasOwnProperty(colourProperty)) {
+ // If a property is in both colours option and Blockly.Colours,
+ // set the Blockly.Colours value to the override.
+ Blockly.Colours[colourProperty] = colours[colourProperty];
+ }
+ }
+ }
+
+ this.RTL = !!options['rtl'];
+ this.collapse = hasCollapse;
+ this.comments = hasComments;
+ this.disable = hasDisable;
+ this.readOnly = readOnly;
+ this.maxBlocks = options['maxBlocks'] || Infinity;
+ this.pathToMedia = pathToMedia;
+ this.hasCategories = hasCategories;
+ this.hasScrollbars = hasScrollbars;
+ this.hasTrashcan = hasTrashcan;
+ this.hasSounds = hasSounds;
+ this.hasCss = hasCss;
+ this.languageTree = languageTree;
+ this.gridOptions = Blockly.Options.parseGridOptions_(options);
+ this.zoomOptions = Blockly.Options.parseZoomOptions_(options);
+ this.enableRealtime = enableRealtime;
+ this.realtimeOptions = realtimeOptions;
+ this.horizontalLayout = horizontalLayout;
+ this.toolboxAtStart = toolboxAtStart;
+ this.toolboxPosition = toolboxPosition;
+};
+
+/**
+ * @type {Blockly.Workspace} the parent of the current workspace, or null if
+ * there is no parent workspace.
+ **/
+Blockly.Options.prototype.parentWorkspace = null;
+
+/**
+ * If set, sets the translation of the workspace to match the scrollbars.
+ * No-op if unset.
+ */
+Blockly.Options.prototype.setMetrics = function(translation) { return; };
+
+/**
+ * Return an object with the metrics required to size the workspace, or null
+ * if unset.
+ * @return {Object} Contains size an position metrics, or null.
+ */
+Blockly.Options.prototype.getMetrics = function() { return null; };
+
+/**
+ * Parse the user-specified zoom options, using reasonable defaults where
+ * behaviour is unspecified. See zoom documentation:
+ * https://developers.google.com/blockly/installation/zoom
+ * @param {!Object} options Dictionary of options.
+ * @return {!Object} A dictionary of normalized options.
+ * @private
+ */
+Blockly.Options.parseZoomOptions_ = function(options) {
+ var zoom = options['zoom'] || {};
+ var zoomOptions = {};
+ if (zoom['controls'] === undefined) {
+ zoomOptions.controls = false;
+ } else {
+ zoomOptions.controls = !!zoom['controls'];
+ }
+ if (zoom['wheel'] === undefined) {
+ zoomOptions.wheel = false;
+ } else {
+ zoomOptions.wheel = !!zoom['wheel'];
+ }
+ if (zoom['startScale'] === undefined) {
+ zoomOptions.startScale = 1;
+ } else {
+ zoomOptions.startScale = parseFloat(zoom['startScale']);
+ }
+ if (zoom['maxScale'] === undefined) {
+ zoomOptions.maxScale = 3;
+ } else {
+ zoomOptions.maxScale = parseFloat(zoom['maxScale']);
+ }
+ if (zoom['minScale'] === undefined) {
+ zoomOptions.minScale = 0.3;
+ } else {
+ zoomOptions.minScale = parseFloat(zoom['minScale']);
+ }
+ if (zoom['scaleSpeed'] === undefined) {
+ zoomOptions.scaleSpeed = 1.2;
+ } else {
+ zoomOptions.scaleSpeed = parseFloat(zoom['scaleSpeed']);
+ }
+ return zoomOptions;
+};
+
+/**
+ * Parse the user-specified grid options, using reasonable defaults where
+ * behaviour is unspecified. See grid documentation:
+ * https://developers.google.com/blockly/installation/grid
+ * @param {!Object} options Dictionary of options.
+ * @return {!Object} A dictionary of normalized options.
+ * @private
+ */
+Blockly.Options.parseGridOptions_ = function(options) {
+ var grid = options['grid'] || {};
+ var gridOptions = {};
+ gridOptions.spacing = parseFloat(grid['spacing']) || 0;
+ gridOptions.colour = grid['colour'] || '#888';
+ gridOptions.length = parseFloat(grid['length']) || 1;
+ gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap'];
+ return gridOptions;
+};
+
+/**
+ * Parse the provided toolbox tree into a consistent DOM format.
+ * @param {Node|string} tree DOM tree of blocks, or text representation of same.
+ * @return {Node} DOM tree of blocks, or null.
+ */
+Blockly.Options.parseToolboxTree = function(tree) {
+ if (tree) {
+ if (typeof tree != 'string') {
+ if (typeof XSLTProcessor == 'undefined' && tree.outerHTML) {
+ // In this case the tree will not have been properly built by the
+ // browser. The HTML will be contained in the element, but it will
+ // not have the proper DOM structure since the browser doesn't support
+ // XSLTProcessor (XML -> HTML). This is the case in IE 9+.
+ tree = tree.outerHTML;
+ } else if (!(tree instanceof Element)) {
+ tree = null;
+ }
+ }
+ if (typeof tree == 'string') {
+ tree = Blockly.Xml.textToDom(tree);
+ }
+ } else {
+ tree = null;
+ }
+ return tree;
+};
diff --git a/core/procedures.js b/core/procedures.js
index e7ba5981e1..594f82e120 100644
--- a/core/procedures.js
+++ b/core/procedures.js
@@ -251,16 +251,28 @@ Blockly.Procedures.disposeCallers = function(name, workspace) {
/**
* When a procedure definition changes its parameters, find and edit all its
* callers.
- * @param {string} name Name of edited procedure definition.
- * @param {!Blockly.Workspace} workspace The workspace to delete callers from.
- * @param {!Array.} paramNames Array of new parameter names.
- * @param {!Array.} paramIds Array of unique parameter IDs.
+ * @param {!Blockly.Block} defBlock Procedure definition block.
*/
-Blockly.Procedures.mutateCallers = function(name, workspace,
- paramNames, paramIds) {
- var callers = Blockly.Procedures.getCallers(name, workspace);
- for (var i = 0; i < callers.length; i++) {
- callers[i].setProcedureParameters(paramNames, paramIds);
+Blockly.Procedures.mutateCallers = function(defBlock) {
+ var oldRecordUndo = Blockly.Events.recordUndo;
+ var name = defBlock.getProcedureDef()[0];
+ var xmlElement = defBlock.mutationToDom(true);
+ var callers = Blockly.Procedures.getCallers(name, defBlock.workspace);
+ for (var i = 0, caller; caller = callers[i]; i++) {
+ var oldMutationDom = caller.mutationToDom();
+ var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
+ caller.domToMutation(xmlElement);
+ var newMutationDom = caller.mutationToDom();
+ var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
+ if (oldMutation != newMutation) {
+ // Fire a mutation on every caller block. But don't record this as an
+ // undo action since it is deterministically tied to the procedure's
+ // definition mutation.
+ Blockly.Events.recordUndo = false;
+ Blockly.Events.fire(new Blockly.Events.Change(
+ caller, 'mutation', null, oldMutation, newMutation));
+ Blockly.Events.recordUndo = oldRecordUndo;
+ }
}
};
diff --git a/core/rendered_connection.js b/core/rendered_connection.js
new file mode 100644
index 0000000000..6aff78a732
--- /dev/null
+++ b/core/rendered_connection.js
@@ -0,0 +1,371 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 Components for creating connections between blocks.
+ * @author fenichel@google.com (Rachel Fenichel)
+ */
+'use strict';
+
+goog.provide('Blockly.RenderedConnection');
+
+goog.require('Blockly.Connection');
+
+
+/**
+ * Class for a connection between blocks that may be rendered on screen.
+ * @param {!Blockly.Block} source The block establishing this connection.
+ * @param {number} type The type of the connection.
+ * @constructor
+ */
+Blockly.RenderedConnection = function(source, type) {
+ Blockly.RenderedConnection.superClass_.constructor.call(this, source, type);
+};
+goog.inherits(Blockly.RenderedConnection, Blockly.Connection);
+
+/**
+ * Returns the distance between this connection and another connection.
+ * @param {!Blockly.Connection} otherConnection The other connection to measure
+ * the distance to.
+ * @return {number} The distance between connections.
+ */
+Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) {
+ var xDiff = this.x_ - otherConnection.x_;
+ var yDiff = this.y_ - otherConnection.y_;
+ return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+};
+
+/**
+ * Move the block(s) belonging to the connection to a point where they don't
+ * visually interfere with the specified connection.
+ * @param {!Blockly.Connection} staticConnection The connection to move away
+ * from.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) {
+ if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
+ // Don't move blocks around while the user is doing the same.
+ return;
+ }
+ // Move the root block.
+ var rootBlock = this.sourceBlock_.getRootBlock();
+ if (rootBlock.isInFlyout) {
+ // Don't move blocks around in a flyout.
+ return;
+ }
+ var reverse = false;
+ if (!rootBlock.isMovable()) {
+ // Can't bump an uneditable block away.
+ // Check to see if the other block is movable.
+ rootBlock = staticConnection.getSourceBlock().getRootBlock();
+ if (!rootBlock.isMovable()) {
+ return;
+ }
+ // Swap the connections and move the 'static' connection instead.
+ staticConnection = this;
+ reverse = true;
+ }
+ // Raise it to the top for extra visibility.
+ rootBlock.getSvgRoot().parentNode.appendChild(rootBlock.getSvgRoot());
+ var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS) - this.x_;
+ var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS) - this.y_;
+ if (reverse) {
+ // When reversing a bump due to an uneditable block, bump up.
+ dy = -dy;
+ }
+ if (rootBlock.RTL) {
+ dx = -dx;
+ }
+ rootBlock.moveBy(dx, dy);
+};
+
+/**
+ * Change the connection's coordinates.
+ * @param {number} x New absolute x coordinate.
+ * @param {number} y New absolute y coordinate.
+ */
+Blockly.RenderedConnection.prototype.moveTo = function(x, y) {
+ // Remove it from its old location in the database (if already present)
+ if (this.inDB_) {
+ this.db_.removeConnection_(this);
+ }
+ this.x_ = x;
+ this.y_ = y;
+ // Insert it into its new location in the database.
+ if (!this.hidden_) {
+ this.db_.addConnection(this);
+ }
+};
+
+/**
+ * Change the connection's coordinates.
+ * @param {number} dx Change to x coordinate.
+ * @param {number} dy Change to y coordinate.
+ */
+Blockly.RenderedConnection.prototype.moveBy = function(dx, dy) {
+ this.moveTo(this.x_ + dx, this.y_ + dy);
+};
+
+/**
+ * Move the blocks on either side of this connection right next to each other.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.tighten_ = function() {
+ var dx = this.targetConnection.x_ - this.x_;
+ var dy = this.targetConnection.y_ - this.y_;
+ if (dx != 0 || dy != 0) {
+ var block = this.targetBlock();
+ var svgRoot = block.getSvgRoot();
+ if (!svgRoot) {
+ throw 'block is not rendered.';
+ }
+ var xy = Blockly.getRelativeXY_(svgRoot);
+ block.getSvgRoot().setAttribute('transform',
+ 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')');
+ block.moveConnections_(-dx, -dy);
+ }
+};
+
+/**
+ * Find the closest compatible connection to this connection.
+ * @param {number} maxLimit The maximum radius to another connection.
+ * @param {!goog.math.Coordinate} dxy Offset between this connection's location
+ * in the database and the current location (as a result of dragging).
+ * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two
+ * properties: 'connection' which is either another connection or null,
+ * and 'radius' which is the distance.
+ */
+Blockly.RenderedConnection.prototype.closest = function(maxLimit, dxy) {
+ return this.dbOpposite_.searchForClosest(this, maxLimit, dxy);
+};
+
+/**
+ * Add highlighting around this connection.
+ */
+Blockly.RenderedConnection.prototype.highlight = function() {
+ var steps;
+ if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) {
+ var tabWidth = this.sourceBlock_.RTL ? -Blockly.BlockSvg.TAB_WIDTH :
+ Blockly.BlockSvg.TAB_WIDTH;
+ steps = 'm 0,0 ' + Blockly.BlockSvg.TAB_PATH_DOWN + ' v 5';
+
+ } else {
+ steps = 'm -20,0 h 5 ' + Blockly.BlockSvg.NOTCH_PATH_LEFT + ' h 5';
+ }
+ var xy = this.sourceBlock_.getRelativeToSurfaceXY();
+ var x = this.x_ - xy.x;
+ var y = this.y_ - xy.y;
+ Blockly.Connection.highlightedPath_ = Blockly.createSvgElement('path',
+ {'class': 'blocklyHighlightedConnectionPath',
+ 'd': steps,
+ transform: 'translate(' + x + ',' + y + ')' +
+ (this.sourceBlock_.RTL ? ' scale(-1 1)' : '')},
+ this.sourceBlock_.getSvgRoot());
+};
+
+/**
+ * Unhide this connection, as well as all down-stream connections on any block
+ * attached to this connection. This happens when a block is expanded.
+ * Also unhides down-stream comments.
+ * @return {!Array.} List of blocks to render.
+ */
+Blockly.RenderedConnection.prototype.unhideAll = function() {
+ this.setHidden(false);
+ // All blocks that need unhiding must be unhidden before any rendering takes
+ // place, since rendering requires knowing the dimensions of lower blocks.
+ // Also, since rendering a block renders all its parents, we only need to
+ // render the leaf nodes.
+ var renderList = [];
+ if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) {
+ // Only spider down.
+ return renderList;
+ }
+ var block = this.targetBlock();
+ if (block) {
+ var connections;
+ if (block.isCollapsed()) {
+ // This block should only be partially revealed since it is collapsed.
+ connections = [];
+ block.outputConnection && connections.push(block.outputConnection);
+ block.nextConnection && connections.push(block.nextConnection);
+ block.previousConnection && connections.push(block.previousConnection);
+ } else {
+ // Show all connections of this block.
+ connections = block.getConnections_(true);
+ }
+ for (var c = 0; c < connections.length; c++) {
+ renderList.push.apply(renderList, connections[c].unhideAll());
+ }
+ if (!renderList.length) {
+ // Leaf block.
+ renderList[0] = block;
+ }
+ }
+ return renderList;
+};
+
+/**
+ * Remove the highlighting around this connection.
+ */
+Blockly.RenderedConnection.prototype.unhighlight = function() {
+ goog.dom.removeNode(Blockly.Connection.highlightedPath_);
+ delete Blockly.Connection.highlightedPath_;
+};
+
+/**
+ * Set whether this connections is hidden (not tracked in a database) or not.
+ * @param {boolean} hidden True if connection is hidden.
+ */
+Blockly.RenderedConnection.prototype.setHidden = function(hidden) {
+ this.hidden_ = hidden;
+ if (hidden && this.inDB_) {
+ this.db_.removeConnection_(this);
+ } else if (!hidden && !this.inDB_) {
+ this.db_.addConnection(this);
+ }
+};
+
+/**
+ * Hide this connection, as well as all down-stream connections on any block
+ * attached to this connection. This happens when a block is collapsed.
+ * Also hides down-stream comments.
+ */
+Blockly.RenderedConnection.prototype.hideAll = function() {
+ this.setHidden(true);
+ if (this.targetConnection) {
+ var blocks = this.targetBlock().getDescendants();
+ for (var b = 0; b < blocks.length; b++) {
+ var block = blocks[b];
+ // Hide all connections of all children.
+ var connections = block.getConnections_(true);
+ for (var c = 0; c < connections.length; c++) {
+ connections[c].setHidden(true);
+ }
+ // Close all bubbles of all children.
+ var icons = block.getIcons();
+ for (var i = 0; i < icons.length; i++) {
+ icons[i].setVisible(false);
+ }
+ }
+ }
+};
+
+/**
+ * Check if the two connections can be dragged to connect to each other.
+ * @param {!Blockly.Connection} candidate A nearby connection to check.
+ * @param {number} maxRadius The maximum radius allowed for connections.
+ * @return {boolean} True if the connection is allowed, false otherwise.
+ */
+Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate,
+ maxRadius) {
+ if (this.distanceFrom(candidate) > maxRadius) {
+ return false;
+ }
+
+ return Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,
+ candidate);
+};
+
+/**
+ * Disconnect two blocks that are connected by this connection.
+ * @param {!Blockly.Block} parentBlock The superior block.
+ * @param {!Blockly.Block} childBlock The inferior block.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.disconnectInternal_ = function(parentBlock,
+ childBlock) {
+ Blockly.RenderedConnection.superClass_.disconnectInternal_.call(this,
+ parentBlock, childBlock);
+ // Rerender the parent so that it may reflow.
+ if (parentBlock.rendered) {
+ parentBlock.render();
+ }
+ if (childBlock.rendered) {
+ childBlock.updateDisabled();
+ childBlock.render();
+ }
+};
+
+/**
+ * Respawn the shadow block if there was one connected to the this connection.
+ * Render/rerender blocks as needed.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.respawnShadow_ = function() {
+ var parentBlock = this.getSourceBlock();
+ // Respawn the shadow block if there is one.
+ var shadow = this.getShadowDom();
+ if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) {
+ var blockShadow =
+ Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);
+ if (!blockShadow) {
+ throw 'Couldn\'t respawn the shadow block that should exist here.';
+ }
+ blockShadow.initSvg();
+ blockShadow.render(false);
+ if (parentBlock.rendered) {
+ parentBlock.render();
+ }
+ }
+};
+
+/**
+ * Find all nearby compatible connections to this connection.
+ * Type checking does not apply, since this function is used for bumping.
+ * @param {number} maxLimit The maximum radius to another connection.
+ * @return {!Array.} List of connections.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.neighbours_ = function(maxLimit) {
+ return this.dbOpposite_.getNeighbours(this, maxLimit);
+};
+
+/**
+ * Connect two connections together. This is the connection on the superior
+ * block. Rerender blocks as needed.
+ * @param {!Blockly.Connection} childConnection Connection on inferior block.
+ * @private
+ */
+Blockly.RenderedConnection.prototype.connect_ = function(childConnection) {
+ Blockly.RenderedConnection.superClass_.connect_.call(this, childConnection);
+
+ var parentConnection = this;
+ var parentBlock = parentConnection.getSourceBlock();
+ var childBlock = childConnection.getSourceBlock();
+
+ if (parentBlock.rendered) {
+ parentBlock.updateDisabled();
+ }
+ if (childBlock.rendered) {
+ childBlock.updateDisabled();
+ }
+ if (parentBlock.rendered && childBlock.rendered) {
+ if (parentConnection.type == Blockly.NEXT_STATEMENT ||
+ parentConnection.type == Blockly.PREVIOUS_STATEMENT) {
+ // Child block may need to square off its corners if it is in a stack.
+ // Rendering a child will render its parent.
+ childBlock.render();
+ } else {
+ // Child block does not change shape. Rendering the parent node will
+ // move its connected children into position.
+ parentBlock.render();
+ }
+ }
+};
diff --git a/core/scrollbar.js b/core/scrollbar.js
index e872900f00..6fb115aef1 100644
--- a/core/scrollbar.js
+++ b/core/scrollbar.js
@@ -136,8 +136,43 @@ Blockly.ScrollbarPair.prototype.resize = function() {
* @param {number} y Vertical scroll value.
*/
Blockly.ScrollbarPair.prototype.set = function(x, y) {
- this.hScroll.set(x);
- this.vScroll.set(y);
+ // This function is equivalent to:
+ // this.hScroll.set(x);
+ // this.vScroll.set(y);
+ // However, that calls setMetrics twice which causes a chain of
+ // getAttribute->setAttribute->getAttribute resulting in an extra layout pass.
+ // Combining them speeds up rendering.
+ var xyRatio = {};
+
+ var hKnobValue = x * this.hScroll.ratio_;
+ var vKnobValue = y * this.vScroll.ratio_;
+
+ var hBarLength =
+ parseFloat(this.hScroll.svgBackground_.getAttribute('width'));
+ var vBarLength =
+ parseFloat(this.vScroll.svgBackground_.getAttribute('height'));
+
+ xyRatio.x = this.getRatio_(hKnobValue, hBarLength);
+ xyRatio.y = this.getRatio_(vKnobValue, vBarLength);
+
+ this.workspace_.setMetrics(xyRatio);
+ this.hScroll.svgKnob_.setAttribute('x', hKnobValue);
+ this.vScroll.svgKnob_.setAttribute('y', vKnobValue);
+};
+
+/**
+ * Helper to calculate the ratio of knob value to bar length.
+ * @param {number} knobValue The value of the knob.
+ * @param {number} barLength The length of the scroll bar.
+ * @return {number} Ratio.
+ * @private
+ */
+Blockly.ScrollbarPair.prototype.getRatio_ = function(knobValue, barLength) {
+ var ratio = knobValue / barLength;
+ if (isNaN(ratio)) {
+ return 0;
+ }
+ return ratio;
};
// --------------------------------------------------------------------
@@ -148,7 +183,7 @@ Blockly.ScrollbarPair.prototype.set = function(x, y) {
* look or behave like the system's scrollbars.
* @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to.
* @param {boolean} horizontal True if horizontal, false if vertical.
- * @param {boolean=} opt_pair True if the scrollbar is part of a horiz/vert pair.
+ * @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair.
* @constructor
*/
Blockly.Scrollbar = function(workspace, horizontal, opt_pair) {
@@ -235,69 +270,89 @@ Blockly.Scrollbar.prototype.resize = function(opt_metrics) {
* .absoluteLeft: Left-edge of view.
*/
if (this.horizontal_) {
- var outerLength = hostMetrics.viewWidth - 1;
- if (this.pair_) {
- // Shorten the scrollbar to make room for the corner square.
- outerLength -= Blockly.Scrollbar.scrollbarThickness;
- } else {
- // Only show the scrollbar if needed.
- // Ideally this would also apply to scrollbar pairs, but that's a bigger
- // headache (due to interactions with the corner square).
- this.setVisible(outerLength < hostMetrics.contentHeight);
- }
- this.ratio_ = outerLength / hostMetrics.contentWidth;
- if (this.ratio_ === -Infinity || this.ratio_ === Infinity ||
- isNaN(this.ratio_)) {
- this.ratio_ = 0;
- }
- var innerLength = hostMetrics.viewWidth * this.ratio_;
- var innerOffset = (hostMetrics.viewLeft - hostMetrics.contentLeft) *
- this.ratio_;
- this.svgKnob_.setAttribute('width', Math.max(0, innerLength));
- this.xCoordinate = hostMetrics.absoluteLeft + 0.5;
- if (this.pair_ && this.workspace_.RTL) {
- this.xCoordinate += hostMetrics.absoluteLeft +
- Blockly.Scrollbar.scrollbarThickness;
- }
- this.yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight -
- Blockly.Scrollbar.scrollbarThickness - 0.5;
- this.svgGroup_.setAttribute('transform',
- 'translate(' + this.xCoordinate + ',' + this.yCoordinate + ')');
- this.svgBackground_.setAttribute('width', Math.max(0, outerLength));
- this.svgKnob_.setAttribute('x', this.constrainKnob_(innerOffset));
+ this.resizeHorizontal_(hostMetrics);
} else {
- var outerLength = hostMetrics.viewHeight - 1;
- if (this.pair_) {
- // Shorten the scrollbar to make room for the corner square.
- outerLength -= Blockly.Scrollbar.scrollbarThickness;
- } else {
- // Only show the scrollbar if needed.
- this.setVisible(outerLength < hostMetrics.contentHeight);
- }
- this.ratio_ = outerLength / hostMetrics.contentHeight;
- if (this.ratio_ === -Infinity || this.ratio_ === Infinity ||
- isNaN(this.ratio_)) {
- this.ratio_ = 0;
- }
- var innerLength = hostMetrics.viewHeight * this.ratio_;
- var innerOffset = (hostMetrics.viewTop - hostMetrics.contentTop) *
- this.ratio_;
- this.svgKnob_.setAttribute('height', Math.max(0, innerLength));
- this.xCoordinate = hostMetrics.absoluteLeft + 0.5;
- if (!this.workspace_.RTL) {
- this.xCoordinate += hostMetrics.viewWidth -
- Blockly.Scrollbar.scrollbarThickness - 1;
- }
- this.yCoordinate = hostMetrics.absoluteTop + 0.5;
- this.svgGroup_.setAttribute('transform',
- 'translate(' + this.xCoordinate + ',' + this.yCoordinate + ')');
- this.svgBackground_.setAttribute('height', Math.max(0, outerLength));
- this.svgKnob_.setAttribute('y', this.constrainKnob_(innerOffset));
+ this.resizeVertical_(hostMetrics);
}
// Resizing may have caused some scrolling.
this.onScroll_();
};
+/**
+ * Recalculate a horizontal scrollbar's location and length.
+ * @param {!Object} hostMetrics A data structure describing all the
+ * required dimensions, possibly fetched from the host object.
+ * @private
+ */
+Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) {
+ var outerLength = hostMetrics.viewWidth - 1;
+ if (this.pair_) {
+ // Shorten the scrollbar to make room for the corner square.
+ outerLength -= Blockly.Scrollbar.scrollbarThickness;
+ } else {
+ // Only show the scrollbar if needed.
+ // Ideally this would also apply to scrollbar pairs, but that's a bigger
+ // headache (due to interactions with the corner square).
+ this.setVisible(outerLength < hostMetrics.contentWidth);
+ }
+ this.ratio_ = outerLength / hostMetrics.contentWidth;
+ if (this.ratio_ === -Infinity || this.ratio_ === Infinity ||
+ isNaN(this.ratio_)) {
+ this.ratio_ = 0;
+ }
+ var innerLength = hostMetrics.viewWidth * this.ratio_;
+ var innerOffset = (hostMetrics.viewLeft - hostMetrics.contentLeft) *
+ this.ratio_;
+ this.svgKnob_.setAttribute('width', Math.max(0, innerLength));
+ this.xCoordinate = hostMetrics.absoluteLeft + 0.5;
+ if (this.pair_ && this.workspace_.RTL) {
+ this.xCoordinate += hostMetrics.absoluteLeft +
+ Blockly.Scrollbar.scrollbarThickness;
+ }
+ this.yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight -
+ Blockly.Scrollbar.scrollbarThickness - 0.5;
+ this.svgGroup_.setAttribute('transform',
+ 'translate(' + this.xCoordinate + ',' + this.yCoordinate + ')');
+ this.svgBackground_.setAttribute('width', Math.max(0, outerLength));
+ this.svgKnob_.setAttribute('x', this.constrainKnob_(innerOffset));
+};
+
+/**
+ * Recalculate a vertical scrollbar's location and length.
+ * @param {!Object} hostMetrics A data structure describing all the
+ * required dimensions, possibly fetched from the host object.
+ * @private
+ */
+Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) {
+ var outerLength = hostMetrics.viewHeight - 1;
+ if (this.pair_) {
+ // Shorten the scrollbar to make room for the corner square.
+ outerLength -= Blockly.Scrollbar.scrollbarThickness;
+ } else {
+ // Only show the scrollbar if needed.
+ this.setVisible(outerLength < hostMetrics.contentHeight);
+ }
+ this.ratio_ = outerLength / hostMetrics.contentHeight;
+ if (this.ratio_ === -Infinity || this.ratio_ === Infinity ||
+ isNaN(this.ratio_)) {
+ this.ratio_ = 0;
+ }
+ var innerLength = hostMetrics.viewHeight * this.ratio_;
+ var innerOffset = (hostMetrics.viewTop - hostMetrics.contentTop) *
+ this.ratio_;
+ this.svgKnob_.setAttribute('height', Math.max(0, innerLength));
+ this.xCoordinate = hostMetrics.absoluteLeft + 0.5;
+ if (!this.workspace_.RTL) {
+ this.xCoordinate += hostMetrics.viewWidth -
+ Blockly.Scrollbar.scrollbarThickness - 1;
+ }
+ this.yCoordinate = hostMetrics.absoluteTop + 0.5;
+ this.svgGroup_.setAttribute('transform',
+ 'translate(' + this.xCoordinate + ',' + this.yCoordinate + ')');
+ this.svgBackground_.setAttribute('height', Math.max(0, outerLength));
+ this.svgKnob_.setAttribute('y', this.constrainKnob_(innerOffset));
+};
+
/**
* Create all the DOM elements required for a scrollbar.
* The resulting widget is not sized.
@@ -389,8 +444,13 @@ Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) {
}
this.svgKnob_.setAttribute(this.horizontal_ ? 'x' : 'y',
this.constrainKnob_(knobValue));
+ // When the scrollbars are clicked, hide the WidgetDiv/DropDownDiv without animation
+ // in anticipation of a workspace move.
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
this.onScroll_();
e.stopPropagation();
+ e.preventDefault();
};
/**
@@ -416,7 +476,12 @@ Blockly.Scrollbar.prototype.onMouseDownKnob_ = function(e) {
'mouseup', this, this.onMouseUpKnob_);
Blockly.Scrollbar.onMouseMoveWrapper_ = Blockly.bindEvent_(document,
'mousemove', this, this.onMouseMoveKnob_);
+ // When the scrollbars are clicked, hide the WidgetDiv/DropDownDiv without animation
+ // in anticipation of a workspace move.
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
e.stopPropagation();
+ e.preventDefault();
};
/**
@@ -439,7 +504,6 @@ Blockly.Scrollbar.prototype.onMouseMoveKnob_ = function(e) {
* @private
*/
Blockly.Scrollbar.prototype.onMouseUpKnob_ = function() {
- Blockly.removeAllRanges();
Blockly.hideChaff(true);
if (Blockly.Scrollbar.onMouseUpWrapper_) {
Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_);
@@ -480,7 +544,7 @@ Blockly.Scrollbar.prototype.onScroll_ = function() {
var barLength = parseFloat(
this.svgBackground_.getAttribute(this.horizontal_ ? 'width' : 'height'));
var ratio = knobValue / barLength;
- if (isNaN(ratio)) {
+ if (isNaN(ratio) || !barLength) {
ratio = 0;
}
var xyRatio = {};
@@ -497,8 +561,9 @@ Blockly.Scrollbar.prototype.onScroll_ = function() {
* @param {number} value The distance from the top/left end of the bar.
*/
Blockly.Scrollbar.prototype.set = function(value) {
+ var constrainedValue = this.constrainKnob_(value * this.ratio_);
// Move the scrollbar slider.
- this.svgKnob_.setAttribute(this.horizontal_ ? 'x' : 'y', value * this.ratio_);
+ this.svgKnob_.setAttribute(this.horizontal_ ? 'x' : 'y', constrainedValue);
this.onScroll_();
};
diff --git a/core/toolbox.js b/core/toolbox.js
index 227e8e0c4d..9bf9f1738c 100644
--- a/core/toolbox.js
+++ b/core/toolbox.js
@@ -31,6 +31,7 @@ goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.html.SafeHtml');
+goog.require('goog.html.SafeStyle');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('goog.ui.tree.TreeControl');
@@ -50,14 +51,85 @@ Blockly.Toolbox = function(workspace) {
* @private
*/
this.workspace_ = workspace;
+
+ /**
+ * Whether toolbox categories should be represented by icons instead of text.
+ * @type {boolean}
+ * @private
+ */
+ this.iconic_ = false;
+
+ /**
+ * Is RTL vs LTR.
+ * @type {boolean}
+ */
+ this.RTL = workspace.options.RTL;
+
+ /**
+ * Whether the toolbox should be laid out horizontally.
+ * @type {boolean}
+ * @private
+ */
+ this.horizontalLayout_ = workspace.options.horizontalLayout;
+
+ /**
+ * Position of the toolbox and flyout relative to the workspace.
+ * @type {number}
+ */
+ this.toolboxPosition = workspace.options.toolboxPosition;
+
+ /**
+ * Configuration constants for Closure's tree UI.
+ * @type {Object.}
+ * @private
+ */
+ this.config_ = {
+ indentWidth: 19,
+ cssRoot: 'blocklyTreeRoot',
+ cssHideRoot: 'blocklyHidden',
+ cssItem: '',
+ cssTreeRow: 'blocklyTreeRow',
+ cssItemLabel: 'blocklyTreeLabel',
+ cssTreeIcon: 'blocklyTreeIcon',
+ cssExpandedFolderIcon: 'blocklyTreeIconOpen',
+ cssFileIcon: 'blocklyTreeIconNone',
+ cssSelectedRow: 'blocklyTreeSelected'
+ };
+
+
+ /**
+ * Configuration constants for tree separator.
+ * @type {Object.}
+ * @private
+ */
+ this.treeSeparatorConfig_ = {
+ cssTreeRow: 'blocklyTreeSeparator'
+ };
+
+ if (this.horizontalLayout_) {
+ this.config_['cssTreeRow'] =
+ this.config_['cssTreeRow'] +
+ (workspace.RTL ? ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
+
+ this.treeSeparatorConfig_['cssTreeRow'] =
+ 'blocklyTreeSeparatorHorizontal' +
+ (workspace.RTL ? ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
+ this.config_['cssTreeIcon'] = '';
+ }
};
/**
- * Width of the toolbox.
+ * Width of the toolbox, which only changes in vertical layout.
* @type {number}
*/
Blockly.Toolbox.prototype.width = 0;
+/**
+ * Height of the toolbox, which only changes in horizontal layout.
+ * @type {number}
+ */
+Blockly.Toolbox.prototype.height = 0;
+
/**
* The SVG group currently selected.
* @type {SVGGElement}
@@ -72,25 +144,6 @@ Blockly.Toolbox.prototype.selectedOption_ = null;
*/
Blockly.Toolbox.prototype.lastCategory_ = null;
-/**
- * Configuration constants for Closure's tree UI.
- * @type {Object.}
- * @const
- * @private
- */
-Blockly.Toolbox.prototype.CONFIG_ = {
- indentWidth: 19,
- cssRoot: 'blocklyTreeRoot',
- cssHideRoot: 'blocklyHidden',
- cssItem: '',
- cssTreeRow: 'blocklyTreeRow',
- cssItemLabel: 'blocklyTreeLabel',
- cssTreeIcon: 'blocklyTreeIcon',
- cssExpandedFolderIcon: 'blocklyTreeIconOpen',
- cssFileIcon: 'blocklyTreeIconNone',
- cssSelectedRow: 'blocklyTreeSelected'
-};
-
/**
* Initializes the toolbox.
*/
@@ -102,9 +155,10 @@ Blockly.Toolbox.prototype.init = function() {
this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR');
document.body.appendChild(this.HtmlDiv);
- // Clicking on toolbar closes popups.
+ // Clicking on toolbox closes popups.
Blockly.bindEvent_(this.HtmlDiv, 'mousedown', this,
function(e) {
+ Blockly.DropDownDiv.hide();
if (Blockly.isRightButton(e) || e.target == this.HtmlDiv) {
// Close flyout.
Blockly.hideChaff(false);
@@ -116,8 +170,11 @@ Blockly.Toolbox.prototype.init = function() {
var workspaceOptions = {
disabledPatternId: workspace.options.disabledPatternId,
parentWorkspace: workspace,
- RTL: workspace.RTL
+ RTL: workspace.RTL,
+ horizontalLayout: workspace.horizontalLayout,
+ toolboxPosition: workspace.options.toolboxPosition
};
+
/**
* @type {!Blockly.Flyout}
* @private
@@ -125,11 +182,12 @@ Blockly.Toolbox.prototype.init = function() {
this.flyout_ = new Blockly.Flyout(workspaceOptions);
goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_);
this.flyout_.init(workspace);
+ this.flyout_.hide();
- this.CONFIG_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
- this.CONFIG_['cssCollapsedFolderIcon'] =
- 'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr');
- var tree = new Blockly.Toolbox.TreeControl(this, this.CONFIG_);
+ this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
+ this.config_['cssCollapsedFolderIcon'] =
+ 'blocklyTreeIconClosed' + (this.RTL ? 'Rtl' : 'Ltr');
+ var tree = new Blockly.Toolbox.TreeControl(this, this.config_);
this.tree_ = tree;
tree.setShowRootNode(false);
tree.setShowLines(false);
@@ -164,18 +222,33 @@ Blockly.Toolbox.prototype.position = function() {
var svg = this.workspace_.getParentSvg();
var svgPosition = goog.style.getPageOffset(svg);
var svgSize = Blockly.svgSize(svg);
- if (this.workspace_.RTL) {
- treeDiv.style.left =
- (svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
- } else {
+ if (this.horizontalLayout_) {
treeDiv.style.left = svgPosition.x + 'px';
- }
- treeDiv.style.height = svgSize.height + 'px';
- treeDiv.style.top = svgPosition.y + 'px';
- this.width = treeDiv.offsetWidth;
- if (!this.workspace_.RTL) {
- // For some reason the LTR toolbox now reports as 1px too wide.
- this.width -= 1;
+ treeDiv.style.height = 'auto';
+ treeDiv.style.width = svgSize.width + 'px';
+ this.height = treeDiv.offsetHeight;
+ if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
+ treeDiv.style.top = svgPosition.y + 'px';
+ this.flyout_.setVerticalOffset(treeDiv.offsetHeight);
+ } else { // Bottom
+ var topOfToolbox = svgPosition.y + svgSize.height;
+ treeDiv.style.top = topOfToolbox + 'px';
+ this.flyout_.setVerticalOffset(topOfToolbox);
+ }
+ } else {
+ if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
+ treeDiv.style.left =
+ (svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
+ } else { // Left
+ treeDiv.style.left = svgPosition.x + 'px';
+ }
+ treeDiv.style.height = svgSize.height + 'px';
+ treeDiv.style.top = svgPosition.y + 'px';
+ this.width = treeDiv.offsetWidth;
+ if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
+ // For some reason the LTR toolbox now reports as 1px too wide.
+ this.width -= 1;
+ }
}
this.flyout_.position();
};
@@ -187,10 +260,12 @@ Blockly.Toolbox.prototype.position = function() {
*/
Blockly.Toolbox.prototype.populate_ = function(newTree) {
var rootOut = this.tree_;
+ var that = this;
rootOut.removeChildren(); // Delete any existing content.
rootOut.blocks = [];
var hasColours = false;
- function syncTrees(treeIn, treeOut) {
+ function syncTrees(treeIn, treeOut, iconic, pathToMedia) {
+ var lastElement = null;
for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) {
if (!childIn.tagName) {
// Skip over text.
@@ -198,15 +273,24 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
}
switch (childIn.tagName.toUpperCase()) {
case 'CATEGORY':
- var childOut = rootOut.createNode(childIn.getAttribute('name'));
+ if (iconic && childIn.getAttribute('icon')) {
+ var childOut = rootOut.createNode(childIn.getAttribute('name'),
+ pathToMedia + childIn.getAttribute('icon'));
+ } else {
+ var childOut = rootOut.createNode(childIn.getAttribute('name'), null);
+ }
childOut.blocks = [];
- treeOut.add(childOut);
+ if (that.horizontalLayout_) {
+ treeOut.add(childOut);
+ } else {
+ treeOut.addChildAt(childOut, 0);
+ }
var custom = childIn.getAttribute('custom');
if (custom) {
// Variables and procedures are special dynamic categories.
childOut.blocks = custom;
} else {
- syncTrees(childIn, childOut);
+ syncTrees(childIn, childOut, iconic, pathToMedia);
}
var colour = childIn.getAttribute('colour');
if (goog.isString(colour)) {
@@ -227,18 +311,43 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
} else {
childOut.setExpanded(false);
}
+ lastElement = childIn;
break;
case 'SEP':
- treeOut.add(new Blockly.Toolbox.TreeSeparator());
+ if (lastElement) {
+ if (lastElement.tagName.toUpperCase() == 'CATEGORY') {
+ // Separator between two categories.
+ //
+ if (that.horizontalLayout_) {
+ treeOut.add(new Blockly.Toolbox.TreeSeparator(that.treeSeparatorConfig_));
+ } else {
+ treeOut.addChildAt(new Blockly.Toolbox.TreeSeparator(that.treeSeparatorConfig_),
+ 0);
+ }
+ } else {
+ // Change the gap between two blocks.
+ //
+ // The default gap is 24, can be set larger or smaller.
+ // Note that a deprecated method is to add a gap to a block.
+ //
+ var newGap = parseFloat(childIn.getAttribute('gap'));
+ if (!isNaN(newGap)) {
+ var oldGap = parseFloat(lastElement.getAttribute('gap'));
+ var gap = isNaN(oldGap) ? newGap : oldGap + newGap;
+ lastElement.setAttribute('gap', gap);
+ }
+ }
+ }
break;
case 'BLOCK':
case 'SHADOW':
treeOut.blocks.push(childIn);
+ lastElement = childIn;
break;
}
}
}
- syncTrees(newTree, this.tree_);
+ syncTrees(newTree, this.tree_, this.iconic_, this.workspace_.options.pathToMedia);
this.hasColours_ = hasColours;
if (rootOut.blocks.length) {
@@ -246,7 +355,7 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
}
// Fire a resize event since the toolbox may have changed width and height.
- Blockly.fireUiEvent(window, 'resize');
+ Blockly.asyncSvgResize(this.workspace_);
};
/**
@@ -266,7 +375,7 @@ Blockly.Toolbox.prototype.addColour_ = function(opt_tree) {
} else {
var border = 'none';
}
- if (this.workspace_.RTL) {
+ if (this.RTL) {
element.style.borderRight = border;
} else {
element.style.borderLeft = border;
@@ -284,23 +393,32 @@ Blockly.Toolbox.prototype.clearSelection = function() {
};
/**
- * Return the deletion rectangle for this toolbar.
+ * Return the deletion rectangle for this toolbar in viewport coordinates.\
* @return {goog.math.Rect} Rectangle in which to delete.
*/
-Blockly.Toolbox.prototype.getRect = function() {
+Blockly.Toolbox.prototype.getClientRect = function() {
// BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
+ var toolboxRect = this.HtmlDiv.getBoundingClientRect();
+
+ var x = toolboxRect.left;
+ var y = toolboxRect.top;
+ var width = toolboxRect.width;
+ var height = toolboxRect.height;
+
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
- if (this.workspace_.RTL) {
- var svgSize = Blockly.svgSize(this.workspace_.getParentSvg());
- var x = svgSize.width - this.width;
- } else {
- var x = -BIG_NUM;
+ if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
+ return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
+ } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
+ return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
+ } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
+ return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM, BIG_NUM + height);
+ } else { // Bottom
+ return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width);
}
- return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + this.width, 2 * BIG_NUM);
};
// Extending Closure's Tree UI.
@@ -353,12 +471,16 @@ Blockly.Toolbox.TreeControl.prototype.handleTouchEvent_ = function(e) {
/**
* Creates a new tree node using a custom tree node.
* @param {string=} opt_html The HTML content of the node label.
+ * @param {string} icon The path to the icon for this category.
* @return {!goog.ui.tree.TreeNode} The new item.
* @override
*/
-Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html) {
- return new Blockly.Toolbox.TreeNode(this.toolbox_, opt_html ?
- goog.html.SafeHtml.htmlEscape(opt_html) : goog.html.SafeHtml.EMPTY,
+Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html, icon) {
+ var icon_html = ' ';
+ var safe_opt_html = opt_html ?
+ goog.html.SafeHtml.htmlEscape(opt_html) : goog.html.SafeHtml.EMPTY;
+ var label_html = icon ? icon_html + ' ' + opt_html : safe_opt_html;
+ return new Blockly.Toolbox.TreeNode(this.toolbox_, label_html,
this.getConfig(), this.getDomHelper());
};
@@ -368,7 +490,6 @@ Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html) {
* @override
*/
Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) {
- Blockly.removeAllRanges();
var toolbox = this.toolbox_;
if (node == this.selectedItem_ || node == toolbox.tree_) {
return;
@@ -383,17 +504,24 @@ Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) {
// not rendered.
toolbox.addColour_(node);
}
+ var oldNode = this.getSelectedItem();
goog.ui.tree.TreeControl.prototype.setSelectedItem.call(this, node);
if (node && node.blocks && node.blocks.length) {
toolbox.flyout_.show(node.blocks);
- // Scroll the flyout to the top if the category has changed.
+ // Scroll the flyout to the start if the category has changed.
if (toolbox.lastCategory_ != node) {
- toolbox.flyout_.scrollToTop();
+ toolbox.flyout_.scrollToStart();
}
} else {
// Hide the flyout.
toolbox.flyout_.hide();
}
+ if (oldNode != node && oldNode != this) {
+ var event = new Blockly.Events.Ui(null, 'category',
+ oldNode && oldNode.getHtml(), node && node.getHtml());
+ event.workspaceId = toolbox.workspace_.id;
+ Blockly.Events.fire(event);
+ }
if (node) {
toolbox.lastCategory_ = node;
}
@@ -414,13 +542,15 @@ Blockly.Toolbox.TreeNode = function(toolbox, html, opt_config, opt_domHelper) {
goog.ui.tree.TreeNode.call(this, html, opt_config, opt_domHelper);
if (toolbox) {
var resize = function() {
- Blockly.fireUiEvent(window, 'resize');
+ Blockly.asyncSvgResize(toolbox.workspace_);
};
// Fire a resize event since the toolbox may have changed width.
goog.events.listen(toolbox.tree_,
goog.ui.tree.BaseNode.EventType.EXPAND, resize);
goog.events.listen(toolbox.tree_,
goog.ui.tree.BaseNode.EventType.COLLAPSE, resize);
+
+ this.toolbox_ = toolbox;
}
};
goog.inherits(Blockly.Toolbox.TreeNode, goog.ui.tree.TreeNode);
@@ -467,18 +597,7 @@ Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) {
* @constructor
* @extends {Blockly.Toolbox.TreeNode}
*/
-Blockly.Toolbox.TreeSeparator = function() {
- Blockly.Toolbox.TreeNode.call(this, null, '',
- Blockly.Toolbox.TreeSeparator.CONFIG_);
+Blockly.Toolbox.TreeSeparator = function(config) {
+ Blockly.Toolbox.TreeNode.call(this, null, '', config);
};
goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode);
-
-/**
- * Configuration constants for tree separator.
- * @type {Object.}
- * @const
- * @private
- */
-Blockly.Toolbox.TreeSeparator.CONFIG_ = {
- cssTreeRow: 'blocklyTreeSeparator'
-};
diff --git a/core/tooltip.js b/core/tooltip.js
index 4ac8049b94..6e520e65b2 100644
--- a/core/tooltip.js
+++ b/core/tooltip.js
@@ -99,7 +99,7 @@ Blockly.Tooltip.RADIUS_OK = 10;
/**
* Delay before tooltip appears.
*/
-Blockly.Tooltip.HOVER_MS = 1000;
+Blockly.Tooltip.HOVER_MS = 750;
/**
* Horizontal padding between tooltip and screen edge.
@@ -184,7 +184,7 @@ Blockly.Tooltip.onMouseMove_ = function(e) {
if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) {
// No tooltip here to show.
return;
- } else if (Blockly.dragMode_ != 0) {
+ } else if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
// Don't display a tooltip during a drag.
return;
} else if (Blockly.WidgetDiv.isVisible()) {
@@ -236,7 +236,7 @@ Blockly.Tooltip.show_ = function() {
goog.dom.removeChildren(/** @type {!Element} */ (Blockly.Tooltip.DIV));
// Get the new text.
var tip = Blockly.Tooltip.element_.tooltip;
- if (goog.isFunction(tip)) {
+ while (goog.isFunction(tip)) {
tip = tip();
}
tip = Blockly.Tooltip.wrap_(tip, Blockly.Tooltip.LIMIT);
diff --git a/core/trashcan.js b/core/trashcan.js
index b7a0781d80..429d613c66 100644
--- a/core/trashcan.js
+++ b/core/trashcan.js
@@ -81,7 +81,7 @@ Blockly.Trashcan.prototype.MARGIN_SIDE_ = 20;
* @type {number}
* @private
*/
-Blockly.Trashcan.prototype.MARGIN_HOTSPOT_ = 25;
+Blockly.Trashcan.prototype.MARGIN_HOTSPOT_ = 10;
/**
* Current open/close state of the lid.
@@ -190,7 +190,7 @@ Blockly.Trashcan.prototype.createDom = function() {
* @return {number} Distance from workspace bottom to the top of trashcan.
*/
Blockly.Trashcan.prototype.init = function(bottom) {
- this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
+ this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
this.setOpen_(false);
return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_;
};
@@ -234,13 +234,13 @@ Blockly.Trashcan.prototype.position = function() {
* Return the deletion rectangle for this trash can.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
-Blockly.Trashcan.prototype.getRect = function() {
- var trashXY = Blockly.getSvgXY_(this.svgGroup_, this.workspace_);
- return new goog.math.Rect(
- trashXY.x - this.MARGIN_HOTSPOT_,
- trashXY.y - this.MARGIN_HOTSPOT_,
- this.WIDTH_ + 2 * this.MARGIN_HOTSPOT_,
- this.BODY_HEIGHT_ + this.LID_HEIGHT_ + 2 * this.MARGIN_HOTSPOT_);
+Blockly.Trashcan.prototype.getClientRect = function() {
+ var trashRect = this.svgGroup_.getBoundingClientRect();
+ return new goog.math.Rect(trashRect.left - this.MARGIN_HOTSPOT_,
+ trashRect.top - this.MARGIN_HOTSPOT_,
+ trashRect.width + 2 * this.MARGIN_HOTSPOT_,
+ trashRect.height + 2 * this.MARGIN_HOTSPOT_);
+
};
/**
diff --git a/core/utils.js b/core/utils.js
index c8fc97c0f1..5e9ca5426c 100644
--- a/core/utils.js
+++ b/core/utils.js
@@ -34,6 +34,13 @@ goog.require('goog.math.Coordinate');
goog.require('goog.userAgent');
+/**
+ * Cached value for whether 3D is supported
+ * @type {boolean}
+ * @private
+ */
+Blockly.cache3dSupported_ = null;
+
/**
* Add a CSS class to a element.
* Similar to Closure's goog.dom.classes.add, except it handles SVG elements.
@@ -163,66 +170,6 @@ Blockly.unbindEvent_ = function(bindData) {
return func;
};
-/**
- * Fire a synthetic event synchronously.
- * @param {!EventTarget} node The event's target node.
- * @param {string} eventName Name of event (e.g. 'click').
- */
-Blockly.fireUiEventNow = function(node, eventName) {
- // Remove the event from the anti-duplicate database.
- var list = Blockly.fireUiEvent.DB_[eventName];
- if (list) {
- var i = list.indexOf(node);
- if (i != -1) {
- list.splice(i, 1);
- }
- }
- // Fire the event in a browser-compatible way.
- if (document.createEvent) {
- // W3
- var evt = document.createEvent('UIEvents');
- evt.initEvent(eventName, true, true); // event type, bubbling, cancelable
- node.dispatchEvent(evt);
- } else if (document.createEventObject) {
- // MSIE
- var evt = document.createEventObject();
- node.fireEvent('on' + eventName, evt);
- } else {
- throw 'FireEvent: No event creation mechanism.';
- }
-};
-
-/**
- * Fire a synthetic event asynchronously. Groups of simultaneous events (e.g.
- * a tree of blocks being deleted) are merged into one event.
- * @param {!EventTarget} node The event's target node.
- * @param {string} eventName Name of event (e.g. 'click').
- */
-Blockly.fireUiEvent = function(node, eventName) {
- var list = Blockly.fireUiEvent.DB_[eventName];
- if (list) {
- if (list.indexOf(node) != -1) {
- // This event is already scheduled to fire.
- return;
- }
- list.push(node);
- } else {
- Blockly.fireUiEvent.DB_[eventName] = [node];
- }
- var fire = function() {
- Blockly.fireUiEventNow(node, eventName);
- };
- setTimeout(fire, 0);
-};
-
-/**
- * Database of upcoming firing event types.
- * Used to fire only one event after multiple changes.
- * @type {!Object}
- * @private
- */
-Blockly.fireUiEvent.DB_ = {};
-
/**
* Don't do anything for this event, just halt propagation.
* @param {!Event} e An event.
@@ -267,13 +214,28 @@ Blockly.getRelativeXY_ = function(element) {
}
// Second, check for transform="translate(...)" attribute.
var transform = element.getAttribute('transform');
- var r = transform && transform.match(Blockly.getRelativeXY_.XY_REGEXP_);
- if (r) {
- xy.x += parseFloat(r[1]);
- if (r[3]) {
- xy.y += parseFloat(r[3]);
+ if (transform) {
+ var transformComponents = transform.match(Blockly.getRelativeXY_.XY_REGEXP_);
+ if (transformComponents) {
+ xy.x += parseFloat(transformComponents[1]);
+ if (transformComponents[3]) {
+ xy.y += parseFloat(transformComponents[3]);
+ }
}
}
+
+ // Third, check for style="transform: translate3d(...)".
+ var style = element.getAttribute('style');
+ if (style && style.indexOf('translate3d') > -1) {
+ var styleComponents = style.match(Blockly.getRelativeXY_.XY_3D_REGEXP_);
+ if (styleComponents) {
+ xy.x += parseFloat(styleComponents[1]);
+ if (styleComponents[3]) {
+ xy.y += parseFloat(styleComponents[3]);
+ }
+ }
+ }
+
return xy;
};
@@ -289,6 +251,15 @@ Blockly.getRelativeXY_ = function(element) {
Blockly.getRelativeXY_.XY_REGEXP_ =
/translate\(\s*([-+\d.e]+)([ ,]\s*([-+\d.e]+)\s*\))?/;
+/**
+ * Static regex to pull the x,y,z values out of a translate3d() style property.
+ * Accounts for same exceptions as XY_REGEXP_.
+ * @type {!RegExp}
+ * @private
+ */
+Blockly.getRelativeXY_.XY_3D_REGEXP_ =
+ /transform:\s*translate3d\(\s*([-+\d.e]+)px([ ,]\s*([-+\d.e]+)\s*)px([ ,]\s*([-+\d.e]+)\s*)px\)?/;
+
/**
* Return the absolute coordinates of the top-left corner of this element,
* scales that after canvas SVG element, if it's a descendant.
@@ -322,6 +293,46 @@ Blockly.getSvgXY_ = function(element, workspace) {
return new goog.math.Coordinate(x, y);
};
+/**
+ * Check if 3D transforms are supported by adding an element
+ * and attempting to set the property.
+ * @return {boolean} true if 3D transforms are supported
+ */
+Blockly.is3dSupported = function() {
+ if (Blockly.cache3dSupported_ !== null) {
+ return Blockly.cache3dSupported_;
+ }
+ // CC-BY-SA Lorenzo Polidori
+ // https://stackoverflow.com/questions/5661671/detecting-transform-translate3d-support
+ if (!window.getComputedStyle) {
+ return false;
+ }
+
+ var el = document.createElement('p'),
+ has3d,
+ transforms = {
+ 'webkitTransform':'-webkit-transform',
+ 'OTransform':'-o-transform',
+ 'msTransform':'-ms-transform',
+ 'MozTransform':'-moz-transform',
+ 'transform':'transform'
+ };
+
+ // Add it to the body to get the computed style.
+ document.body.insertBefore(el, null);
+
+ for (var t in transforms) {
+ if (el.style[t] !== undefined) {
+ el.style[t] = "translate3d(1px,1px,1px)";
+ has3d = window.getComputedStyle(el).getPropertyValue(transforms[t]);
+ }
+ }
+
+ document.body.removeChild(el);
+ Blockly.cache3dSupported_ = (has3d !== undefined && has3d.length > 0 && has3d !== "none");
+ return Blockly.cache3dSupported_;
+};
+
/**
* Helper method for creating SVG elements.
* @param {string} name Element's tag name.
@@ -349,26 +360,6 @@ Blockly.createSvgElement = function(name, attrs, parent, opt_workspace) {
return e;
};
-/**
- * Deselect any selections on the webpage.
- * Chrome will select text outside the SVG when double-clicking.
- * Deselect this text, so that it doesn't mess up any subsequent drag.
- */
-Blockly.removeAllRanges = function() {
- if (window.getSelection) {
- setTimeout(function() {
- try {
- var selection = window.getSelection();
- if (!selection.isCollapsed) {
- selection.removeAllRanges();
- }
- } catch (e) {
- // MSIE throws 'error 800a025e' here.
- }
- }, 0);
- }
-};
-
/**
* Is this event a right-click?
* @param {!Event} e Mouse event.
@@ -554,40 +545,39 @@ Blockly.tokenizeInterpolation = function(message) {
/**
* Generate a unique ID. This should be globally unique.
- * 88 characters ^ 20 length ≈ 129 bits (one bit better than a UUID).
- * @return {string}
+ * 87 characters ^ 20 length > 128 bits (better than a UUID).
+ * @return {string} A globally unique ID string.
*/
Blockly.genUid = function() {
var length = 20;
var soupLength = Blockly.genUid.soup_.length;
var id = [];
- if (Blockly.genUid.crypto_) {
- // Cryptographically strong randomness is supported.
- var array = new Uint32Array(length);
- Blockly.genUid.crypto_.getRandomValues(array);
- for (var i = 0; i < length; i++) {
- id[i] = Blockly.genUid.soup_.charAt(array[i] % soupLength);
- }
- } else {
- // Fall back to Math.random for IE 10.
- for (var i = 0; i < length; i++) {
- id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength);
- }
+ for (var i = 0; i < length; i++) {
+ id[i] = Blockly.genUid.soup_.charAt(Math.random() * soupLength);
}
return id.join('');
};
-/**
- * Determine if window.crypto or global.crypto exists.
- * @type {=RandomSource}
- * @private
- */
-Blockly.genUid.crypto_ = this.crypto;
-
/**
* Legal characters for the unique ID.
* Should be all on a US keyboard. No XML special characters or control codes.
+ * Removed $ due to issue 251.
* @private
*/
-Blockly.genUid.soup_ = '!#$%()*+,-./:;=?@[]^_`{|}~' +
+Blockly.genUid.soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+/**
+ * Measure some text using a canvas in-memory.
+ * @param {string} fontSize E.g., '10pt'
+ * @param {string} fontFamily E.g., 'Arial'
+ * @param {string} fontWeight E.g., '600'
+ * @param {string} text The actual text to measure
+ * @return {number} Width of the text in px.
+ */
+Blockly.measureText = function(fontSize, fontFamily, fontWeight, text) {
+ var canvas = document.createElement('canvas');
+ var context = canvas.getContext('2d');
+ context.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;
+ return context.measureText(text).width;
+};
diff --git a/core/variables.js b/core/variables.js
index db506fb9cf..12b8eeb06e 100644
--- a/core/variables.js
+++ b/core/variables.js
@@ -56,14 +56,12 @@ Blockly.Variables.allVariables = function(root) {
var variableHash = Object.create(null);
// Iterate through every block and add each variable to the hash.
for (var x = 0; x < blocks.length; x++) {
- if (blocks[x].getVars) {
- var blockVariables = blocks[x].getVars();
- for (var y = 0; y < blockVariables.length; y++) {
- var varName = blockVariables[y];
- // Variable name may be null if the block is only half-built.
- if (varName) {
- variableHash[varName.toLowerCase()] = varName;
- }
+ var blockVariables = blocks[x].getVars();
+ for (var y = 0; y < blockVariables.length; y++) {
+ var varName = blockVariables[y];
+ // Variable name may be null if the block is only half-built.
+ if (varName) {
+ variableHash[varName.toLowerCase()] = varName;
}
}
}
@@ -82,13 +80,13 @@ Blockly.Variables.allVariables = function(root) {
* @param {!Blockly.Workspace} workspace Workspace rename variables in.
*/
Blockly.Variables.renameVariable = function(oldName, newName, workspace) {
+ Blockly.Events.setGroup(true);
var blocks = workspace.getAllBlocks();
// Iterate through every block.
for (var i = 0; i < blocks.length; i++) {
- if (blocks[i].renameVar) {
- blocks[i].renameVar(oldName, newName);
- }
+ blocks[i].renameVar(oldName, newName);
}
+ Blockly.Events.setGroup(false);
};
/**
diff --git a/core/warning.js b/core/warning.js
index 8d990b1d28..bffbf06df0 100644
--- a/core/warning.js
+++ b/core/warning.js
@@ -105,13 +105,14 @@ Blockly.Warning.prototype.setVisible = function(visible) {
// No change.
return;
}
+ Blockly.Events.fire(
+ new Blockly.Events.Ui(this.block_, 'warningOpen', !visible, visible));
if (visible) {
// Create the bubble to display all warnings.
var paragraph = Blockly.Warning.textToDom_(this.getText());
this.bubble_ = new Blockly.Bubble(
- /** @type {!Blockly.Workspace} */ (this.block_.workspace),
- paragraph, this.block_.svgPath_,
- this.iconX_, this.iconY_, null, null);
+ /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace),
+ paragraph, this.block_.svgPath_, this.iconXY_, null, null);
if (this.block_.RTL) {
// Right-align the paragraph.
// This cannot be done until the bubble is rendered on screen.
diff --git a/core/widgetdiv.js b/core/widgetdiv.js
index 3342aa3654..5b81f4efe2 100644
--- a/core/widgetdiv.js
+++ b/core/widgetdiv.js
@@ -48,11 +48,36 @@ Blockly.WidgetDiv.owner_ = null;
/**
* Optional cleanup function set by whichever object uses the widget.
+ * This is called as soon as a dispose is desired. If the dispose should
+ * be animated, the animation should start on the call of dispose_.
* @type {Function}
* @private
*/
Blockly.WidgetDiv.dispose_ = null;
+/**
+ * Optional function called at the end of a dispose animation.
+ * Set by whichever object is using the widget.
+ * @type {Function}
+ * @private
+ */
+Blockly.WidgetDiv.disposeAnimationFinished_ = null;
+
+/**
+ * Timer ID for the dispose animation.
+ * @type {number}
+ * @private
+ */
+Blockly.WidgetDiv.disposeAnimationTimer_ = null;
+
+/**
+ * Length of time in seconds for the dispose animation.
+ * @type {number}
+ * @private
+ */
+Blockly.WidgetDiv.disposeAnimationTimerLength_ = 0;
+
+
/**
* Create the widget div and inject it onto the page.
*/
@@ -69,13 +94,20 @@ Blockly.WidgetDiv.createDom = function() {
* Initialize and display the widget div. Close the old one if needed.
* @param {!Object} newOwner The object that will be using this container.
* @param {boolean} rtl Right-to-left (true) or left-to-right (false).
- * @param {Function} dispose Optional cleanup function to be run when the widget
- * is closed.
+ * @param {Function=} opt_dispose Optional cleanup function to be run when the widget
+ * is closed. If the dispose is animated, this function must start the animation.
+ * @param {Function=} opt_disposeAnimationFinished Optional cleanup function to be run
+ * when the widget is done animating and must disappear.
+ * @param {number=} opt_disposeAnimationTimerLength Length of animation time in seconds
+ if a dispose animation is provided.
*/
-Blockly.WidgetDiv.show = function(newOwner, rtl, dispose) {
+Blockly.WidgetDiv.show = function(newOwner, rtl, opt_dispose,
+ opt_disposeAnimationFinished, opt_disposeAnimationTimerLength) {
Blockly.WidgetDiv.hide();
Blockly.WidgetDiv.owner_ = newOwner;
- Blockly.WidgetDiv.dispose_ = dispose;
+ Blockly.WidgetDiv.dispose_ = opt_dispose;
+ Blockly.WidgetDiv.disposeAnimationFinished_ = opt_disposeAnimationFinished;
+ Blockly.WidgetDiv.disposeAnimationTimerLength_ = opt_disposeAnimationTimerLength;
// Temporarily move the widget to the top of the screen so that it does not
// cause a scrollbar jump in Firefox when displayed.
var xy = goog.style.getViewportPageOffset(document);
@@ -86,20 +118,54 @@ Blockly.WidgetDiv.show = function(newOwner, rtl, dispose) {
/**
* Destroy the widget and hide the div.
+ * @param {boolean=} opt_noAnimate If set, animation will not be run for the hide.
*/
-Blockly.WidgetDiv.hide = function() {
- if (Blockly.WidgetDiv.owner_) {
- Blockly.WidgetDiv.DIV.style.display = 'none';
- Blockly.WidgetDiv.DIV.style.left = '';
- Blockly.WidgetDiv.DIV.style.top = '';
- Blockly.WidgetDiv.DIV.style.height = '';
- Blockly.WidgetDiv.dispose_ && Blockly.WidgetDiv.dispose_();
+Blockly.WidgetDiv.hide = function(opt_noAnimate) {
+ if (Blockly.WidgetDiv.disposeAnimationTimer_) {
+ // An animation timer is set already.
+ // This happens when a previous widget was animating out,
+ // but Blockly is hiding the widget to create a new one.
+ // So, short-circuit the animation and clear the timer.
+ window.clearTimeout(Blockly.WidgetDiv.disposeAnimationTimer_);
+ Blockly.WidgetDiv.disposeAnimationFinished_ && Blockly.WidgetDiv.disposeAnimationFinished_();
+ Blockly.WidgetDiv.disposeAnimationFinished_ = null;
+ Blockly.WidgetDiv.disposeAnimationTimer_ = null;
Blockly.WidgetDiv.owner_ = null;
+ Blockly.WidgetDiv.hideAndClearDom_();
+ } else if (Blockly.WidgetDiv.isVisible()) {
+ // No animation timer set, but the widget is visible
+ // Start animation out (or immediately hide)
+ Blockly.WidgetDiv.dispose_ && Blockly.WidgetDiv.dispose_();
Blockly.WidgetDiv.dispose_ = null;
- goog.dom.removeChildren(Blockly.WidgetDiv.DIV);
+ // If we want to animate out, set the appropriate timer for final dispose.
+ if (Blockly.WidgetDiv.disposeAnimationFinished_ && !opt_noAnimate) {
+ Blockly.WidgetDiv.disposeAnimationTimer_ = window.setTimeout(
+ Blockly.WidgetDiv.hide, // Come back to hide and take the first branch.
+ Blockly.WidgetDiv.disposeAnimationTimerLength_ * 1000
+ );
+ } else {
+ // No timer provided (or no animation desired) - auto-hide the DOM now.
+ Blockly.WidgetDiv.disposeAnimationFinished_ && Blockly.WidgetDiv.disposeAnimationFinished_();
+ Blockly.WidgetDiv.disposeAnimationFinished_ = null;
+ Blockly.WidgetDiv.owner_ = null;
+ Blockly.WidgetDiv.hideAndClearDom_();
+ }
+ Blockly.Events.setGroup(false);
}
};
+/**
+ * Hide all DOM for the WidgetDiv, and clear its children.
+ * @private
+ */
+Blockly.WidgetDiv.hideAndClearDom_ = function() {
+ Blockly.WidgetDiv.DIV.style.display = 'none';
+ Blockly.WidgetDiv.DIV.style.left = '';
+ Blockly.WidgetDiv.DIV.style.top = '';
+ Blockly.WidgetDiv.DIV.style.height = '';
+ goog.dom.removeChildren(Blockly.WidgetDiv.DIV);
+};
+
/**
* Is the container visible?
* @return {boolean} True if visible.
diff --git a/core/workspace.js b/core/workspace.js
index 400412a314..e6564fc510 100644
--- a/core/workspace.js
+++ b/core/workspace.js
@@ -32,19 +32,51 @@ goog.require('goog.math');
/**
* Class for a workspace. This is a data structure that contains blocks.
* There is no UI, and can be created headlessly.
- * @param {Object=} opt_options Dictionary of options.
+ * @param {Blockly.Options} opt_options Dictionary of options.
* @constructor
*/
Blockly.Workspace = function(opt_options) {
/** @type {string} */
this.id = Blockly.genUid();
Blockly.Workspace.WorkspaceDB_[this.id] = this;
- /** @type {!Object} */
+ /** @type {!Blockly.Options} */
this.options = opt_options || {};
/** @type {boolean} */
this.RTL = !!this.options.RTL;
- /** @type {!Array.} */
+ /** @type {boolean} */
+ this.horizontalLayout = !!this.options.horizontalLayout;
+
+ /**
+ * @type {!Array.}
+ * @private
+ */
this.topBlocks_ = [];
+ /**
+ * @type {!Array.}
+ * @private
+ */
+ this.listeners_ = [];
+
+ /** @type {!Array.} */
+ this.tapListeners_ = [];
+
+ /**
+ * @type {!Array.}
+ * @private
+ */
+ this.undoStack_ = [];
+
+ /**
+ * @type {!Array.}
+ * @private
+ */
+ this.redoStack_ = [];
+
+ /**
+ * @type {!Object}
+ * @private
+ */
+ this.blockDB_ = Object.create(null);
};
/**
@@ -53,11 +85,18 @@ Blockly.Workspace = function(opt_options) {
*/
Blockly.Workspace.prototype.rendered = false;
+/**
+ * Maximum number of undo events in stack.
+ * @type {number} 0 to turn off undo, Infinity for unlimited.
+ */
+Blockly.Workspace.prototype.MAX_UNDO = 1024;
+
/**
* Dispose of this workspace.
* Unlink from all DOM elements to prevent memory leaks.
*/
Blockly.Workspace.prototype.dispose = function() {
+ this.listeners_.length = 0;
this.clear();
// Remove from workspace database.
delete Blockly.Workspace.WorkspaceDB_[this.id];
@@ -77,7 +116,6 @@ Blockly.Workspace.SCAN_ANGLE = 3;
*/
Blockly.Workspace.prototype.addTopBlock = function(block) {
this.topBlocks_.push(block);
- this.fireChangeEvent();
};
/**
@@ -96,7 +134,6 @@ Blockly.Workspace.prototype.removeTopBlock = function(block) {
if (!found) {
throw 'Block not present in workspace\'s list of top-most blocks.';
}
- this.fireChangeEvent();
};
/**
@@ -138,9 +175,16 @@ Blockly.Workspace.prototype.getAllBlocks = function() {
* Dispose of all blocks in workspace.
*/
Blockly.Workspace.prototype.clear = function() {
+ var existingGroup = Blockly.Events.getGroup();
+ if (!existingGroup) {
+ Blockly.Events.setGroup(true);
+ }
while (this.topBlocks_.length) {
this.topBlocks_[0].dispose();
}
+ if (!existingGroup) {
+ Blockly.Events.setGroup(false);
+ }
};
/**
@@ -165,22 +209,6 @@ Blockly.Workspace.prototype.newBlock = function(prototypeName, opt_id) {
return new Blockly.Block(this, prototypeName, opt_id);
};
-/**
- * Finds the block with the specified ID in this workspace.
- * @param {string} id ID of block to find.
- * @return {Blockly.Block} The matching block, or null if not found.
- */
-Blockly.Workspace.prototype.getBlockById = function(id) {
- // If this O(n) function fails to scale well, maintain a hash table of IDs.
- var blocks = this.getAllBlocks();
- for (var i = 0, block; block = blocks[i]; i++) {
- if (block.id == id) {
- return block;
- }
- }
- return null;
-};
-
/**
* The number of blocks that may be added to the workspace before reaching
* the maxBlocks.
@@ -194,10 +222,124 @@ Blockly.Workspace.prototype.remainingCapacity = function() {
};
/**
- * Something on this workspace has changed.
+ * Undo or redo the previous action.
+ * @param {boolean} redo False if undo, true if redo.
+ */
+Blockly.Workspace.prototype.undo = function(redo) {
+ var inputStack = redo ? this.redoStack_ : this.undoStack_;
+ var outputStack = redo ? this.undoStack_ : this.redoStack_;
+ var inputEvent = inputStack.pop();
+ if (!inputEvent) {
+ return;
+ }
+ var events = [inputEvent];
+ // Do another undo/redo if the next one is of the same group.
+ while (inputStack.length && inputEvent.group &&
+ inputEvent.group == inputStack[inputStack.length - 1].group) {
+ events.push(inputStack.pop());
+ }
+ // Push these popped events on the opposite stack.
+ for (var i = 0, event; event = events[i]; i++) {
+ outputStack.push(event);
+ }
+ events = Blockly.Events.filter(events, redo);
+ Blockly.Events.recordUndo = false;
+ for (var i = 0, event; event = events[i]; i++) {
+ event.run(redo);
+ }
+ Blockly.Events.recordUndo = true;
+};
+
+/**
+ * Clear the undo/redo stacks.
+ */
+Blockly.Workspace.prototype.clearUndo = function() {
+ this.undoStack_.length = 0;
+ this.redoStack_.length = 0;
+ // Stop any events already in the firing queue from being undoable.
+ Blockly.Events.clearPendingUndo();
+};
+
+/**
+ * When something in this workspace changes, call a function.
+ * @param {!Function} func Function to call.
+ * @return {!Function} Function that can be passed to
+ * removeChangeListener.
*/
-Blockly.Workspace.prototype.fireChangeEvent = function() {
- // NOP.
+Blockly.Workspace.prototype.addChangeListener = function(func) {
+ this.listeners_.push(func);
+ return func;
+};
+
+/**
+ * Stop listening for this workspace's changes.
+ * @param {Function} func Function to stop calling.
+ */
+Blockly.Workspace.prototype.removeChangeListener = function(func) {
+ var i = this.listeners_.indexOf(func);
+ if (i != -1) {
+ this.listeners_.splice(i, 1);
+ }
+};
+
+/**
+ * Fire a change event.
+ * @param {!Blockly.Events.Abstract} event Event to fire.
+ */
+Blockly.Workspace.prototype.fireChangeListener = function(event) {
+ if (event.recordUndo) {
+ this.undoStack_.push(event);
+ this.redoStack_.length = 0;
+ if (this.undoStack_.length > this.MAX_UNDO) {
+ this.undoStack_.unshift();
+ }
+ }
+ for (var i = 0, func; func = this.listeners_[i]; i++) {
+ func(event);
+ }
+};
+
+/**
+ * When a block in the workspace is tapped, call a function with the
+ * blockId and root blockId.
+ * @param {!Function} func Function to call.
+ * @return {!Function} Function that can be passed to
+ * removeTapListener.
+ */
+Blockly.Workspace.prototype.addTapListener = function(func) {
+ this.tapListeners_.push(func);
+ return func;
+};
+
+/**
+ * Stop listening for this workspace's taps.
+ * @param {Function} func Function to stop calling.
+ */
+Blockly.Workspace.prototype.removeTapListener = function(func) {
+ var i = this.tapListeners_.indexOf(func);
+ if (i != -1) {
+ this.tapListeners_.splice(i, 1);
+ }
+};
+
+/**
+ * Fire a tap event.
+ * @param {string} blockId ID of block that was tapped
+ * @param {string} rootBlockId ID of root block in tree that was tapped
+ */
+Blockly.Workspace.prototype.fireTapListener = function(blockId, rootBlockId) {
+ for (var i = 0, func; func = this.tapListeners_[i]; i++) {
+ func(blockId, rootBlockId);
+ }
+};
+
+/**
+ * Find the block on this workspace with the specified ID.
+ * @param {string} id ID of block to find.
+ * @return {Blockly.Block} The sought after block or null if not found.
+ */
+Blockly.Workspace.prototype.getBlockById = function(id) {
+ return this.blockDB_[id] || null;
};
/**
@@ -217,3 +359,9 @@ Blockly.Workspace.getById = function(id) {
// Export symbols that would otherwise be renamed by Closure compiler.
Blockly.Workspace.prototype['clear'] = Blockly.Workspace.prototype.clear;
+Blockly.Workspace.prototype['clearUndo'] =
+ Blockly.Workspace.prototype.clearUndo;
+Blockly.Workspace.prototype['addChangeListener'] =
+ Blockly.Workspace.prototype.addChangeListener;
+Blockly.Workspace.prototype['removeChangeListener'] =
+ Blockly.Workspace.prototype.removeChangeListener;
diff --git a/core/workspace_svg.js b/core/workspace_svg.js
index 185255a541..ba96540b47 100644
--- a/core/workspace_svg.js
+++ b/core/workspace_svg.js
@@ -28,11 +28,14 @@ goog.provide('Blockly.WorkspaceSvg');
// TODO(scr): Fix circular dependencies
// goog.require('Blockly.Block');
+goog.require('Blockly.ConnectionDB');
+goog.require('Blockly.Events');
+goog.require('Blockly.Options');
goog.require('Blockly.ScrollbarPair');
goog.require('Blockly.Trashcan');
-goog.require('Blockly.ZoomControls');
goog.require('Blockly.Workspace');
goog.require('Blockly.Xml');
+goog.require('Blockly.ZoomControls');
goog.require('goog.dom');
goog.require('goog.math.Coordinate');
@@ -42,30 +45,29 @@ goog.require('goog.userAgent');
/**
* Class for a workspace. This is an onscreen area with optional trashcan,
* scrollbars, bubbles, and dragging.
- * @param {!Object} options Dictionary of options.
+ * @param {!Blockly.Options} options Dictionary of options.
+ * @param {Blockly.DragSurfaceSvg=} opt_dragSurface Drag surface for the workspace.
* @extends {Blockly.Workspace}
* @constructor
*/
-Blockly.WorkspaceSvg = function(options) {
+Blockly.WorkspaceSvg = function(options, opt_dragSurface) {
Blockly.WorkspaceSvg.superClass_.constructor.call(this, options);
this.getMetrics = options.getMetrics;
this.setMetrics = options.setMetrics;
Blockly.ConnectionDB.init(this);
+ if (opt_dragSurface) {
+ this.dragSurface = opt_dragSurface;
+ }
+
+ Blockly.ConnectionDB.init(this);
/**
* Database of pre-loaded sounds.
* @private
* @const
*/
this.SOUNDS_ = Object.create(null);
-
- /**
- * Opaque data that can be passed to Blockly.unbindEvent_.
- * @type {!Array.}
- * @private
- */
- this.eventWrappers_ = [];
};
goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace);
@@ -112,18 +114,11 @@ Blockly.WorkspaceSvg.prototype.startScrollX = 0;
Blockly.WorkspaceSvg.prototype.startScrollY = 0;
/**
- * Horizontal distance from mouse to object being dragged.
- * @type {number}
- * @private
- */
-Blockly.WorkspaceSvg.prototype.dragDeltaX_ = 0;
-
-/**
- * Vertical distance from mouse to object being dragged.
- * @type {number}
+ * Distance from mouse to object being dragged.
+ * @type {goog.math.Coordinate}
* @private
*/
-Blockly.WorkspaceSvg.prototype.dragDeltaY_ = 0;
+Blockly.WorkspaceSvg.prototype.dragDeltaXY_ = null;
/**
* Current scale.
@@ -143,6 +138,12 @@ Blockly.WorkspaceSvg.prototype.trashcan = null;
*/
Blockly.WorkspaceSvg.prototype.scrollbar = null;
+/**
+ * This workspace's drag surface, if it exists.
+ * @type {Blockly.DragSurfaceSvg}
+ */
+Blockly.WorkspaceSvg.prototype.dragSurface = null;
+
/**
* Create the workspace DOM elements.
* @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
@@ -212,7 +213,6 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
Blockly.WorkspaceSvg.prototype.dispose = function() {
// Stop rerendering.
this.rendered = false;
- Blockly.unbindEvent_(this.eventWrappers_);
Blockly.WorkspaceSvg.superClass_.dispose.call(this);
if (this.svgGroup_) {
goog.dom.removeNode(this.svgGroup_);
@@ -250,7 +250,7 @@ Blockly.WorkspaceSvg.prototype.dispose = function() {
* Obtain a newly created block.
* @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
+ * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new id.
* @return {!Blockly.BlockSvg} The created block.
*/
@@ -294,7 +294,9 @@ Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
var workspaceOptions = {
disabledPatternId: this.options.disabledPatternId,
parentWorkspace: this,
- RTL: this.RTL
+ RTL: this.RTL,
+ horizontalLayout: this.horizontalLayout,
+ toolboxPosition: this.options.toolboxPosition
};
/** @type {Blockly.Flyout} */
this.flyout_ = new Blockly.Flyout(workspaceOptions);
@@ -369,6 +371,9 @@ Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
'scale(' + this.scale + ')';
this.svgBlockCanvas_.setAttribute('transform', translation);
this.svgBubbleCanvas_.setAttribute('transform', translation);
+ if (this.dragSurface) {
+ this.dragSurface.translateAndScaleGroup(x, y, this.scale);
+ }
};
/**
@@ -377,7 +382,8 @@ Blockly.WorkspaceSvg.prototype.translate = function(x, y) {
* @return {number} Width.
*/
Blockly.WorkspaceSvg.prototype.getWidth = function() {
- return this.getMetrics().viewWidth;
+ var metrics = this.getMetrics();
+ return metrics ? metrics.viewWidth / this.scale : 0;
};
/**
@@ -398,6 +404,7 @@ Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) {
}
} else {
Blockly.hideChaff(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
}
};
@@ -425,7 +432,7 @@ Blockly.WorkspaceSvg.prototype.traceOn = function(armed) {
}
if (armed) {
this.traceWrapper_ = Blockly.bindEvent_(this.svgBlockCanvas_,
- 'blocklySelectChange', this, function() {this.traceOn_ = false});
+ 'blocklySelectChange', this, function() {this.traceOn_ = false; });
}
};
@@ -434,7 +441,7 @@ Blockly.WorkspaceSvg.prototype.traceOn = function(armed) {
* @param {?string} id ID of block to find.
*/
Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
- if (this.traceOn_ && Blockly.dragMode_ != 0) {
+ if (this.traceOn_ && Blockly.dragMode_ != Blockly.DRAG_NONE) {
// The blocklySelectChange event normally prevents this, but sometimes
// there is a race condition on fast-executing apps.
this.traceOn(false);
@@ -444,7 +451,7 @@ Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
}
var block = null;
if (id) {
- block = Blockly.Block.getById(id);
+ block = this.getBlockById(id);
if (!block) {
return;
}
@@ -464,16 +471,35 @@ Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) {
};
/**
- * Fire a change event for this workspace. Changes include new block, dropdown
- * edits, mutations, connections, etc. Groups of simultaneous changes (e.g.
- * a tree of blocks being deleted) are merged into one event.
- * Applications may hook workspace changes by listening for
- * 'blocklyWorkspaceChange' on workspace.getCanvas().
+ * Glow/unglow a block in the workspace.
+ * @param {?string} id ID of block to find.
+ * @param {boolean} isGlowingBlock Whether to glow the block.
*/
-Blockly.WorkspaceSvg.prototype.fireChangeEvent = function() {
- if (this.rendered && this.svgBlockCanvas_) {
- Blockly.fireUiEvent(this.svgBlockCanvas_, 'blocklyWorkspaceChange');
+Blockly.WorkspaceSvg.prototype.glowBlock = function(id, isGlowingBlock) {
+ var block = null;
+ if (id) {
+ block = this.getBlockById(id);
+ if (!block) {
+ throw 'Tried to glow block that does not exist.';
+ }
}
+ block.setGlowBlock(isGlowingBlock);
+};
+
+/**
+ * Glow/unglow a stack in the workspace.
+ * @param {?string} id ID of block which starts the stack.
+ * @param {boolean} isGlowingStack Whether to glow the stack.
+ */
+Blockly.WorkspaceSvg.prototype.glowStack = function(id, isGlowingStack) {
+ var block = null;
+ if (id) {
+ block = this.getBlockById(id);
+ if (!block) {
+ throw 'Tried to glow stack on block that does not exist.';
+ }
+ }
+ block.setGlowStack(isGlowingStack);
};
/**
@@ -486,7 +512,8 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
return;
}
Blockly.terminateDrag_(); // Dragging while pasting? No.
- var block = Blockly.Xml.domToBlock(this, xmlBlock);
+ Blockly.Events.disable();
+ var block = Blockly.Xml.domToBlock(xmlBlock, this);
// Move the duplicate to original position.
var blockX = parseInt(xmlBlock.getAttribute('x'), 10);
var blockY = parseInt(xmlBlock.getAttribute('y'), 10);
@@ -511,8 +538,8 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
// Check for blocks in snap range to any of its connections.
var connections = block.getConnections_(false);
for (var i = 0, connection; connection = connections[i]; i++) {
- var neighbour =
- connection.closest(Blockly.SNAP_RADIUS, blockX, blockY);
+ var neighbour = connection.closest(Blockly.SNAP_RADIUS,
+ new goog.math.Coordinate(blockX, blockY));
if (neighbour.connection) {
collide = true;
break;
@@ -530,6 +557,10 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
} while (collide);
block.moveBy(blockX, blockY);
}
+ Blockly.Events.enable();
+ if (Blockly.Events.isEnabled() && !block.isShadow()) {
+ Blockly.Events.fire(new Blockly.Events.Create(block));
+ }
block.select();
};
@@ -538,29 +569,27 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
*/
Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() {
if (this.trashcan) {
- this.deleteAreaTrash_ = this.trashcan.getRect();
+ this.deleteAreaTrash_ = this.trashcan.getClientRect();
} else {
this.deleteAreaTrash_ = null;
}
if (this.flyout_) {
- this.deleteAreaToolbox_ = this.flyout_.getRect();
+ this.deleteAreaToolbox_ = this.flyout_.getClientRect();
} else if (this.toolbox_) {
- this.deleteAreaToolbox_ = this.toolbox_.getRect();
+ this.deleteAreaToolbox_ = this.toolbox_.getClientRect();
} else {
this.deleteAreaToolbox_ = null;
}
};
/**
- * Is the mouse event over a delete area (toolbar or non-closing flyout)?
+ * Is the mouse event over a delete area (toolbox or non-closing flyout)?
* Opens or closes the trashcan and sets the cursor as a side effect.
* @param {!Event} e Mouse move event.
* @return {boolean} True if event is in a delete area.
*/
Blockly.WorkspaceSvg.prototype.isDeleteArea = function(e) {
- var isDelete = false;
- var mouseXY = Blockly.mouseToSvg(e, Blockly.mainWorkspace.getParentSvg());
- var xy = new goog.math.Coordinate(mouseXY.x, mouseXY.y);
+ var xy = new goog.math.Coordinate(e.clientX, e.clientY);
if (this.deleteAreaTrash_) {
if (this.deleteAreaTrash_.contains(xy)) {
this.trashcan.setOpen_(true);
@@ -592,6 +621,7 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
Blockly.svgResize(this);
Blockly.terminateDrag_(); // In case mouse-up event was lost.
Blockly.hideChaff();
+ Blockly.DropDownDiv.hide();
var isTargetWorkspace = e.target && e.target.nodeName &&
(e.target.nodeName.toLowerCase() == 'svg' ||
e.target == this.svgBackground_);
@@ -603,7 +633,6 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
// Right-click.
this.showContextMenu_(e);
} else if (this.scrollbar) {
- Blockly.removeAllRanges();
// If the workspace is editable, only allow scrolling when gripping empty
// space. Otherwise, allow scrolling when gripping anywhere.
this.isScrolling = true;
@@ -619,30 +648,31 @@ Blockly.WorkspaceSvg.prototype.onMouseDown_ = function(e) {
// See comment in inject.js Blockly.init_ as to why mouseup events are
// bound to the document instead of the SVG's surface.
if ('mouseup' in Blockly.bindEvent_.TOUCH_MAP) {
- Blockly.onTouchUpWrapper_ =
- Blockly.bindEvent_(document, 'mouseup', null, Blockly.onMouseUp_);
+ Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_ || [];
+ Blockly.onTouchUpWrapper_ = Blockly.onTouchUpWrapper_.concat(
+ Blockly.bindEvent_(document, 'mouseup', null, Blockly.onMouseUp_));
}
- Blockly.onMouseMoveWrapper_ =
- Blockly.bindEvent_(document, 'mousemove', null, Blockly.onMouseMove_);
+ Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_ || [];
+ Blockly.onMouseMoveWrapper_ = Blockly.onMouseMoveWrapper_.concat(
+ Blockly.bindEvent_(document, 'mousemove', null, Blockly.onMouseMove_));
}
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
+ e.preventDefault();
};
/**
* Start tracking a drag of an object on this workspace.
* @param {!Event} e Mouse down event.
- * @param {number} x Starting horizontal location of object.
- * @param {number} y Starting vertical location of object.
+ * @param {!goog.math.Coordinate} xy Starting location of object.
*/
-Blockly.WorkspaceSvg.prototype.startDrag = function(e, x, y) {
+Blockly.WorkspaceSvg.prototype.startDrag = function(e, xy) {
// Record the starting offset between the bubble's location and the mouse.
var point = Blockly.mouseToSvg(e, this.getParentSvg());
// Fix scale of mouse event.
point.x /= this.scale;
point.y /= this.scale;
- this.dragDeltaX_ = x - point.x;
- this.dragDeltaY_ = y - point.y;
+ this.dragDeltaXY_ = goog.math.Coordinate.difference(xy, point);
};
/**
@@ -655,9 +685,7 @@ Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
// Fix scale of mouse event.
point.x /= this.scale;
point.y /= this.scale;
- var x = this.dragDeltaX_ + point.x;
- var y = this.dragDeltaY_ + point.y;
- return new goog.math.Coordinate(x, y);
+ return goog.math.Coordinate.sum(this.dragDeltaXY_, point);
};
/**
@@ -667,18 +695,72 @@ Blockly.WorkspaceSvg.prototype.moveDrag = function(e) {
*/
Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
// TODO: Remove terminateDrag and compensate for coordinate skew during zoom.
- Blockly.terminateDrag_();
- var delta = e.deltaY > 0 ? -1 : 1;
- var position = Blockly.mouseToSvg(e, this.getParentSvg());
- this.zoom(position.x, position.y, delta);
+ if (e.ctrlKey) {
+ // Pinch-to-zoom in Chrome only
+ Blockly.terminateDrag_();
+ var delta = e.deltaY > 0 ? -1 : 1;
+ var position = Blockly.mouseToSvg(e, this.getParentSvg());
+ this.zoom(position.x, position.y, delta);
+ } else {
+ // This is a regular mouse wheel event - scroll the workspace
+ // First hide the WidgetDiv without animation
+ // (mouse scroll makes field out of place with div)
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ var x = this.scrollX - e.deltaX;
+ var y = this.scrollY - e.deltaY;
+ this.startDragMetrics = this.getMetrics();
+ this.scroll(x, y);
+ }
e.preventDefault();
};
+/**
+ * Calculate the bounding box for the blocks on the workspace.
+ *
+ * @return {Object} Contains the position and size of the bounding box
+ * containing the blocks on the workspace.
+ */
+Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
+ var topBlocks = this.getTopBlocks();
+ // There are no blocks, return empty rectangle.
+ if (!topBlocks.length) {
+ return {x: 0, y: 0, width: 0, height: 0};
+ }
+
+ // Initialize boundary using the first block.
+ var boundary = topBlocks[0].getBoundingRectangle();
+
+ // Start at 1 since the 0th block was used for initialization
+ for (var i = 1; i < topBlocks.length; i++) {
+ var blockBoundary = topBlocks[i].getBoundingRectangle();
+ if (blockBoundary.topLeft.x < boundary.topLeft.x) {
+ boundary.topLeft.x = blockBoundary.topLeft.x;
+ }
+ if (blockBoundary.bottomRight.x > boundary.bottomRight.x) {
+ boundary.bottomRight.x = blockBoundary.bottomRight.x;
+ }
+ if (blockBoundary.topLeft.y < boundary.topLeft.y) {
+ boundary.topLeft.y = blockBoundary.topLeft.y;
+ }
+ if (blockBoundary.bottomRight.y > boundary.bottomRight.y) {
+ boundary.bottomRight.y = blockBoundary.bottomRight.y;
+ }
+ }
+ return {
+ x: boundary.topLeft.x,
+ y: boundary.topLeft.y,
+ width: boundary.bottomRight.x - boundary.topLeft.x,
+ height: boundary.bottomRight.y - boundary.topLeft.y
+ };
+};
+
/**
* Clean up the workspace by ordering all the blocks in a column.
* @private
*/
Blockly.WorkspaceSvg.prototype.cleanUp_ = function() {
+ Blockly.Events.setGroup(true);
var topBlocks = this.getTopBlocks(true);
var cursorY = 0;
for (var i = 0, block; block = topBlocks[i]; i++) {
@@ -688,9 +770,9 @@ Blockly.WorkspaceSvg.prototype.cleanUp_ = function() {
cursorY = block.getRelativeToSurfaceXY().y +
block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y;
}
+ Blockly.Events.setGroup(false);
// Fire an event to allow scrollbars to resize.
- Blockly.fireUiEvent(window, 'resize');
- this.fireChangeEvent();
+ Blockly.asyncSvgResize(this);
};
/**
@@ -704,12 +786,28 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
}
var menuOptions = [];
var topBlocks = this.getTopBlocks(true);
+ var eventGroup = Blockly.genUid();
+
+ // Options to undo/redo previous action.
+ var undoOption = {};
+ undoOption.text = Blockly.Msg.UNDO;
+ undoOption.enabled = this.undoStack_.length > 0;
+ undoOption.callback = this.undo.bind(this, false);
+ menuOptions.push(undoOption);
+ var redoOption = {};
+ redoOption.text = Blockly.Msg.REDO;
+ redoOption.enabled = this.redoStack_.length > 0;
+ redoOption.callback = this.undo.bind(this, true);
+ menuOptions.push(redoOption);
+
// Option to clean up blocks.
- var cleanOption = {};
- cleanOption.text = Blockly.Msg.CLEAN_UP;
- cleanOption.enabled = topBlocks.length > 1;
- cleanOption.callback = this.cleanUp_.bind(this);
- menuOptions.push(cleanOption);
+ if (this.scrollbar) {
+ var cleanOption = {};
+ cleanOption.text = Blockly.Msg.CLEAN_UP;
+ cleanOption.enabled = topBlocks.length > 1;
+ cleanOption.callback = this.cleanUp_.bind(this);
+ menuOptions.push(cleanOption);
+ }
// Add a little animation to collapsing and expanding.
var DELAY = 10;
@@ -779,7 +877,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
addDeletableBlocks(topBlocks[i]);
}
var deleteOption = {
- text: deleteList.length <= 1 ? Blockly.Msg.DELETE_BLOCK :
+ text: deleteList.length == 1 ? Blockly.Msg.DELETE_BLOCK :
Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)),
enabled: deleteList.length > 0,
callback: function() {
@@ -791,6 +889,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
}
};
function deleteNext() {
+ Blockly.Events.setGroup(eventGroup);
var block = deleteList.shift();
if (block) {
if (block.workspace) {
@@ -800,6 +899,7 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
deleteNext();
}
}
+ Blockly.Events.setGroup(false);
}
menuOptions.push(deleteOption);
@@ -865,6 +965,10 @@ Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
* @param {number=} opt_volume Volume of sound (0-1).
*/
Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
+ // Send a UI event in case we wish to play the sound externally
+ var event = new Blockly.Events.Ui(null, 'sound', null, name);
+ event.workspaceId = this.id;
+ Blockly.Events.fire(event);
var sound = this.SOUNDS_[name];
if (sound) {
var mySound;
@@ -891,18 +995,17 @@ Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
* @param {Node|string} tree DOM tree of blocks, or text representation of same.
*/
Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
- tree = Blockly.parseToolboxTree_(tree);
+ tree = Blockly.Options.parseToolboxTree(tree);
if (!tree) {
if (this.options.languageTree) {
throw 'Can\'t nullify an existing toolbox.';
}
- // No change (null to null).
- return;
+ return; // No change (null to null).
}
if (!this.options.languageTree) {
throw 'Existing toolbox is null. Can\'t create new toolbox.';
}
- if (this.options.hasCategories) {
+ if (tree.getElementsByTagName('category').length) {
if (!this.toolbox_) {
throw 'Existing toolbox has no categories. Can\'t change mode.';
}
@@ -918,31 +1021,6 @@ Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) {
}
};
-/**
- * When something in this workspace changes, call a function.
- * @param {!Function} func Function to call.
- * @return {!Array.} Opaque data that can be passed to
- * removeChangeListener.
- */
-Blockly.WorkspaceSvg.prototype.addChangeListener = function(func) {
- var wrapper = Blockly.bindEvent_(this.getCanvas(),
- 'blocklyWorkspaceChange', null, func);
- Array.prototype.push.apply(this.eventWrappers_, wrapper);
- return wrapper;
-};
-
-/**
- * Stop listening for this workspace's changes.
- * @param {!Array.} bindData Opaque data from addChangeListener.
- */
-Blockly.WorkspaceSvg.prototype.removeChangeListener = function(bindData) {
- Blockly.unbindEvent_(bindData);
- var i = this.eventWrappers_.indexOf(bindData);
- if (i != -1) {
- this.eventWrappers_.splice(i, 1);
- }
-};
-
/**
* Mark this workspace as the currently focused main workspace.
*/
@@ -979,27 +1057,21 @@ Blockly.WorkspaceSvg.prototype.zoom = function(x, y, type) {
} else if (newScale < this.options.zoomOptions.minScale) {
scaleChange = this.options.zoomOptions.minScale / this.scale;
}
- var matrix = canvas.getCTM()
- .translate(x * (1 - scaleChange), y * (1 - scaleChange))
- .scale(scaleChange);
- // newScale and matrix.a should be identical (within a rounding error).
- if (this.scale == matrix.a) {
+ if (this.scale == newScale) {
return; // No change in zoom.
}
- this.scale = matrix.a;
- this.scrollX = matrix.e - metrics.absoluteLeft;
- this.scrollY = matrix.f - metrics.absoluteTop;
- this.updateGridPattern_();
if (this.scrollbar) {
- this.scrollbar.resize();
- } else {
- this.translate(0, 0);
- }
- Blockly.hideChaff(false);
- if (this.flyout_) {
- // No toolbox, resize flyout.
- this.flyout_.reflow();
- }
+ var matrix = canvas.getCTM()
+ .translate(x * (1 - scaleChange), y * (1 - scaleChange))
+ .scale(scaleChange);
+ // newScale and matrix.a should be identical (within a rounding error).
+ this.scrollX = matrix.e - metrics.absoluteLeft;
+ this.scrollY = matrix.f - metrics.absoluteTop;
+ }
+ this.setScale(newScale);
+ // Hide the WidgetDiv without animation (zoom makes field out of place with div)
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
};
/**
@@ -1014,31 +1086,103 @@ Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) {
};
/**
- * Reset zooming and dragging.
- * @param {!Event} e Mouse down event.
+ * Zoom the blocks to fit in the workspace if possible.
*/
-Blockly.WorkspaceSvg.prototype.zoomReset = function(e) {
- this.scale = 1;
- this.updateGridPattern_();
- Blockly.hideChaff(false);
+Blockly.WorkspaceSvg.prototype.zoomToFit = function() {
+ var metrics = this.getMetrics();
+ var blocksBox = this.getBlocksBoundingBox();
+ var blocksWidth = blocksBox.width;
+ var blocksHeight = blocksBox.height;
+ if (!blocksWidth) {
+ return; // Prevents zooming to infinity.
+ }
+ var workspaceWidth = metrics.viewWidth;
+ var workspaceHeight = metrics.viewHeight;
if (this.flyout_) {
- // No toolbox, resize flyout.
- this.flyout_.reflow();
+ workspaceWidth -= this.flyout_.width_;
}
- // Zoom level has changed, update the scrollbars.
- if (this.scrollbar) {
- this.scrollbar.resize();
+ if (!this.scrollbar) {
+ // Orgin point of 0,0 is fixed, blocks will not scroll to center.
+ blocksWidth += metrics.contentLeft;
+ blocksHeight += metrics.contentTop;
+ }
+ var ratioX = workspaceWidth / blocksWidth;
+ var ratioY = workspaceHeight / blocksHeight;
+ this.setScale(Math.min(ratioX, ratioY));
+ this.scrollCenter();
+};
+
+/**
+ * Center the workspace.
+ */
+Blockly.WorkspaceSvg.prototype.scrollCenter = function() {
+ if (!this.scrollbar) {
+ // Can't center a non-scrolling workspace.
+ return;
}
- // Center the workspace.
+ // Hide the WidgetDiv without animation (zoom makes field out of place with div)
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ Blockly.hideChaff(false);
var metrics = this.getMetrics();
+ var x = (metrics.contentWidth - metrics.viewWidth) / 2;
+ if (this.flyout_) {
+ x -= this.flyout_.width_ / 2;
+ }
+ var y = (metrics.contentHeight - metrics.viewHeight) / 2;
+ this.scrollbar.set(x, y);
+};
+
+/**
+ * Set the workspace's zoom factor.
+ * @param {number} newScale Zoom factor.
+ */
+Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
+ if (this.options.zoomOptions.maxScale &&
+ newScale > this.options.zoomOptions.maxScale) {
+ newScale = this.options.zoomOptions.maxScale;
+ } else if (this.options.zoomOptions.minScale &&
+ newScale < this.options.zoomOptions.minScale) {
+ newScale = this.options.zoomOptions.minScale;
+ }
+ this.scale = newScale;
+ this.updateGridPattern_();
+ // Hide the WidgetDiv without animation (zoom makes field out of place with div)
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
if (this.scrollbar) {
- this.scrollbar.set((metrics.contentWidth - metrics.viewWidth) / 2,
- (metrics.contentHeight - metrics.viewHeight) / 2);
+ this.scrollbar.resize();
} else {
- this.translate(0, 0);
+ this.translate(this.scrollX, this.scrollY);
}
- // This event has been handled. Don't start a workspace drag.
- e.stopPropagation();
+ Blockly.hideChaff(false);
+ if (this.flyout_) {
+ // No toolbox, resize flyout.
+ this.flyout_.reflow();
+ }
+};
+
+/**
+ * Scroll the workspace by a specified amount, keeping in the bounds.
+ * Be sure to set this.startDragMetrics with cached metrics before calling.
+ * @param {number} x Target X to scroll to
+ * @param {number} y Target Y to scroll to
+ */
+Blockly.WorkspaceSvg.prototype.scroll = function(x, y) {
+ var metrics = this.startDragMetrics; // Cached values
+ x = Math.min(x, -metrics.contentLeft);
+ y = Math.min(y, -metrics.contentTop);
+ x = Math.max(x, metrics.viewWidth - metrics.contentLeft -
+ metrics.contentWidth);
+ y = Math.max(y, metrics.viewHeight - metrics.contentTop -
+ metrics.contentHeight);
+ // When the workspace starts scrolling, hide the WidgetDiv without animation.
+ // This is to prevent a dispoal animation from happening in the wrong location.
+ Blockly.WidgetDiv.hide(true);
+ Blockly.DropDownDiv.hideWithoutAnimation();
+ // Move the scrollbars and the page will scroll automatically.
+ this.scrollbar.set(-x - metrics.contentLeft,
+ -y - metrics.contentTop);
};
/**
@@ -1080,7 +1224,3 @@ Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
// Export symbols that would otherwise be renamed by Closure compiler.
Blockly.WorkspaceSvg.prototype['setVisible'] =
Blockly.WorkspaceSvg.prototype.setVisible;
-Blockly.WorkspaceSvg.prototype['addChangeListener'] =
- Blockly.WorkspaceSvg.prototype.addChangeListener;
-Blockly.WorkspaceSvg.prototype['removeChangeListener'] =
- Blockly.WorkspaceSvg.prototype.removeChangeListener;
diff --git a/core/xml.js b/core/xml.js
index fc696d7255..88837d0df5 100644
--- a/core/xml.js
+++ b/core/xml.js
@@ -37,35 +37,41 @@ goog.require('goog.dom');
* @return {!Element} XML document.
*/
Blockly.Xml.workspaceToDom = function(workspace) {
- var width; // Not used in LTR.
- if (workspace.RTL) {
- width = workspace.getWidth();
- }
var xml = goog.dom.createDom('xml');
var blocks = workspace.getTopBlocks(true);
for (var i = 0, block; block = blocks[i]; i++) {
- var element = Blockly.Xml.blockToDom_(block);
- var xy = block.getRelativeToSurfaceXY();
- element.setAttribute('x', Math.round(workspace.RTL ? width - xy.x : xy.x));
- element.setAttribute('y', Math.round(xy.y));
- xml.appendChild(element);
+ xml.appendChild(Blockly.Xml.blockToDomWithXY(block));
}
return xml;
};
+/**
+ * Encode a block subtree as XML with XY coordinates.
+ * @param {!Blockly.Block} block The root block to encode.
+ * @return {!Element} Tree of XML elements.
+ */
+Blockly.Xml.blockToDomWithXY = function(block) {
+ var width; // Not used in LTR.
+ if (block.workspace.RTL) {
+ width = block.workspace.getWidth();
+ }
+ var element = Blockly.Xml.blockToDom(block);
+ var xy = block.getRelativeToSurfaceXY();
+ element.setAttribute('x',
+ Math.round(block.workspace.RTL ? width - xy.x : xy.x));
+ element.setAttribute('y', Math.round(xy.y));
+ return element;
+};
+
/**
* Encode a block subtree as XML.
* @param {!Blockly.Block} block The root block to encode.
* @return {!Element} Tree of XML elements.
- * @private
*/
-Blockly.Xml.blockToDom_ = function(block) {
+Blockly.Xml.blockToDom = function(block) {
var element = goog.dom.createDom(block.isShadow() ? 'shadow' : 'block');
element.setAttribute('type', block.type);
- if (false) {
- // Only used by realtime.
- element.setAttribute('id', block.id);
- }
+ element.setAttribute('id', block.id);
if (block.mutationToDom) {
// Custom data for an advanced block.
var mutation = block.mutationToDom();
@@ -120,7 +126,7 @@ Blockly.Xml.blockToDom_ = function(block) {
container.appendChild(Blockly.Xml.cloneShadow_(shadow));
}
if (childBlock) {
- container.appendChild(Blockly.Xml.blockToDom_(childBlock));
+ container.appendChild(Blockly.Xml.blockToDom(childBlock));
empty = false;
}
}
@@ -151,7 +157,7 @@ Blockly.Xml.blockToDom_ = function(block) {
var nextBlock = block.getNextBlock();
if (nextBlock) {
var container = goog.dom.createDom('next', null,
- Blockly.Xml.blockToDom_(nextBlock));
+ Blockly.Xml.blockToDom(nextBlock));
element.appendChild(container);
}
var shadow = block.nextConnection && block.nextConnection.getShadowDom();
@@ -262,10 +268,17 @@ Blockly.Xml.textToDom = function(text) {
/**
* Decode an XML DOM and create blocks on the workspace.
- * @param {!Blockly.Workspace} workspace The workspace.
* @param {!Element} xml XML DOM.
+ * @param {!Blockly.Workspace} workspace The workspace.
*/
-Blockly.Xml.domToWorkspace = function(workspace, xml) {
+Blockly.Xml.domToWorkspace = function(xml, workspace) {
+ if (xml instanceof Blockly.Workspace) {
+ var swap = xml;
+ xml = workspace;
+ workspace = swap;
+ console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' +
+ 'swap the arguments.');
+ }
var width; // Not used in LTR.
if (workspace.RTL) {
width = workspace.getWidth();
@@ -275,11 +288,15 @@ Blockly.Xml.domToWorkspace = function(workspace, xml) {
// children beyond the lists' length. Trust the length, do not use the
// looping pattern of checking the index for an object.
var childCount = xml.childNodes.length;
+ var existingGroup = Blockly.Events.getGroup();
+ if (!existingGroup) {
+ Blockly.Events.setGroup(true);
+ }
for (var i = 0; i < childCount; i++) {
var xmlChild = xml.childNodes[i];
var name = xmlChild.nodeName.toLowerCase();
if (name == 'block' || name == 'shadow') {
- var block = Blockly.Xml.domToBlock(workspace, xmlChild);
+ var block = Blockly.Xml.domToBlock(xmlChild, workspace);
var blockX = parseInt(xmlChild.getAttribute('x'), 10);
var blockY = parseInt(xmlChild.getAttribute('y'), 10);
if (!isNaN(blockX) && !isNaN(blockY)) {
@@ -287,19 +304,30 @@ Blockly.Xml.domToWorkspace = function(workspace, xml) {
}
}
}
+ if (!existingGroup) {
+ Blockly.Events.setGroup(false);
+ }
Blockly.Field.stopCache();
};
/**
* Decode an XML block tag and create a block (and possibly sub blocks) on the
* workspace.
- * @param {!Blockly.Workspace} workspace The workspace.
* @param {!Element} xmlBlock XML block element.
+ * @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.Block} The root block created.
*/
-Blockly.Xml.domToBlock = function(workspace, xmlBlock) {
+Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
+ if (xmlBlock instanceof Blockly.Workspace) {
+ var swap = xmlBlock;
+ xmlBlock = workspace;
+ workspace = swap;
+ console.warn('Deprecated call to Blockly.Xml.domToBlock, ' +
+ 'swap the arguments.');
+ }
// Create top-level block.
- var topBlock = Blockly.Xml.domToBlockHeadless_(workspace, xmlBlock);
+ Blockly.Events.disable();
+ var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace);
if (workspace.rendered) {
// Hide connections to speed up assembly.
topBlock.setConnectionsHidden(true);
@@ -313,15 +341,23 @@ Blockly.Xml.domToBlock = function(workspace, xmlBlock) {
blocks[i].render(false);
}
// Populating the connection database may be defered until after the blocks
- // have renderend.
- setTimeout(function() {
- if (topBlock.workspace) { // Check that the block hasn't been deleted.
- topBlock.setConnectionsHidden(false);
- }
- }, 1);
+ // have rendered.
+ if (!workspace.isFlyout) {
+ setTimeout(function() {
+ if (topBlock.workspace) { // Check that the block hasn't been deleted.
+ topBlock.setConnectionsHidden(false);
+ }
+ }, 1);
+ }
topBlock.updateDisabled();
// Fire an event to allow scrollbars to resize.
- Blockly.fireUiEvent(window, 'resize');
+ if (!workspace.isFlyout) {
+ Blockly.asyncSvgResize(workspace);
+ }
+ }
+ Blockly.Events.enable();
+ if (Blockly.Events.isEnabled()) {
+ Blockly.Events.fire(new Blockly.Events.Create(topBlock));
}
return topBlock;
};
@@ -329,12 +365,12 @@ Blockly.Xml.domToBlock = function(workspace, xmlBlock) {
/**
* Decode an XML block tag and create a block (and possibly sub blocks) on the
* workspace.
- * @param {!Blockly.Workspace} workspace The workspace.
* @param {!Element} xmlBlock XML block element.
+ * @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.Block} The root block created.
* @private
*/
-Blockly.Xml.domToBlockHeadless_ = function(workspace, xmlBlock) {
+Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
var block = null;
var prototypeName = xmlBlock.getAttribute('type');
if (!prototypeName) {
@@ -354,7 +390,6 @@ Blockly.Xml.domToBlockHeadless_ = function(workspace, xmlBlock) {
// Find any enclosed blocks or shadows in this tag.
var childBlockNode = null;
var childShadowNode = null;
- var shadowActive = false;
for (var j = 0, grandchildNode; grandchildNode = xmlChild.childNodes[j];
j++) {
if (grandchildNode.nodeType == 1) {
@@ -368,7 +403,6 @@ Blockly.Xml.domToBlockHeadless_ = function(workspace, xmlBlock) {
// Use the shadow block if there is no child block.
if (!childBlockNode && childShadowNode) {
childBlockNode = childShadowNode;
- shadowActive = true;
}
var name = xmlChild.getAttribute('name');
@@ -429,8 +463,8 @@ Blockly.Xml.domToBlockHeadless_ = function(workspace, xmlBlock) {
input.connection.setShadowDom(childShadowNode);
}
if (childBlockNode) {
- blockChild = Blockly.Xml.domToBlockHeadless_(workspace,
- childBlockNode);
+ blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode,
+ workspace);
if (blockChild.outputConnection) {
input.connection.connect(blockChild.outputConnection);
} else if (blockChild.previousConnection) {
@@ -447,12 +481,12 @@ Blockly.Xml.domToBlockHeadless_ = function(workspace, xmlBlock) {
if (childBlockNode) {
if (!block.nextConnection) {
throw 'Next statement does not exist.';
- } else if (block.nextConnection.targetConnection) {
+ } else if (block.nextConnection.isConnected()) {
// This could happen if there is more than one XML 'next' tag.
throw 'Next statement is already connected.';
}
- blockChild = Blockly.Xml.domToBlockHeadless_(workspace,
- childBlockNode);
+ blockChild = Blockly.Xml.domToBlockHeadless_(childBlockNode,
+ workspace);
if (!blockChild.previousConnection) {
throw 'Next block does not have previous statement.';
}
diff --git a/core/zoom_controls.js b/core/zoom_controls.js
index a4b674ac76..d50110829e 100644
--- a/core/zoom_controls.js
+++ b/core/zoom_controls.js
@@ -98,17 +98,17 @@ Blockly.ZoomControls.prototype.createDom = function() {
-
+
-
+
-
+
*/
@@ -162,14 +162,21 @@ Blockly.ZoomControls.prototype.createDom = function() {
workspace.options.pathToMedia + Blockly.SPRITE.url);
// Attach event listeners.
- Blockly.bindEvent_(zoomresetSvg, 'mousedown', workspace, workspace.zoomReset);
+ Blockly.bindEvent_(zoomresetSvg, 'mousedown', null, function(e) {
+ workspace.setScale(1);
+ workspace.scrollCenter();
+ e.stopPropagation(); // Don't start a workspace scroll.
+ e.preventDefault(); // Stop double-clicking from selecting text.
+ });
Blockly.bindEvent_(zoominSvg, 'mousedown', null, function(e) {
workspace.zoomCenter(1);
e.stopPropagation(); // Don't start a workspace scroll.
+ e.preventDefault(); // Stop double-clicking from selecting text.
});
Blockly.bindEvent_(zoomoutSvg, 'mousedown', null, function(e) {
workspace.zoomCenter(-1);
e.stopPropagation(); // Don't start a workspace scroll.
+ e.preventDefault(); // Stop double-clicking from selecting text.
});
return this.svgGroup_;
diff --git a/dart_compressed.js b/dart_compressed.js
deleted file mode 100644
index 7e5976da6e..0000000000
--- a/dart_compressed.js
+++ /dev/null
@@ -1,82 +0,0 @@
-// Do not edit this file; automatically generated by build.py.
-'use strict';
-
-
-// Copyright 2014 Google Inc. Apache License 2.0
-Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert,break,case,catch,class,const,continue,default,do,else,enum,extends,false,final,finally,for,if,in,is,new,null,rethrow,return,super,switch,this,throw,true,try,var,void,while,with,print,identityHashCode,identical,BidirectionalIterator,Comparable,double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,ArgumentError,AssertionError,CastError,ConcurrentModificationError,CyclicInitializationError,Error,Exception,FallThroughError,FormatException,IntegerDivisionByZeroException,NoSuchMethodError,NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,StateError,TypeError,UnimplementedError,UnsupportedError");
-Blockly.Dart.ORDER_ATOMIC=0;Blockly.Dart.ORDER_UNARY_POSTFIX=1;Blockly.Dart.ORDER_UNARY_PREFIX=2;Blockly.Dart.ORDER_MULTIPLICATIVE=3;Blockly.Dart.ORDER_ADDITIVE=4;Blockly.Dart.ORDER_SHIFT=5;Blockly.Dart.ORDER_BITWISE_AND=6;Blockly.Dart.ORDER_BITWISE_XOR=7;Blockly.Dart.ORDER_BITWISE_OR=8;Blockly.Dart.ORDER_RELATIONAL=9;Blockly.Dart.ORDER_EQUALITY=10;Blockly.Dart.ORDER_LOGICAL_AND=11;Blockly.Dart.ORDER_LOGICAL_OR=12;Blockly.Dart.ORDER_CONDITIONAL=13;Blockly.Dart.ORDER_CASCADE=14;
-Blockly.Dart.ORDER_ASSIGNMENT=15;Blockly.Dart.ORDER_NONE=99;
-Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var b=[];a=Blockly.Variables.allVariables(a);for(var c=0;c",GTE:">="}[a.getFieldValue("OP")],c="=="==b||"!="==b?Blockly.Dart.ORDER_EQUALITY:Blockly.Dart.ORDER_RELATIONAL,d=Blockly.Dart.valueToCode(a,"A",c)||"0";a=Blockly.Dart.valueToCode(a,"B",c)||"0";return[d+" "+b+" "+a,c]};
-Blockly.Dart.logic_operation=function(a){var b="AND"==a.getFieldValue("OP")?"&&":"||",c="&&"==b?Blockly.Dart.ORDER_LOGICAL_AND:Blockly.Dart.ORDER_LOGICAL_OR,d=Blockly.Dart.valueToCode(a,"A",c);a=Blockly.Dart.valueToCode(a,"B",c);if(d||a){var e="&&"==b?"true":"false";d||(d=e);a||(a=e)}else a=d="false";return[d+" "+b+" "+a,c]};Blockly.Dart.logic_negate=function(a){var b=Blockly.Dart.ORDER_UNARY_PREFIX;return["!"+(Blockly.Dart.valueToCode(a,"BOOL",b)||"true"),b]};
-Blockly.Dart.logic_boolean=function(a){return["TRUE"==a.getFieldValue("BOOL")?"true":"false",Blockly.Dart.ORDER_ATOMIC]};Blockly.Dart.logic_null=function(a){return["null",Blockly.Dart.ORDER_ATOMIC]};Blockly.Dart.logic_ternary=function(a){var b=Blockly.Dart.valueToCode(a,"IF",Blockly.Dart.ORDER_CONDITIONAL)||"false",c=Blockly.Dart.valueToCode(a,"THEN",Blockly.Dart.ORDER_CONDITIONAL)||"null";a=Blockly.Dart.valueToCode(a,"ELSE",Blockly.Dart.ORDER_CONDITIONAL)||"null";return[b+" ? "+c+" : "+a,Blockly.Dart.ORDER_CONDITIONAL]};Blockly.Dart.loops={};
-Blockly.Dart.controls_repeat_ext=function(a){var b=a.getField("TIMES")?String(Number(a.getFieldValue("TIMES"))):Blockly.Dart.valueToCode(a,"TIMES",Blockly.Dart.ORDER_ASSIGNMENT)||"0",c=Blockly.Dart.statementToCode(a,"DO"),c=Blockly.Dart.addLoopTrap(c,a.id);a="";var d=Blockly.Dart.variableDB_.getDistinctName("count",Blockly.Variables.NAME_TYPE),e=b;b.match(/^\w+$/)||Blockly.isNumber(b)||(e=Blockly.Dart.variableDB_.getDistinctName("repeat_end",Blockly.Variables.NAME_TYPE),a+="var "+e+" = "+b+";\n");
-return a+("for (int "+d+" = 0; "+d+" < "+e+"; "+d+"++) {\n"+c+"}\n")};Blockly.Dart.controls_repeat=Blockly.Dart.controls_repeat_ext;Blockly.Dart.controls_whileUntil=function(a){var b="UNTIL"==a.getFieldValue("MODE"),c=Blockly.Dart.valueToCode(a,"BOOL",b?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_NONE)||"false",d=Blockly.Dart.statementToCode(a,"DO"),d=Blockly.Dart.addLoopTrap(d,a.id);b&&(c="!"+c);return"while ("+c+") {\n"+d+"}\n"};
-Blockly.Dart.controls_for=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),c=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_ASSIGNMENT)||"0",d=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_ASSIGNMENT)||"0",e=Blockly.Dart.valueToCode(a,"BY",Blockly.Dart.ORDER_ASSIGNMENT)||"1",f=Blockly.Dart.statementToCode(a,"DO"),f=Blockly.Dart.addLoopTrap(f,a.id);if(Blockly.isNumber(c)&&Blockly.isNumber(d)&&Blockly.isNumber(e)){var g=parseFloat(c)<=
-parseFloat(d);a="for ("+b+" = "+c+"; "+b+(g?" <= ":" >= ")+d+"; "+b;b=Math.abs(parseFloat(e));a=(1==b?a+(g?"++":"--"):a+((g?" += ":" -= ")+b))+(") {\n"+f+"}\n")}else a="",g=c,c.match(/^\w+$/)||Blockly.isNumber(c)||(g=Blockly.Dart.variableDB_.getDistinctName(b+"_start",Blockly.Variables.NAME_TYPE),a+="var "+g+" = "+c+";\n"),c=d,d.match(/^\w+$/)||Blockly.isNumber(d)||(c=Blockly.Dart.variableDB_.getDistinctName(b+"_end",Blockly.Variables.NAME_TYPE),a+="var "+c+" = "+d+";\n"),d=Blockly.Dart.variableDB_.getDistinctName(b+
-"_inc",Blockly.Variables.NAME_TYPE),a+="num "+d+" = ",a=Blockly.isNumber(e)?a+(Math.abs(e)+";\n"):a+("("+e+").abs();\n"),a+="if ("+g+" > "+c+") {\n",a+=Blockly.Dart.INDENT+d+" = -"+d+";\n",a+="}\n",a+="for ("+b+" = "+g+";\n "+d+" >= 0 ? "+b+" <= "+c+" : "+b+" >= "+c+";\n "+b+" += "+d+") {\n"+f+"}\n";return a};
-Blockly.Dart.controls_forEach=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),c=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_ASSIGNMENT)||"[]",d=Blockly.Dart.statementToCode(a,"DO"),d=Blockly.Dart.addLoopTrap(d,a.id);return"for (var "+b+" in "+c+") {\n"+d+"}\n"};
-Blockly.Dart.controls_flow_statements=function(a){switch(a.getFieldValue("FLOW")){case "BREAK":return"break;\n";case "CONTINUE":return"continue;\n"}throw"Unknown flow statement.";};Blockly.Dart.math={};Blockly.Dart.addReservedWords("Math");Blockly.Dart.math_number=function(a){a=parseFloat(a.getFieldValue("NUM"));var b;Infinity==a?(a="double.INFINITY",b=Blockly.Dart.ORDER_UNARY_POSTFIX):-Infinity==a?(a="-double.INFINITY",b=Blockly.Dart.ORDER_UNARY_PREFIX):b=0>a?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_ATOMIC;return[a,b]};
-Blockly.Dart.math_arithmetic=function(a){var b={ADD:[" + ",Blockly.Dart.ORDER_ADDITIVE],MINUS:[" - ",Blockly.Dart.ORDER_ADDITIVE],MULTIPLY:[" * ",Blockly.Dart.ORDER_MULTIPLICATIVE],DIVIDE:[" / ",Blockly.Dart.ORDER_MULTIPLICATIVE],POWER:[null,Blockly.Dart.ORDER_NONE]}[a.getFieldValue("OP")],c=b[0],b=b[1],d=Blockly.Dart.valueToCode(a,"A",b)||"0";a=Blockly.Dart.valueToCode(a,"B",b)||"0";return c?[d+c+a,b]:(Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",["Math.pow("+d+", "+a+
-")",Blockly.Dart.ORDER_UNARY_POSTFIX])};
-Blockly.Dart.math_single=function(a){var b=a.getFieldValue("OP"),c;if("NEG"==b)return a=Blockly.Dart.valueToCode(a,"NUM",Blockly.Dart.ORDER_UNARY_PREFIX)||"0","-"==a[0]&&(a=" "+a),["-"+a,Blockly.Dart.ORDER_UNARY_PREFIX];Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";a="ABS"==b||"ROUND"==b.substring(0,5)?Blockly.Dart.valueToCode(a,"NUM",Blockly.Dart.ORDER_UNARY_POSTFIX)||"0":"SIN"==b||"COS"==b||"TAN"==b?Blockly.Dart.valueToCode(a,"NUM",Blockly.Dart.ORDER_MULTIPLICATIVE)||
-"0":Blockly.Dart.valueToCode(a,"NUM",Blockly.Dart.ORDER_NONE)||"0";switch(b){case "ABS":c=a+".abs()";break;case "ROOT":c="Math.sqrt("+a+")";break;case "LN":c="Math.log("+a+")";break;case "EXP":c="Math.exp("+a+")";break;case "POW10":c="Math.pow(10,"+a+")";break;case "ROUND":c=a+".round()";break;case "ROUNDUP":c=a+".ceil()";break;case "ROUNDDOWN":c=a+".floor()";break;case "SIN":c="Math.sin("+a+" / 180 * Math.PI)";break;case "COS":c="Math.cos("+a+" / 180 * Math.PI)";break;case "TAN":c="Math.tan("+a+
-" / 180 * Math.PI)"}if(c)return[c,Blockly.Dart.ORDER_UNARY_POSTFIX];switch(b){case "LOG10":c="Math.log("+a+") / Math.log(10)";break;case "ASIN":c="Math.asin("+a+") / Math.PI * 180";break;case "ACOS":c="Math.acos("+a+") / Math.PI * 180";break;case "ATAN":c="Math.atan("+a+") / Math.PI * 180";break;default:throw"Unknown math operator: "+b;}return[c,Blockly.Dart.ORDER_MULTIPLICATIVE]};
-Blockly.Dart.math_constant=function(a){var b={PI:["Math.PI",Blockly.Dart.ORDER_UNARY_POSTFIX],E:["Math.E",Blockly.Dart.ORDER_UNARY_POSTFIX],GOLDEN_RATIO:["(1 + Math.sqrt(5)) / 2",Blockly.Dart.ORDER_MULTIPLICATIVE],SQRT2:["Math.SQRT2",Blockly.Dart.ORDER_UNARY_POSTFIX],SQRT1_2:["Math.SQRT1_2",Blockly.Dart.ORDER_UNARY_POSTFIX],INFINITY:["double.INFINITY",Blockly.Dart.ORDER_ATOMIC]};a=a.getFieldValue("CONSTANT");"INFINITY"!=a&&(Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;");
-return b[a]};
-Blockly.Dart.math_number_property=function(a){var b=Blockly.Dart.valueToCode(a,"NUMBER_TO_CHECK",Blockly.Dart.ORDER_MULTIPLICATIVE);if(!b)return["false",Blockly.Python.ORDER_ATOMIC];var c=a.getFieldValue("PROPERTY"),d;if("PRIME"==c)return Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",d=Blockly.Dart.provideFunction_("math_isPrime",["bool "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(n) {"," // https://en.wikipedia.org/wiki/Primality_test#Naive_methods"," if (n == 2 || n == 3) {"," return true;",
-" }"," // False if n is null, negative, is 1, or not whole."," // And false if n is divisible by 2 or 3."," if (n == null || n <= 1 || n % 1 != 0 || n % 2 == 0 || n % 3 == 0) {"," return false;"," }"," // Check all the numbers of form 6k +/- 1, up to sqrt(n)."," for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) {"," if (n % (x - 1) == 0 || n % (x + 1) == 0) {"," return false;"," }"," }"," return true;","}"])+"("+b+")",[d,Blockly.Dart.ORDER_UNARY_POSTFIX];switch(c){case "EVEN":d=
-b+" % 2 == 0";break;case "ODD":d=b+" % 2 == 1";break;case "WHOLE":d=b+" % 1 == 0";break;case "POSITIVE":d=b+" > 0";break;case "NEGATIVE":d=b+" < 0";break;case "DIVISIBLE_BY":a=Blockly.Dart.valueToCode(a,"DIVISOR",Blockly.Dart.ORDER_MULTIPLICATIVE);if(!a)return["false",Blockly.Python.ORDER_ATOMIC];d=b+" % "+a+" == 0"}return[d,Blockly.Dart.ORDER_EQUALITY]};
-Blockly.Dart.math_change=function(a){var b=Blockly.Dart.valueToCode(a,"DELTA",Blockly.Dart.ORDER_ADDITIVE)||"0";a=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE);return a+" = ("+a+" is num ? "+a+" : 0) + "+b+";\n"};Blockly.Dart.math_round=Blockly.Dart.math_single;Blockly.Dart.math_trig=Blockly.Dart.math_single;
-Blockly.Dart.math_on_list=function(a){var b=a.getFieldValue("OP");a=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_NONE)||"[]";switch(b){case "SUM":b=Blockly.Dart.provideFunction_("math_sum",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," num sumVal = 0;"," myList.forEach((num entry) {sumVal += entry;});"," return sumVal;","}"]);b=b+"("+a+")";break;case "MIN":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_min",
-["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num minVal = myList[0];"," myList.forEach((num entry) {minVal = Math.min(minVal, entry);});"," return minVal;","}"]);b=b+"("+a+")";break;case "MAX":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_max",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num maxVal = myList[0];",
-" myList.forEach((num entry) {maxVal = Math.max(maxVal, entry);});"," return maxVal;","}"]);b=b+"("+a+")";break;case "AVERAGE":b=Blockly.Dart.provideFunction_("math_average",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only."," List localList = new List.from(myList);"," localList.removeMatching((a) => a is! num);"," if (localList.isEmpty) return null;"," num sumVal = 0;"," localList.forEach((num entry) {sumVal += entry;});"," return sumVal / localList.length;",
-"}"]);b=b+"("+a+")";break;case "MEDIAN":b=Blockly.Dart.provideFunction_("math_median",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only, then sort, then return middle value"," // or the average of two middle values if list has an even number of elements."," List localList = new List.from(myList);"," localList.removeMatching((a) => a is! num);"," if (localList.isEmpty) return null;"," localList.sort((a, b) => (a - b));"," int index = localList.length ~/ 2;",
-" if (localList.length % 2 == 1) {"," return localList[index];"," } else {"," return (localList[index - 1] + localList[index]) / 2;"," }","}"]);b=b+"("+a+")";break;case "MODE":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_modes",["List "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List values) {"," List modes = [];"," List counts = [];"," int maxCount = 0;"," for (int i = 0; i < values.length; i++) {"," var value = values[i];",
-" bool found = false;"," int thisCount;"," for (int j = 0; j < counts.length; j++) {"," if (counts[j][0] == value) {"," thisCount = ++counts[j][1];"," found = true;"," break;"," }"," }"," if (!found) {"," counts.add([value, 1]);"," thisCount = 1;"," }"," maxCount = Math.max(thisCount, maxCount);"," }"," for (int j = 0; j < counts.length; j++) {"," if (counts[j][1] == maxCount) {"," modes.add(counts[j][0]);"," }"," }"," return modes;",
-"}"]);b=b+"("+a+")";break;case "STD_DEV":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_standard_deviation",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only."," List numbers = new List.from(myList);"," numbers.removeMatching((a) => a is! num);"," if (numbers.isEmpty) return null;"," num n = numbers.length;"," num sum = 0;"," numbers.forEach((x) => sum += x);"," num mean = sum / n;",
-" num sumSquare = 0;"," numbers.forEach((x) => sumSquare += Math.pow(x - mean, 2));"," return Math.sqrt(sumSquare / n);","}"]);b=b+"("+a+")";break;case "RANDOM":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_random_item",["dynamic "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," int x = new Math.Random().nextInt(myList.length);"," return myList[x];","}"]);b=b+"("+a+")";break;default:throw"Unknown operator: "+b;}return[b,
-Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.math_modulo=function(a){var b=Blockly.Dart.valueToCode(a,"DIVIDEND",Blockly.Dart.ORDER_MULTIPLICATIVE)||"0";a=Blockly.Dart.valueToCode(a,"DIVISOR",Blockly.Dart.ORDER_MULTIPLICATIVE)||"0";return[b+" % "+a,Blockly.Dart.ORDER_MULTIPLICATIVE]};
-Blockly.Dart.math_constrain=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";var b=Blockly.Dart.valueToCode(a,"VALUE",Blockly.Dart.ORDER_NONE)||"0",c=Blockly.Dart.valueToCode(a,"LOW",Blockly.Dart.ORDER_NONE)||"0";a=Blockly.Dart.valueToCode(a,"HIGH",Blockly.Dart.ORDER_NONE)||"double.INFINITY";return["Math.min(Math.max("+b+", "+c+"), "+a+")",Blockly.Dart.ORDER_UNARY_POSTFIX]};
-Blockly.Dart.math_random_int=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";var b=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_NONE)||"0";a=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_NONE)||"0";return[Blockly.Dart.provideFunction_("math_random_int",["int "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(num a, num b) {"," if (a > b) {"," // Swap a and b to ensure a is smaller."," num c = a;"," a = b;"," b = c;"," }"," return new Math.Random().nextInt(b - a + 1) + a;",
-"}"])+"("+b+", "+a+")",Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.math_random_float=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";return["new Math.Random().nextDouble()",Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.procedures={};
-Blockly.Dart.procedures_defreturn=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("NAME"),Blockly.Procedures.NAME_TYPE),c=Blockly.Dart.statementToCode(a,"STACK");Blockly.Dart.STATEMENT_PREFIX&&(c=Blockly.Dart.prefixLines(Blockly.Dart.STATEMENT_PREFIX.replace(/%1/g,"'"+a.id+"'"),Blockly.Dart.INDENT)+c);Blockly.Dart.INFINITE_LOOP_TRAP&&(c=Blockly.Dart.INFINITE_LOOP_TRAP.replace(/%1/g,"'"+a.id+"'")+c);var d=Blockly.Dart.valueToCode(a,"RETURN",Blockly.Dart.ORDER_NONE)||"";d&&(d=" return "+
-d+";\n");for(var e=d?"dynamic":"void",f=[],g=0;g list = str.split(exp);"," final title = new StringBuffer();"," for (String part in list) {"," if (part.length > 0) {",
-" title.write(part[0].toUpperCase());"," if (part.length > 0) {"," title.write(part.substring(1).toLowerCase());"," }"," }"," }"," return title.toString();","}"]),a=Blockly.Dart.valueToCode(a,"TEXT",Blockly.Dart.ORDER_NONE)||"''",a=b+"("+a+")");return[a,Blockly.Dart.ORDER_UNARY_POSTFIX]};
-Blockly.Dart.text_trim=function(a){var b={LEFT:".replaceFirst(new RegExp(r'^\\s+'), '')",RIGHT:".replaceFirst(new RegExp(r'\\s+$'), '')",BOTH:".trim()"}[a.getFieldValue("MODE")];return[(Blockly.Dart.valueToCode(a,"TEXT",Blockly.Dart.ORDER_UNARY_POSTFIX)||"''")+b,Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.text_print=function(a){return"print("+(Blockly.Dart.valueToCode(a,"TEXT",Blockly.Dart.ORDER_NONE)||"''")+");\n"};
-Blockly.Dart.text_prompt_ext=function(a){Blockly.Dart.definitions_.import_dart_html="import 'dart:html' as Html;";var b="Html.window.prompt("+(a.getField("TEXT")?Blockly.Dart.quote_(a.getFieldValue("TEXT")):Blockly.Dart.valueToCode(a,"TEXT",Blockly.Dart.ORDER_NONE)||"''")+", '')";"NUMBER"==a.getFieldValue("TYPE")&&(Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",b="Math.parseDouble("+b+")");return[b,Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.text_prompt=Blockly.Dart.text_prompt_ext;Blockly.Dart.variables={};Blockly.Dart.variables_get=function(a){return[Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),Blockly.Dart.ORDER_ATOMIC]};Blockly.Dart.variables_set=function(a){var b=Blockly.Dart.valueToCode(a,"VALUE",Blockly.Dart.ORDER_ASSIGNMENT)||"0";return Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE)+" = "+b+";\n"};
\ No newline at end of file
diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js
deleted file mode 100644
index 52172f5db6..0000000000
--- a/demos/blockfactory/blocks.js
+++ /dev/null
@@ -1,751 +0,0 @@
-/**
- * Blockly Demos: Block Factory Blocks
- *
- * 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 Blocks for Blockly's Block Factory application.
- * @author fraser@google.com (Neil Fraser)
- */
-'use strict';
-
-Blockly.Blocks['factory_base'] = {
- // Base of new block.
- init: function() {
- this.setColour(120);
- this.appendDummyInput()
- .appendField('name')
- .appendField(new Blockly.FieldTextInput('math_foo'), 'NAME');
- this.appendStatementInput('INPUTS')
- .setCheck('Input')
- .appendField('inputs');
- var dropdown = new Blockly.FieldDropdown([
- ['automatic inputs', 'AUTO'],
- ['external inputs', 'EXT'],
- ['inline inputs', 'INT']]);
- this.appendDummyInput()
- .appendField(dropdown, 'INLINE');
- dropdown = new Blockly.FieldDropdown([
- ['no connections', 'NONE'],
- ['← left output', 'LEFT'],
- ['↕ top+bottom connections', 'BOTH'],
- ['↑ top connection', 'TOP'],
- ['↓ bottom connection', 'BOTTOM']],
- function(option) {
- this.sourceBlock_.updateShape_(option);
- });
- this.appendDummyInput()
- .appendField(dropdown, 'CONNECTIONS');
- this.appendValueInput('COLOUR')
- .setCheck('Colour')
- .appendField('colour');
- /*
- this.appendValueInput('TOOLTIP')
- .setCheck('String')
- .appendField('tooltip');
- this.appendValueInput('HELP')
- .setCheck('String')
- .appendField('help url');
- */
- this.setTooltip('Build a custom block by plugging\n' +
- 'fields, inputs and other blocks here.');
- this.setHelpUrl(
- 'https://developers.google.com/blockly/custom-blocks/block-factory');
- },
- mutationToDom: function() {
- var container = document.createElement('mutation');
- container.setAttribute('connections', this.getFieldValue('CONNECTIONS'));
- return container;
- },
- domToMutation: function(xmlElement) {
- var connections = xmlElement.getAttribute('connections');
- this.updateShape_(connections);
- },
- updateShape_: function(option) {
- var outputExists = this.getInput('OUTPUTTYPE');
- var topExists = this.getInput('TOPTYPE');
- var bottomExists = this.getInput('BOTTOMTYPE');
- if (option == 'LEFT') {
- if (!outputExists) {
- this.appendValueInput('OUTPUTTYPE')
- .setCheck('Type')
- .appendField('output type');
- this.moveInputBefore('OUTPUTTYPE', 'COLOUR');
- }
- } else if (outputExists) {
- this.removeInput('OUTPUTTYPE');
- }
- if (option == 'TOP' || option == 'BOTH') {
- if (!topExists) {
- this.appendValueInput('TOPTYPE')
- .setCheck('Type')
- .appendField('top type');
- this.moveInputBefore('TOPTYPE', 'COLOUR');
- }
- } else if (topExists) {
- this.removeInput('TOPTYPE');
- }
- if (option == 'BOTTOM' || option == 'BOTH') {
- if (!bottomExists) {
- this.appendValueInput('BOTTOMTYPE')
- .setCheck('Type')
- .appendField('bottom type');
- this.moveInputBefore('BOTTOMTYPE', 'COLOUR');
- }
- } else if (bottomExists) {
- this.removeInput('BOTTOMTYPE');
- }
- }
-};
-
-var ALIGNMENT_OPTIONS =
- [['left', 'LEFT'], ['right', 'RIGHT'], ['centre', 'CENTRE']];
-
-Blockly.Blocks['input_value'] = {
- // Value input.
- init: function() {
- this.setColour(210);
- this.appendDummyInput()
- .appendField('value input')
- .appendField(new Blockly.FieldTextInput('NAME'), 'INPUTNAME');
- this.appendStatementInput('FIELDS')
- .setCheck('Field')
- .appendField('fields')
- .appendField(new Blockly.FieldDropdown(ALIGNMENT_OPTIONS), 'ALIGN');
- this.appendValueInput('TYPE')
- .setCheck('Type')
- .setAlign(Blockly.ALIGN_RIGHT)
- .appendField('type');
- this.setPreviousStatement(true, 'Input');
- this.setNextStatement(true, 'Input');
- this.setTooltip('A value socket for horizontal connections.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=71');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- inputNameCheck(this);
- }
-};
-
-Blockly.Blocks['input_statement'] = {
- // Statement input.
- init: function() {
- this.setColour(210);
- this.appendDummyInput()
- .appendField('statement input')
- .appendField(new Blockly.FieldTextInput('NAME'), 'INPUTNAME');
- this.appendStatementInput('FIELDS')
- .setCheck('Field')
- .appendField('fields')
- .appendField(new Blockly.FieldDropdown(ALIGNMENT_OPTIONS), 'ALIGN');
- this.appendValueInput('TYPE')
- .setCheck('Type')
- .setAlign(Blockly.ALIGN_RIGHT)
- .appendField('type');
- this.setPreviousStatement(true, 'Input');
- this.setNextStatement(true, 'Input');
- this.setTooltip('A statement socket for enclosed vertical stacks.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=246');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- inputNameCheck(this);
- }
-};
-
-Blockly.Blocks['input_dummy'] = {
- // Dummy input.
- init: function() {
- this.setColour(210);
- this.appendDummyInput()
- .appendField('dummy input');
- this.appendStatementInput('FIELDS')
- .setCheck('Field')
- .appendField('fields')
- .appendField(new Blockly.FieldDropdown(ALIGNMENT_OPTIONS), 'ALIGN');
- this.setPreviousStatement(true, 'Input');
- this.setNextStatement(true, 'Input');
- this.setTooltip('For adding fields on a separate row with no ' +
- 'connections. Alignment options (left, right, centre) ' +
- 'apply only to multi-line fields.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293');
- }
-};
-
-Blockly.Blocks['field_static'] = {
- // Text value.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('text')
- .appendField(new Blockly.FieldTextInput(''), 'TEXT');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Static text that serves as a label.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=88');
- }
-};
-
-Blockly.Blocks['field_input'] = {
- // Text input.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('text input')
- .appendField(new Blockly.FieldTextInput('default'), 'TEXT')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('An input field for the user to enter text.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=319');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_angle'] = {
- // Angle input.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('angle input')
- .appendField(new Blockly.FieldAngle('90'), 'ANGLE')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('An input field for the user to enter an angle.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=372');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_dropdown'] = {
- // Dropdown menu.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('dropdown')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.appendDummyInput('OPTION0')
- .appendField(new Blockly.FieldTextInput('option'), 'USER0')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU0');
- this.appendDummyInput('OPTION1')
- .appendField(new Blockly.FieldTextInput('option'), 'USER1')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU1');
- this.appendDummyInput('OPTION2')
- .appendField(new Blockly.FieldTextInput('option'), 'USER2')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU2');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setMutator(new Blockly.Mutator(['field_dropdown_option']));
- this.setTooltip('Dropdown menu with a list of options.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
- this.optionCount_ = 3;
- },
- mutationToDom: function(workspace) {
- var container = document.createElement('mutation');
- container.setAttribute('options', this.optionCount_);
- return container;
- },
- domToMutation: function(container) {
- for (var x = 0; x < this.optionCount_; x++) {
- this.removeInput('OPTION' + x);
- }
- this.optionCount_ = parseInt(container.getAttribute('options'), 10);
- for (var x = 0; x < this.optionCount_; x++) {
- var input = this.appendDummyInput('OPTION' + x);
- input.appendField(new Blockly.FieldTextInput('option'), 'USER' + x);
- input.appendField(',');
- input.appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + x);
- }
- },
- decompose: function(workspace) {
- var containerBlock = workspace.newBlock('field_dropdown_container');
- containerBlock.initSvg();
- var connection = containerBlock.getInput('STACK').connection;
- for (var x = 0; x < this.optionCount_; x++) {
- var optionBlock = workspace.newBlock('field_dropdown_option');
- optionBlock.initSvg();
- connection.connect(optionBlock.previousConnection);
- connection = optionBlock.nextConnection;
- }
- return containerBlock;
- },
- compose: function(containerBlock) {
- // Disconnect all input blocks and remove all inputs.
- for (var x = this.optionCount_ - 1; x >= 0; x--) {
- this.removeInput('OPTION' + x);
- }
- this.optionCount_ = 0;
- // Rebuild the block's inputs.
- var optionBlock = containerBlock.getInputTargetBlock('STACK');
- while (optionBlock) {
- this.appendDummyInput('OPTION' + this.optionCount_)
- .appendField(new Blockly.FieldTextInput(
- optionBlock.userData_ || 'option'), 'USER' + this.optionCount_)
- .appendField(',')
- .appendField(new Blockly.FieldTextInput(
- optionBlock.cpuData_ || 'OPTIONNAME'), 'CPU' + this.optionCount_);
- this.optionCount_++;
- optionBlock = optionBlock.nextConnection &&
- optionBlock.nextConnection.targetBlock();
- }
- },
- saveConnections: function(containerBlock) {
- // Store names and values for each option.
- var optionBlock = containerBlock.getInputTargetBlock('STACK');
- var x = 0;
- while (optionBlock) {
- optionBlock.userData_ = this.getFieldValue('USER' + x);
- optionBlock.cpuData_ = this.getFieldValue('CPU' + x);
- x++;
- optionBlock = optionBlock.nextConnection &&
- optionBlock.nextConnection.targetBlock();
- }
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- if (this.optionCount_ < 1) {
- this.setWarningText('Drop down menu must\nhave at least one option.');
- } else {
- fieldNameCheck(this);
- }
- }
-};
-
-Blockly.Blocks['field_dropdown_container'] = {
- // Container.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('add options');
- this.appendStatementInput('STACK');
- this.setTooltip('Add, remove, or reorder options\n' +
- 'to reconfigure this dropdown menu.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
- this.contextMenu = false;
- }
-};
-
-Blockly.Blocks['field_dropdown_option'] = {
- // Add option.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('option');
- this.setPreviousStatement(true);
- this.setNextStatement(true);
- this.setTooltip('Add a new option to the dropdown menu.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386');
- this.contextMenu = false;
- }
-};
-
-Blockly.Blocks['field_checkbox'] = {
- // Checkbox.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('checkbox')
- .appendField(new Blockly.FieldCheckbox('TRUE'), 'CHECKED')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Checkbox field.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=485');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_colour'] = {
- // Colour input.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('colour')
- .appendField(new Blockly.FieldColour('#ff0000'), 'COLOUR')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Colour input field.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=495');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_date'] = {
- // Date input.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('date')
- .appendField(new Blockly.FieldDate(), 'DATE')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Date input field.');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_variable'] = {
- // Dropdown for variables.
- init: function() {
- this.setColour(160);
- this.appendDummyInput()
- .appendField('variable')
- .appendField(new Blockly.FieldTextInput('item'), 'TEXT')
- .appendField(',')
- .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Dropdown menu for variable names.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=510');
- },
- onchange: function() {
- if (!this.workspace) {
- // Block has been deleted.
- return;
- }
- fieldNameCheck(this);
- }
-};
-
-Blockly.Blocks['field_image'] = {
- // Image.
- init: function() {
- this.setColour(160);
- var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif';
- this.appendDummyInput()
- .appendField('image')
- .appendField(new Blockly.FieldTextInput(src), 'SRC');
- this.appendDummyInput()
- .appendField('width')
- .appendField(new Blockly.FieldTextInput('15',
- Blockly.FieldTextInput.numberValidator), 'WIDTH')
- .appendField('height')
- .appendField(new Blockly.FieldTextInput('15',
- Blockly.FieldTextInput.numberValidator), 'HEIGHT')
- .appendField('alt text')
- .appendField(new Blockly.FieldTextInput('*'), 'ALT');
- this.setPreviousStatement(true, 'Field');
- this.setNextStatement(true, 'Field');
- this.setTooltip('Static image (JPEG, PNG, GIF, SVG, BMP).\n' +
- 'Retains aspect ratio regardless of height and width.\n' +
- 'Alt text is for when collapsed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=567');
- }
-};
-
-Blockly.Blocks['type_group'] = {
- // Group of types.
- init: function() {
- this.setColour(230);
- this.appendValueInput('TYPE0')
- .setCheck('Type')
- .appendField('any of');
- this.appendValueInput('TYPE1')
- .setCheck('Type');
- this.setOutput(true, 'Type');
- this.setMutator(new Blockly.Mutator(['type_group_item']));
- this.setTooltip('Allows more than one type to be accepted.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677');
- this.typeCount_ = 2;
- },
- mutationToDom: function(workspace) {
- var container = document.createElement('mutation');
- container.setAttribute('types', this.typeCount_);
- return container;
- },
- domToMutation: function(container) {
- for (var x = 0; x < this.typeCount_; x++) {
- this.removeInput('TYPE' + x);
- }
- this.typeCount_ = parseInt(container.getAttribute('types'), 10);
- for (var x = 0; x < this.typeCount_; x++) {
- var input = this.appendValueInput('TYPE' + x)
- .setCheck('Type');
- if (x == 0) {
- input.appendField('any of');
- }
- }
- },
- decompose: function(workspace) {
- var containerBlock = workspace.newBlock('type_group_container');
- containerBlock.initSvg();
- var connection = containerBlock.getInput('STACK').connection;
- for (var x = 0; x < this.typeCount_; x++) {
- var typeBlock = workspace.newBlock('type_group_item');
- typeBlock.initSvg();
- connection.connect(typeBlock.previousConnection);
- connection = typeBlock.nextConnection;
- }
- return containerBlock;
- },
- compose: function(containerBlock) {
- // Disconnect all input blocks and remove all inputs.
- for (var x = this.typeCount_ - 1; x >= 0; x--) {
- this.removeInput('TYPE' + x);
- }
- this.typeCount_ = 0;
- // Rebuild the block's inputs.
- var typeBlock = containerBlock.getInputTargetBlock('STACK');
- while (typeBlock) {
- var input = this.appendValueInput('TYPE' + this.typeCount_)
- .setCheck('Type');
- if (this.typeCount_ == 0) {
- input.appendField('any of');
- }
- // Reconnect any child blocks.
- if (typeBlock.valueConnection_) {
- input.connection.connect(typeBlock.valueConnection_);
- }
- this.typeCount_++;
- typeBlock = typeBlock.nextConnection &&
- typeBlock.nextConnection.targetBlock();
- }
- },
- saveConnections: function(containerBlock) {
- // Store a pointer to any connected child blocks.
- var typeBlock = containerBlock.getInputTargetBlock('STACK');
- var x = 0;
- while (typeBlock) {
- var input = this.getInput('TYPE' + x);
- typeBlock.valueConnection_ = input && input.connection.targetConnection;
- x++;
- typeBlock = typeBlock.nextConnection &&
- typeBlock.nextConnection.targetBlock();
- }
- }
-};
-
-Blockly.Blocks['type_group_container'] = {
- // Container.
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('add types');
- this.appendStatementInput('STACK');
- this.setTooltip('Add, or remove allowed type.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677');
- this.contextMenu = false;
- }
-};
-
-Blockly.Blocks['type_group_item'] = {
- // Add type.
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('type');
- this.setPreviousStatement(true);
- this.setNextStatement(true);
- this.setTooltip('Add a new allowed type.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=677');
- this.contextMenu = false;
- }
-};
-
-Blockly.Blocks['type_null'] = {
- // Null type.
- valueType: null,
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('any');
- this.setOutput(true, 'Type');
- this.setTooltip('Any type is allowed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602');
- }
-};
-
-Blockly.Blocks['type_boolean'] = {
- // Boolean type.
- valueType: 'Boolean',
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('boolean');
- this.setOutput(true, 'Type');
- this.setTooltip('Booleans (true/false) are allowed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602');
- }
-};
-
-Blockly.Blocks['type_number'] = {
- // Number type.
- valueType: 'Number',
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('number');
- this.setOutput(true, 'Type');
- this.setTooltip('Numbers (int/float) are allowed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602');
- }
-};
-
-Blockly.Blocks['type_string'] = {
- // String type.
- valueType: 'String',
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('string');
- this.setOutput(true, 'Type');
- this.setTooltip('Strings (text) are allowed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602');
- }
-};
-
-Blockly.Blocks['type_list'] = {
- // List type.
- valueType: 'Array',
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('list');
- this.setOutput(true, 'Type');
- this.setTooltip('Arrays (lists) are allowed.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=602');
- }
-};
-
-Blockly.Blocks['type_other'] = {
- // Other type.
- init: function() {
- this.setColour(230);
- this.appendDummyInput()
- .appendField('other')
- .appendField(new Blockly.FieldTextInput(''), 'TYPE');
- this.setOutput(true, 'Type');
- this.setTooltip('Custom type to allow.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=702');
- }
-};
-
-Blockly.Blocks['colour_hue'] = {
- // Set the colour of the block.
- init: function() {
- this.appendDummyInput()
- .appendField('hue:')
- .appendField(new Blockly.FieldAngle('0', this.validator), 'HUE');
- this.setOutput(true, 'Colour');
- this.setTooltip('Paint the block with this colour.');
- this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=55');
- },
- validator: function(text) {
- // Update the current block's colour to match.
- this.sourceBlock_.setColour(text);
- },
- mutationToDom: function(workspace) {
- var container = document.createElement('mutation');
- container.setAttribute('colour', this.getColour());
- return container;
- },
- domToMutation: function(container) {
- this.setColour(container.getAttribute('colour'));
- }
-};
-
-/**
- * Check to see if more than one field has this name.
- * Highly inefficient (On^2), but n is small.
- * @param {!Blockly.Block} referenceBlock Block to check.
- */
-function fieldNameCheck(referenceBlock) {
- var name = referenceBlock.getFieldValue('FIELDNAME').toLowerCase();
- var count = 0;
- var blocks = referenceBlock.workspace.getAllBlocks();
- for (var x = 0, block; block = blocks[x]; x++) {
- var otherName = block.getFieldValue('FIELDNAME');
- if (!block.disabled && !block.getInheritedDisabled() &&
- otherName && otherName.toLowerCase() == name) {
- count++;
- }
- }
- var msg = (count > 1) ?
- 'There are ' + count + ' field blocks\n with this name.' : null;
- referenceBlock.setWarningText(msg);
-}
-
-/**
- * Check to see if more than one input has this name.
- * Highly inefficient (On^2), but n is small.
- * @param {!Blockly.Block} referenceBlock Block to check.
- */
-function inputNameCheck(referenceBlock) {
- var name = referenceBlock.getFieldValue('INPUTNAME').toLowerCase();
- var count = 0;
- var blocks = referenceBlock.workspace.getAllBlocks();
- for (var x = 0, block; block = blocks[x]; x++) {
- var otherName = block.getFieldValue('INPUTNAME');
- if (!block.disabled && !block.getInheritedDisabled() &&
- otherName && otherName.toLowerCase() == name) {
- count++;
- }
- }
- var msg = (count > 1) ?
- 'There are ' + count + ' input blocks\n with this name.' : null;
- referenceBlock.setWarningText(msg);
-}
diff --git a/demos/blockfactory/factory.js b/demos/blockfactory/factory.js
deleted file mode 100644
index 6256c13dc2..0000000000
--- a/demos/blockfactory/factory.js
+++ /dev/null
@@ -1,790 +0,0 @@
-/**
- * Blockly Demos: Block Factory
- *
- * 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 JavaScript for Blockly's Block Factory application.
- * @author fraser@google.com (Neil Fraser)
- */
-'use strict';
-
-/**
- * Workspace for user to build block.
- * @type {Blockly.Workspace}
- */
-var mainWorkspace = null;
-
-/**
- * Workspace for preview of block.
- * @type {Blockly.Workspace}
- */
-var previewWorkspace = null;
-
-/**
- * Name of block if not named.
- */
-var UNNAMED = 'unnamed';
-
-/**
- * Change the language code format.
- */
-function formatChange() {
- var mask = document.getElementById('blocklyMask');
- var languagePre = document.getElementById('languagePre');
- var languageTA = document.getElementById('languageTA');
- if (document.getElementById('format').value == 'Manual') {
- Blockly.hideChaff();
- mask.style.display = 'block';
- languagePre.style.display = 'none';
- languageTA.style.display = 'block';
- var code = languagePre.textContent.trim();
- languageTA.value = code;
- languageTA.focus();
- updatePreview();
- } else {
- mask.style.display = 'none';
- languageTA.style.display = 'none';
- languagePre.style.display = 'block';
- updateLanguage();
- }
- disableEnableLink();
-}
-
-/**
- * Update the language code based on constructs made in Blockly.
- */
-function updateLanguage() {
- var rootBlock = getRootBlock();
- if (!rootBlock) {
- return;
- }
- var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
- if (!blockType) {
- blockType = UNNAMED;
- }
- blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
- switch (document.getElementById('format').value) {
- case 'JSON':
- var code = formatJson_(blockType, rootBlock);
- break;
- case 'JavaScript':
- var code = formatJavaScript_(blockType, rootBlock);
- break;
- }
- injectCode(code, 'languagePre');
- updatePreview();
-}
-
-/**
- * Update the language code as JSON.
- * @param {string} blockType Name of block.
- * @param {!Blockly.Block} rootBlock Factory_base block.
- * @return {string} Generanted language code.
- * @private
- */
-function formatJson_(blockType, rootBlock) {
- var JS = {};
- // ID is not used by Blockly, but may be used by a loader.
- JS.id = blockType;
- // Generate inputs.
- var message = [];
- var args = [];
- var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
- var lastInput = null;
- while (contentsBlock) {
- if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
- var fields = getFieldsJson_(contentsBlock.getInputTargetBlock('FIELDS'));
- for (var i = 0; i < fields.length; i++) {
- if (typeof fields[i] == 'string') {
- message.push(fields[i].replace(/%/g, '%%'));
- } else {
- args.push(fields[i]);
- message.push('%' + args.length);
- }
- }
-
- var input = {type: contentsBlock.type};
- // Dummy inputs don't have names. Other inputs do.
- if (contentsBlock.type != 'input_dummy') {
- input.name = contentsBlock.getFieldValue('INPUTNAME');
- }
- var check = JSON.parse(getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
- if (check) {
- input.check = check;
- }
- var align = contentsBlock.getFieldValue('ALIGN');
- if (align != 'LEFT') {
- input.align = align;
- }
- args.push(input);
- message.push('%' + args.length);
- lastInput = contentsBlock;
- }
- contentsBlock = contentsBlock.nextConnection &&
- contentsBlock.nextConnection.targetBlock();
- }
- // Remove last input if dummy and not empty.
- if (lastInput && lastInput.type == 'input_dummy') {
- var fields = lastInput.getInputTargetBlock('FIELDS');
- if (fields && getFieldsJson_(fields).join('').trim() != '') {
- var align = lastInput.getFieldValue('ALIGN');
- if (align != 'LEFT') {
- JS.lastDummyAlign0 = align;
- }
- args.pop();
- message.pop();
- }
- }
- JS.message0 = message.join(' ');
- JS.args0 = args;
- // Generate inline/external switch.
- if (rootBlock.getFieldValue('INLINE') == 'EXT') {
- JS.inputsInline = false;
- } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
- JS.inputsInline = true;
- }
- // Generate output, or next/previous connections.
- switch (rootBlock.getFieldValue('CONNECTIONS')) {
- case 'LEFT':
- JS.output =
- JSON.parse(getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
- break;
- case 'BOTH':
- JS.previousStatement =
- JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
- JS.nextStatement =
- JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
- break;
- case 'TOP':
- JS.previousStatement =
- JSON.parse(getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
- break;
- case 'BOTTOM':
- JS.nextStatement =
- JSON.parse(getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
- break;
- }
- // Generate colour.
- var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
- if (colourBlock && !colourBlock.disabled) {
- var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
- JS.colour = hue;
- }
- JS.tooltip = '';
- JS.helpUrl = 'http://www.example.com/';
- return JSON.stringify(JS, null, ' ');
-}
-
-/**
- * Update the language code as JavaScript.
- * @param {string} blockType Name of block.
- * @param {!Blockly.Block} rootBlock Factory_base block.
- * @return {string} Generanted language code.
- * @private
- */
-function formatJavaScript_(blockType, rootBlock) {
- var code = [];
- code.push("Blockly.Blocks['" + blockType + "'] = {");
- code.push(" init: function() {");
- // Generate inputs.
- var TYPES = {'input_value': 'appendValueInput',
- 'input_statement': 'appendStatementInput',
- 'input_dummy': 'appendDummyInput'};
- var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
- while (contentsBlock) {
- if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
- var name = '';
- // Dummy inputs don't have names. Other inputs do.
- if (contentsBlock.type != 'input_dummy') {
- name = escapeString(contentsBlock.getFieldValue('INPUTNAME'));
- }
- code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')');
- var check = getOptTypesFrom(contentsBlock, 'TYPE');
- if (check) {
- code.push(' .setCheck(' + check + ')');
- }
- var align = contentsBlock.getFieldValue('ALIGN');
- if (align != 'LEFT') {
- code.push(' .setAlign(Blockly.ALIGN_' + align + ')');
- }
- var fields = getFieldsJs_(contentsBlock.getInputTargetBlock('FIELDS'));
- for (var i = 0; i < fields.length; i++) {
- code.push(' .appendField(' + fields[i] + ')');
- }
- // Add semicolon to last line to finish the statement.
- code[code.length - 1] += ';';
- }
- contentsBlock = contentsBlock.nextConnection &&
- contentsBlock.nextConnection.targetBlock();
- }
- // Generate inline/external switch.
- if (rootBlock.getFieldValue('INLINE') == 'EXT') {
- code.push(' this.setInputsInline(false);');
- } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
- code.push(' this.setInputsInline(true);');
- }
- // Generate output, or next/previous connections.
- switch (rootBlock.getFieldValue('CONNECTIONS')) {
- case 'LEFT':
- code.push(connectionLineJs_('setOutput', 'OUTPUTTYPE'));
- break;
- case 'BOTH':
- code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
- code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
- break;
- case 'TOP':
- code.push(connectionLineJs_('setPreviousStatement', 'TOPTYPE'));
- break;
- case 'BOTTOM':
- code.push(connectionLineJs_('setNextStatement', 'BOTTOMTYPE'));
- break;
- }
- // Generate colour.
- var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
- if (colourBlock && !colourBlock.disabled) {
- var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
- code.push(' this.setColour(' + hue + ');');
- }
- code.push(" this.setTooltip('');");
- code.push(" this.setHelpUrl('http://www.example.com/');");
- code.push(' }');
- code.push('};');
- return code.join('\n');
-}
-
-/**
- * Create JS code required to create a top, bottom, or value connection.
- * @param {string} functionName JavaScript function name.
- * @param {string} typeName Name of type input.
- * @return {string} Line of JavaScript code to create connection.
- * @private
- */
-function connectionLineJs_(functionName, typeName) {
- var type = getOptTypesFrom(getRootBlock(), typeName);
- if (type) {
- type = ', ' + type;
- } else {
- type = '';
- }
- return ' this.' + functionName + '(true' + type + ');';
-}
-
-/**
- * Returns field strings and any config.
- * @param {!Blockly.Block} block Input block.
- * @return {!Array.} Field strings.
- * @private
- */
-function getFieldsJs_(block) {
- var fields = [];
- while (block) {
- if (!block.disabled && !block.getInheritedDisabled()) {
- switch (block.type) {
- case 'field_static':
- // Result: 'hello'
- fields.push(escapeString(block.getFieldValue('TEXT')));
- break;
- case 'field_input':
- // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
- fields.push('new Blockly.FieldTextInput(' +
- escapeString(block.getFieldValue('TEXT')) + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_angle':
- // Result: new Blockly.FieldAngle(90), 'ANGLE'
- fields.push('new Blockly.FieldAngle(' +
- parseFloat(block.getFieldValue('ANGLE')) + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_checkbox':
- // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
- fields.push('new Blockly.FieldCheckbox(' +
- escapeString(block.getFieldValue('CHECKED')) + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_colour':
- // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
- fields.push('new Blockly.FieldColour(' +
- escapeString(block.getFieldValue('COLOUR')) + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_date':
- // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
- fields.push('new Blockly.FieldDate(' +
- escapeString(block.getFieldValue('DATE')) + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_variable':
- // Result: new Blockly.FieldVariable('item'), 'VAR'
- var varname = escapeString(block.getFieldValue('TEXT') || null);
- fields.push('new Blockly.FieldVariable(' + varname + '), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- break;
- case 'field_dropdown':
- // Result:
- // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
- var options = [];
- for (var i = 0; i < block.optionCount_; i++) {
- options[i] = '[' + escapeString(block.getFieldValue('USER' + i)) +
- ', ' + escapeString(block.getFieldValue('CPU' + i)) + ']';
- }
- if (options.length) {
- fields.push('new Blockly.FieldDropdown([' +
- options.join(', ') + ']), ' +
- escapeString(block.getFieldValue('FIELDNAME')));
- }
- break;
- case 'field_image':
- // Result: new Blockly.FieldImage('http://...', 80, 60)
- var src = escapeString(block.getFieldValue('SRC'));
- var width = Number(block.getFieldValue('WIDTH'));
- var height = Number(block.getFieldValue('HEIGHT'));
- var alt = escapeString(block.getFieldValue('ALT'));
- fields.push('new Blockly.FieldImage(' +
- src + ', ' + width + ', ' + height + ', ' + alt + ')');
- break;
- }
- }
- block = block.nextConnection && block.nextConnection.targetBlock();
- }
- return fields;
-}
-
-/**
- * Returns field strings and any config.
- * @param {!Blockly.Block} block Input block.
- * @return {!Array.} Array of static text and field configs.
- * @private
- */
-function getFieldsJson_(block) {
- var fields = [];
- while (block) {
- if (!block.disabled && !block.getInheritedDisabled()) {
- switch (block.type) {
- case 'field_static':
- // Result: 'hello'
- fields.push(block.getFieldValue('TEXT'));
- break;
- case 'field_input':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- text: block.getFieldValue('TEXT')
- });
- break;
- case 'field_angle':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- angle: Number(block.getFieldValue('ANGLE'))
- });
- break;
- case 'field_checkbox':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- checked: block.getFieldValue('CHECKED') == 'TRUE'
- });
- break;
- case 'field_colour':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- colour: block.getFieldValue('COLOUR')
- });
- break;
- case 'field_date':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- date: block.getFieldValue('DATE')
- });
- break;
- case 'field_variable':
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- variable: block.getFieldValue('TEXT') || null
- });
- break;
- case 'field_dropdown':
- var options = [];
- for (var i = 0; i < block.optionCount_; i++) {
- options[i] = [block.getFieldValue('USER' + i),
- block.getFieldValue('CPU' + i)];
- }
- if (options.length) {
- fields.push({
- type: block.type,
- name: block.getFieldValue('FIELDNAME'),
- options: options
- });
- }
- break;
- case 'field_image':
- fields.push({
- type: block.type,
- src: block.getFieldValue('SRC'),
- width: Number(block.getFieldValue('WIDTH')),
- height: Number(block.getFieldValue('HEIGHT')),
- alt: block.getFieldValue('ALT')
- });
- break;
- }
- }
- block = block.nextConnection && block.nextConnection.targetBlock();
- }
- return fields;
-}
-
-/**
- * Escape a string.
- * @param {string} string String to escape.
- * @return {string} Escaped string surrouned by quotes.
- */
-function escapeString(string) {
- return JSON.stringify(string);
-}
-
-/**
- * Fetch the type(s) defined in the given input.
- * Format as a string for appending to the generated code.
- * @param {!Blockly.Block} block Block with input.
- * @param {string} name Name of the input.
- * @return {?string} String defining the types.
- */
-function getOptTypesFrom(block, name) {
- var types = getTypesFrom_(block, name);
- if (types.length == 0) {
- return undefined;
- } else if (types.indexOf('null') != -1) {
- return 'null';
- } else if (types.length == 1) {
- return types[0];
- } else {
- return '[' + types.join(', ') + ']';
- }
-}
-
-/**
- * Fetch the type(s) defined in the given input.
- * @param {!Blockly.Block} block Block with input.
- * @param {string} name Name of the input.
- * @return {!Array.} List of types.
- * @private
- */
-function getTypesFrom_(block, name) {
- var typeBlock = block.getInputTargetBlock(name);
- var types;
- if (!typeBlock || typeBlock.disabled) {
- types = [];
- } else if (typeBlock.type == 'type_other') {
- types = [escapeString(typeBlock.getFieldValue('TYPE'))];
- } else if (typeBlock.type == 'type_group') {
- types = [];
- for (var n = 0; n < typeBlock.typeCount_; n++) {
- types = types.concat(getTypesFrom_(typeBlock, 'TYPE' + n));
- }
- // Remove duplicates.
- var hash = Object.create(null);
- for (var n = types.length - 1; n >= 0; n--) {
- if (hash[types[n]]) {
- types.splice(n, 1);
- }
- hash[types[n]] = true;
- }
- } else {
- types = [escapeString(typeBlock.valueType)];
- }
- return types;
-}
-
-/**
- * Update the generator code.
- * @param {!Blockly.Block} block Rendered block in preview workspace.
- */
-function updateGenerator(block) {
- function makeVar(root, name) {
- name = name.toLowerCase().replace(/\W/g, '_');
- return ' var ' + root + '_' + name;
- }
- var language = document.getElementById('language').value;
- var code = [];
- code.push("Blockly." + language + "['" + block.type +
- "'] = function(block) {");
-
- // Generate getters for any fields or inputs.
- for (var i = 0, input; input = block.inputList[i]; i++) {
- for (var j = 0, field; field = input.fieldRow[j]; j++) {
- var name = field.name;
- if (!name) {
- continue;
- }
- if (field instanceof Blockly.FieldVariable) {
- // Subclass of Blockly.FieldDropdown, must test first.
- code.push(makeVar('variable', name) +
- " = Blockly." + language +
- ".variableDB_.getName(block.getFieldValue('" + name +
- "'), Blockly.Variables.NAME_TYPE);");
- } else if (field instanceof Blockly.FieldAngle) {
- // Subclass of Blockly.FieldTextInput, must test first.
- code.push(makeVar('angle', name) +
- " = block.getFieldValue('" + name + "');");
- } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
- // Blockly.FieldDate may not be compiled into Blockly.
- code.push(makeVar('date', name) +
- " = block.getFieldValue('" + name + "');");
- } else if (field instanceof Blockly.FieldColour) {
- code.push(makeVar('colour', name) +
- " = block.getFieldValue('" + name + "');");
- } else if (field instanceof Blockly.FieldCheckbox) {
- code.push(makeVar('checkbox', name) +
- " = block.getFieldValue('" + name + "') == 'TRUE';");
- } else if (field instanceof Blockly.FieldDropdown) {
- code.push(makeVar('dropdown', name) +
- " = block.getFieldValue('" + name + "');");
- } else if (field instanceof Blockly.FieldTextInput) {
- code.push(makeVar('text', name) +
- " = block.getFieldValue('" + name + "');");
- }
- }
- var name = input.name;
- if (name) {
- if (input.type == Blockly.INPUT_VALUE) {
- code.push(makeVar('value', name) +
- " = Blockly." + language + ".valueToCode(block, '" + name +
- "', Blockly." + language + ".ORDER_ATOMIC);");
- } else if (input.type == Blockly.NEXT_STATEMENT) {
- code.push(makeVar('statements', name) +
- " = Blockly." + language + ".statementToCode(block, '" +
- name + "');");
- }
- }
- }
- code.push(" // TODO: Assemble " + language + " into code variable.");
- code.push(" var code = \'...\';");
- if (block.outputConnection) {
- code.push(" // TODO: Change ORDER_NONE to the correct strength.");
- code.push(" return [code, Blockly." + language + ".ORDER_NONE];");
- } else {
- code.push(" return code;");
- }
- code.push("};");
-
- injectCode(code.join('\n'), 'generatorPre');
-}
-
-/**
- * Existing direction ('ltr' vs 'rtl') of preview.
- */
-var oldDir = null;
-
-/**
- * Update the preview display.
- */
-function updatePreview() {
- // Toggle between LTR/RTL if needed (also used in first display).
- var newDir = document.getElementById('direction').value;
- if (oldDir != newDir) {
- if (previewWorkspace) {
- previewWorkspace.dispose();
- }
- var rtl = newDir == 'rtl';
- previewWorkspace = Blockly.inject('preview',
- {rtl: rtl,
- media: '../../media/',
- scrollbars: true});
- oldDir = newDir;
- }
- previewWorkspace.clear();
-
- // Fetch the code and determine its format (JSON or JavaScript).
- var format = document.getElementById('format').value;
- if (format == 'Manual') {
- var code = document.getElementById('languageTA').value;
- // If the code is JSON, it will parse, otherwise treat as JS.
- try {
- JSON.parse(code);
- format = 'JSON';
- } catch (e) {
- format = 'JavaScript';
- }
- } else {
- var code = document.getElementById('languagePre').textContent;
- }
- if (!code.trim()) {
- // Nothing to render. Happens while cloud storage is loading.
- return;
- }
-
- // Backup Blockly.Blocks object so that main workspace and preview don't
- // collide if user creates a 'factory_base' block, for instance.
- var backupBlocks = Blockly.Blocks;
- try {
- // Make a shallow copy.
- Blockly.Blocks = {};
- for (var prop in backupBlocks) {
- Blockly.Blocks[prop] = backupBlocks[prop];
- }
-
- if (format == 'JSON') {
- var json = JSON.parse(code);
- Blockly.Blocks[json.id || UNNAMED] = {
- init: function() {
- this.jsonInit(json);
- }
- };
- } else if (format == 'JavaScript') {
- eval(code);
- } else {
- throw 'Unknown format: ' + format;
- }
-
- // Look for a block on Blockly.Blocks that does not match the backup.
- var blockType = null;
- for (var type in Blockly.Blocks) {
- if (typeof Blockly.Blocks[type].init == 'function' &&
- Blockly.Blocks[type] != backupBlocks[type]) {
- blockType = type;
- break;
- }
- }
- if (!blockType) {
- return;
- }
-
- // Create the preview block.
- var previewBlock = previewWorkspace.newBlock(blockType);
- previewBlock.initSvg();
- previewBlock.render();
- previewBlock.setMovable(false);
- previewBlock.setDeletable(false);
- previewBlock.moveBy(15, 10);
-
- updateGenerator(previewBlock);
- } finally {
- Blockly.Blocks = backupBlocks;
- }
-}
-
-/**
- * Inject code into a pre tag, with syntax highlighting.
- * Safe from HTML/script injection.
- * @param {string} code Lines of code.
- * @param {string} id ID of element to inject into.
- */
-function injectCode(code, id) {
- Blockly.removeAllRanges();
- var pre = document.getElementById(id);
- pre.textContent = code;
- code = pre.innerHTML;
- code = prettyPrintOne(code, 'js');
- pre.innerHTML = code;
-}
-
-/**
- * Return the uneditable container block that everything else attaches to.
- * @return {Blockly.Block}
- */
-function getRootBlock() {
- var blocks = mainWorkspace.getTopBlocks(false);
- for (var i = 0, block; block = blocks[i]; i++) {
- if (block.type == 'factory_base') {
- return block;
- }
- }
- return null;
-}
-
-/**
- * Disable the link button if the format is 'Manual', enable otherwise.
- */
-function disableEnableLink() {
- var linkButton = document.getElementById('linkButton');
- linkButton.disabled = document.getElementById('format').value == 'Manual';
-}
-
-/**
- * Initialize Blockly and layout. Called on page load.
- */
-function init() {
- if ('BlocklyStorage' in window) {
- BlocklyStorage.HTTPREQUEST_ERROR =
- 'There was a problem with the request.\n';
- BlocklyStorage.LINK_ALERT =
- 'Share your blocks with this link:\n\n%1';
- BlocklyStorage.HASH_ERROR =
- 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
- BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n'+
- 'Perhaps it was created with a different version of Blockly?';
- var linkButton = document.getElementById('linkButton');
- linkButton.style.display = 'inline-block';
- linkButton.addEventListener('click',
- function() {BlocklyStorage.link(mainWorkspace);});
- disableEnableLink();
- }
-
- document.getElementById('helpButton').addEventListener('click',
- function() {
- open('https://developers.google.com/blockly/custom-blocks/block-factory',
- 'BlockFactoryHelp');
- });
-
- var expandList = [
- document.getElementById('blockly'),
- document.getElementById('blocklyMask'),
- document.getElementById('preview'),
- document.getElementById('languagePre'),
- document.getElementById('languageTA'),
- document.getElementById('generatorPre')
- ];
- var onresize = function(e) {
- for (var i = 0, expand; expand = expandList[i]; i++) {
- expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
- expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
- }
- };
- onresize();
- window.addEventListener('resize', onresize);
-
- var toolbox = document.getElementById('toolbox');
- mainWorkspace = Blockly.inject('blockly',
- {toolbox: toolbox, media: '../../media/'});
-
- // Create the root block.
- if ('BlocklyStorage' in window && window.location.hash.length > 1) {
- BlocklyStorage.retrieveXml(window.location.hash.substring(1),
- mainWorkspace);
- } else {
- var xml = ' ';
- Blockly.Xml.domToWorkspace(mainWorkspace, Blockly.Xml.textToDom(xml));
- }
-
- mainWorkspace.addChangeListener(updateLanguage);
- document.getElementById('direction')
- .addEventListener('change', updatePreview);
- document.getElementById('languageTA')
- .addEventListener('change', updatePreview);
- document.getElementById('languageTA')
- .addEventListener('keyup', updatePreview);
- document.getElementById('format')
- .addEventListener('change', formatChange);
- document.getElementById('language')
- .addEventListener('change', updatePreview);
-}
-window.addEventListener('load', init);
diff --git a/demos/blockfactory/icon.png b/demos/blockfactory/icon.png
deleted file mode 100644
index d4d19b4576..0000000000
Binary files a/demos/blockfactory/icon.png and /dev/null differ
diff --git a/demos/blockfactory/index.html b/demos/blockfactory/index.html
deleted file mode 100644
index dfa98de784..0000000000
--- a/demos/blockfactory/index.html
+++ /dev/null
@@ -1,220 +0,0 @@
-
-
-
-
-
- Blockly Demo: Block Factory
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Preview:
-
- LTR
- RTL
-
-
-
-
-
-
-
-
-
-
-
- Help
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Language code:
-
- JavaScript
- JSON
- Manual edit...
-
-
-
-
-
-
-
-
-
-
-
-
- Generator stub:
-
- JavaScript
- Python
- PHP
- Dart
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 20
- 65
- 120
- 160
- 210
- 230
- 260
- 290
- 330
-
-
-
-
diff --git a/demos/blockfactory/link.png b/demos/blockfactory/link.png
deleted file mode 100644
index 11dfd82845..0000000000
Binary files a/demos/blockfactory/link.png and /dev/null differ
diff --git a/demos/code/code.js b/demos/code/code.js
deleted file mode 100644
index 66ca98a81b..0000000000
--- a/demos/code/code.js
+++ /dev/null
@@ -1,531 +0,0 @@
-/**
- * Blockly Demos: Code
- *
- * 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 JavaScript for Blockly's Code demo.
- * @author fraser@google.com (Neil Fraser)
- */
-'use strict';
-
-/**
- * Create a namespace for the application.
- */
-var Code = {};
-
-/**
- * Lookup for names of supported languages. Keys should be in ISO 639 format.
- */
-Code.LANGUAGE_NAME = {
- 'ar': 'العربية',
- 'be-tarask': 'Taraškievica',
- 'br': 'Brezhoneg',
- 'ca': 'Català',
- 'cs': 'Česky',
- 'da': 'Dansk',
- 'de': 'Deutsch',
- 'el': 'Ελληνικά',
- 'en': 'English',
- 'es': 'Español',
- 'fa': 'فارسی',
- 'fr': 'Français',
- 'he': 'עברית',
- 'hrx': 'Hunsrik',
- 'hu': 'Magyar',
- 'ia': 'Interlingua',
- 'is': 'Íslenska',
- 'it': 'Italiano',
- 'ja': '日本語',
- 'ko': '한국어',
- 'mk': 'Македонски',
- 'ms': 'Bahasa Melayu',
- 'nb': 'Norsk Bokmål',
- 'nl': 'Nederlands, Vlaams',
- 'oc': 'Lenga d\'òc',
- 'pl': 'Polski',
- 'pms': 'Piemontèis',
- 'pt-br': 'Português Brasileiro',
- 'ro': 'Română',
- 'ru': 'Русский',
- 'sc': 'Sardu',
- 'sk': 'Slovenčina',
- 'sr': 'Српски',
- 'sv': 'Svenska',
- 'ta': 'தமிழ்',
- 'th': 'ภาษาไทย',
- 'tlh': 'tlhIngan Hol',
- 'tr': 'Türkçe',
- 'uk': 'Українська',
- 'vi': 'Tiếng Việt',
- 'zh-hans': '簡體中文',
- 'zh-hant': '正體中文'
-};
-
-/**
- * List of RTL languages.
- */
-Code.LANGUAGE_RTL = ['ar', 'fa', 'he', 'lki'];
-
-/**
- * Blockly's main workspace.
- * @type {Blockly.WorkspaceSvg}
- */
-Code.workspace = null;
-
-/**
- * Extracts a parameter from the URL.
- * If the parameter is absent default_value is returned.
- * @param {string} name The name of the parameter.
- * @param {string} defaultValue Value to return if paramater not found.
- * @return {string} The parameter value or the default value if not found.
- */
-Code.getStringParamFromUrl = function(name, defaultValue) {
- var val = location.search.match(new RegExp('[?&]' + name + '=([^&]+)'));
- return val ? decodeURIComponent(val[1].replace(/\+/g, '%20')) : defaultValue;
-};
-
-/**
- * Get the language of this user from the URL.
- * @return {string} User's language.
- */
-Code.getLang = function() {
- var lang = Code.getStringParamFromUrl('lang', '');
- if (Code.LANGUAGE_NAME[lang] === undefined) {
- // Default to English.
- lang = 'en';
- }
- return lang;
-};
-
-/**
- * Is the current language (Code.LANG) an RTL language?
- * @return {boolean} True if RTL, false if LTR.
- */
-Code.isRtl = function() {
- return Code.LANGUAGE_RTL.indexOf(Code.LANG) != -1;
-};
-
-/**
- * Load blocks saved on App Engine Storage or in session/local storage.
- * @param {string} defaultXml Text representation of default blocks.
- */
-Code.loadBlocks = function(defaultXml) {
- try {
- var loadOnce = window.sessionStorage.loadOnceBlocks;
- } catch(e) {
- // Firefox sometimes throws a SecurityError when accessing sessionStorage.
- // Restarting Firefox fixes this, so it looks like a bug.
- var loadOnce = null;
- }
- if ('BlocklyStorage' in window && window.location.hash.length > 1) {
- // An href with #key trigers an AJAX call to retrieve saved blocks.
- BlocklyStorage.retrieveXml(window.location.hash.substring(1));
- } else if (loadOnce) {
- // Language switching stores the blocks during the reload.
- delete window.sessionStorage.loadOnceBlocks;
- var xml = Blockly.Xml.textToDom(loadOnce);
- Blockly.Xml.domToWorkspace(Code.workspace, xml);
- } else if (defaultXml) {
- // Load the editor with default starting blocks.
- var xml = Blockly.Xml.textToDom(defaultXml);
- Blockly.Xml.domToWorkspace(Code.workspace, xml);
- } else if ('BlocklyStorage' in window) {
- // Restore saved blocks in a separate thread so that subsequent
- // initialization is not affected from a failed load.
- window.setTimeout(BlocklyStorage.restoreBlocks, 0);
- }
-};
-
-/**
- * Save the blocks and reload with a different language.
- */
-Code.changeLanguage = function() {
- // Store the blocks for the duration of the reload.
- // This should be skipped for the index page, which has no blocks and does
- // not load Blockly.
- // MSIE 11 does not support sessionStorage on file:// URLs.
- if (typeof Blockly != 'undefined' && window.sessionStorage) {
- var xml = Blockly.Xml.workspaceToDom(Code.workspace);
- var text = Blockly.Xml.domToText(xml);
- window.sessionStorage.loadOnceBlocks = text;
- }
-
- var languageMenu = document.getElementById('languageMenu');
- var newLang = encodeURIComponent(
- languageMenu.options[languageMenu.selectedIndex].value);
- var search = window.location.search;
- if (search.length <= 1) {
- search = '?lang=' + newLang;
- } else if (search.match(/[?&]lang=[^&]*/)) {
- search = search.replace(/([?&]lang=)[^&]*/, '$1' + newLang);
- } else {
- search = search.replace(/\?/, '?lang=' + newLang + '&');
- }
-
- window.location = window.location.protocol + '//' +
- window.location.host + window.location.pathname + search;
-};
-
-/**
- * Bind a function to a button's click event.
- * On touch enabled browsers, ontouchend is treated as equivalent to onclick.
- * @param {!Element|string} el Button element or ID thereof.
- * @param {!Function} func Event handler to bind.
- */
-Code.bindClick = function(el, func) {
- if (typeof el == 'string') {
- el = document.getElementById(el);
- }
- el.addEventListener('click', func, true);
- el.addEventListener('touchend', func, true);
-};
-
-/**
- * Load the Prettify CSS and JavaScript.
- */
-Code.importPrettify = function() {
- //
- //
- var link = document.createElement('link');
- link.setAttribute('rel', 'stylesheet');
- link.setAttribute('href', '../prettify.css');
- document.head.appendChild(link);
- var script = document.createElement('script');
- script.setAttribute('src', '../prettify.js');
- document.head.appendChild(script);
-};
-
-/**
- * Compute the absolute coordinates and dimensions of an HTML element.
- * @param {!Element} element Element to match.
- * @return {!Object} Contains height, width, x, and y properties.
- * @private
- */
-Code.getBBox_ = function(element) {
- var height = element.offsetHeight;
- var width = element.offsetWidth;
- var x = 0;
- var y = 0;
- do {
- x += element.offsetLeft;
- y += element.offsetTop;
- element = element.offsetParent;
- } while (element);
- return {
- height: height,
- width: width,
- x: x,
- y: y
- };
-};
-
-/**
- * User's language (e.g. "en").
- * @type {string}
- */
-Code.LANG = Code.getLang();
-
-/**
- * List of tab names.
- * @private
- */
-Code.TABS_ = ['blocks', 'javascript', 'php', 'python', 'dart', 'xml'];
-
-Code.selected = 'blocks';
-
-/**
- * Switch the visible pane when a tab is clicked.
- * @param {string} clickedName Name of tab clicked.
- */
-Code.tabClick = function(clickedName) {
- // If the XML tab was open, save and render the content.
- if (document.getElementById('tab_xml').className == 'tabon') {
- var xmlTextarea = document.getElementById('content_xml');
- var xmlText = xmlTextarea.value;
- var xmlDom = null;
- try {
- xmlDom = Blockly.Xml.textToDom(xmlText);
- } catch (e) {
- var q =
- window.confirm(MSG['badXml'].replace('%1', e));
- if (!q) {
- // Leave the user on the XML tab.
- return;
- }
- }
- if (xmlDom) {
- Code.workspace.clear();
- Blockly.Xml.domToWorkspace(Code.workspace, xmlDom);
- }
- }
-
- if (document.getElementById('tab_blocks').className == 'tabon') {
- Code.workspace.setVisible(false);
- }
- // Deselect all tabs and hide all panes.
- for (var i = 0; i < Code.TABS_.length; i++) {
- var name = Code.TABS_[i];
- document.getElementById('tab_' + name).className = 'taboff';
- document.getElementById('content_' + name).style.visibility = 'hidden';
- }
-
- // Select the active tab.
- Code.selected = clickedName;
- document.getElementById('tab_' + clickedName).className = 'tabon';
- // Show the selected pane.
- document.getElementById('content_' + clickedName).style.visibility =
- 'visible';
- Code.renderContent();
- if (clickedName == 'blocks') {
- Code.workspace.setVisible(true);
- }
- Blockly.fireUiEvent(window, 'resize');
-};
-
-/**
- * Populate the currently selected pane with content generated from the blocks.
- */
-Code.renderContent = function() {
- var content = document.getElementById('content_' + Code.selected);
- // Initialize the pane.
- if (content.id == 'content_xml') {
- var xmlTextarea = document.getElementById('content_xml');
- var xmlDom = Blockly.Xml.workspaceToDom(Code.workspace);
- var xmlText = Blockly.Xml.domToPrettyText(xmlDom);
- xmlTextarea.value = xmlText;
- xmlTextarea.focus();
- } else if (content.id == 'content_javascript') {
- var code = Blockly.JavaScript.workspaceToCode(Code.workspace);
- content.textContent = code;
- if (typeof prettyPrintOne == 'function') {
- code = content.innerHTML;
- code = prettyPrintOne(code, 'js');
- content.innerHTML = code;
- }
- } else if (content.id == 'content_python') {
- code = Blockly.Python.workspaceToCode(Code.workspace);
- content.textContent = code;
- if (typeof prettyPrintOne == 'function') {
- code = content.innerHTML;
- code = prettyPrintOne(code, 'py');
- content.innerHTML = code;
- }
- } else if (content.id == 'content_php') {
- code = Blockly.PHP.workspaceToCode(Code.workspace);
- content.textContent = code;
- if (typeof prettyPrintOne == 'function') {
- code = content.innerHTML;
- code = prettyPrintOne(code, 'php');
- content.innerHTML = code;
- }
- } else if (content.id == 'content_dart') {
- code = Blockly.Dart.workspaceToCode(Code.workspace);
- content.textContent = code;
- if (typeof prettyPrintOne == 'function') {
- code = content.innerHTML;
- code = prettyPrintOne(code, 'dart');
- content.innerHTML = code;
- }
- }
-};
-
-/**
- * Initialize Blockly. Called on page load.
- */
-Code.init = function() {
- Code.initLanguage();
-
- var rtl = Code.isRtl();
- var container = document.getElementById('content_area');
- var onresize = function(e) {
- var bBox = Code.getBBox_(container);
- for (var i = 0; i < Code.TABS_.length; i++) {
- var el = document.getElementById('content_' + Code.TABS_[i]);
- el.style.top = bBox.y + 'px';
- el.style.left = bBox.x + 'px';
- // Height and width need to be set, read back, then set again to
- // compensate for scrollbars.
- el.style.height = bBox.height + 'px';
- el.style.height = (2 * bBox.height - el.offsetHeight) + 'px';
- el.style.width = bBox.width + 'px';
- el.style.width = (2 * bBox.width - el.offsetWidth) + 'px';
- }
- // Make the 'Blocks' tab line up with the toolbox.
- if (Code.workspace && Code.workspace.toolbox_.width) {
- document.getElementById('tab_blocks').style.minWidth =
- (Code.workspace.toolbox_.width - 38) + 'px';
- // Account for the 19 pixel margin and on each side.
- }
- };
- onresize();
- window.addEventListener('resize', onresize, false);
-
- var toolbox = document.getElementById('toolbox');
- Code.workspace = Blockly.inject('content_blocks',
- {grid:
- {spacing: 25,
- length: 3,
- colour: '#ccc',
- snap: true},
- media: '../../media/',
- rtl: rtl,
- toolbox: toolbox,
- zoom:
- {controls: true,
- wheel: true}
- });
-
- // Add to reserved word list: Local variables in execution environment (runJS)
- // and the infinite loop detection function.
- Blockly.JavaScript.addReservedWords('code,timeouts,checkTimeout');
-
- Code.loadBlocks('');
-
- if ('BlocklyStorage' in window) {
- // Hook a save function onto unload.
- BlocklyStorage.backupOnUnload(Code.workspace);
- }
-
- Code.tabClick(Code.selected);
-
- Code.bindClick('trashButton',
- function() {Code.discard(); Code.renderContent();});
- Code.bindClick('runButton', Code.runJS);
- // Disable the link button if page isn't backed by App Engine storage.
- var linkButton = document.getElementById('linkButton');
- if ('BlocklyStorage' in window) {
- BlocklyStorage['HTTPREQUEST_ERROR'] = MSG['httpRequestError'];
- BlocklyStorage['LINK_ALERT'] = MSG['linkAlert'];
- BlocklyStorage['HASH_ERROR'] = MSG['hashError'];
- BlocklyStorage['XML_ERROR'] = MSG['xmlError'];
- Code.bindClick(linkButton,
- function() {BlocklyStorage.link(Code.workspace);});
- } else if (linkButton) {
- linkButton.className = 'disabled';
- }
-
- for (var i = 0; i < Code.TABS_.length; i++) {
- var name = Code.TABS_[i];
- Code.bindClick('tab_' + name,
- function(name_) {return function() {Code.tabClick(name_);};}(name));
- }
-
- // Lazy-load the syntax-highlighting.
- window.setTimeout(Code.importPrettify, 1);
-};
-
-/**
- * Initialize the page language.
- */
-Code.initLanguage = function() {
- // Set the HTML's language and direction.
- var rtl = Code.isRtl();
- document.dir = rtl ? 'rtl' : 'ltr';
- document.head.parentElement.setAttribute('lang', Code.LANG);
-
- // Sort languages alphabetically.
- var languages = [];
- for (var lang in Code.LANGUAGE_NAME) {
- languages.push([Code.LANGUAGE_NAME[lang], lang]);
- }
- var comp = function(a, b) {
- // Sort based on first argument ('English', 'Русский', '简体字', etc).
- if (a[0] > b[0]) return 1;
- if (a[0] < b[0]) return -1;
- return 0;
- };
- languages.sort(comp);
- // Populate the language selection menu.
- var languageMenu = document.getElementById('languageMenu');
- languageMenu.options.length = 0;
- for (var i = 0; i < languages.length; i++) {
- var tuple = languages[i];
- var lang = tuple[tuple.length - 1];
- var option = new Option(tuple[0], lang);
- if (lang == Code.LANG) {
- option.selected = true;
- }
- languageMenu.options.add(option);
- }
- languageMenu.addEventListener('change', Code.changeLanguage, true);
-
- // Inject language strings.
- document.title += ' ' + MSG['title'];
- document.getElementById('title').textContent = MSG['title'];
- document.getElementById('tab_blocks').textContent = MSG['blocks'];
-
- document.getElementById('linkButton').title = MSG['linkTooltip'];
- document.getElementById('runButton').title = MSG['runTooltip'];
- document.getElementById('trashButton').title = MSG['trashTooltip'];
-
- var categories = ['catLogic', 'catLoops', 'catMath', 'catText', 'catLists',
- 'catColour', 'catVariables', 'catFunctions'];
- for (var i = 0, cat; cat = categories[i]; i++) {
- document.getElementById(cat).setAttribute('name', MSG[cat]);
- }
- var textVars = document.getElementsByClassName('textVar');
- for (var i = 0, textVar; textVar = textVars[i]; i++) {
- textVar.textContent = MSG['textVariable'];
- }
- var listVars = document.getElementsByClassName('listVar');
- for (var i = 0, listVar; listVar = listVars[i]; i++) {
- listVar.textContent = MSG['listVariable'];
- }
-};
-
-/**
- * Execute the user's code.
- * Just a quick and dirty eval. Catch infinite loops.
- */
-Code.runJS = function() {
- Blockly.JavaScript.INFINITE_LOOP_TRAP = ' checkTimeout();\n';
- var timeouts = 0;
- var checkTimeout = function() {
- if (timeouts++ > 1000000) {
- throw MSG['timeout'];
- }
- };
- var code = Blockly.JavaScript.workspaceToCode(Code.workspace);
- Blockly.JavaScript.INFINITE_LOOP_TRAP = null;
- try {
- eval(code);
- } catch (e) {
- alert(MSG['badCode'].replace('%1', e));
- }
-};
-
-/**
- * Discard all blocks from the workspace.
- */
-Code.discard = function() {
- var count = Code.workspace.getAllBlocks().length;
- if (count < 2 ||
- window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1', count))) {
- Code.workspace.clear();
- if (window.location.hash) {
- window.location.hash = '';
- }
- }
-};
-
-// Load the Code demo's language strings.
-document.write('\n');
-// Load Blockly's language strings.
-document.write('\n');
-
-window.addEventListener('load', Code.init);
diff --git a/demos/code/icon.png b/demos/code/icon.png
deleted file mode 100644
index e2f23bd830..0000000000
Binary files a/demos/code/icon.png and /dev/null differ
diff --git a/demos/code/icons.png b/demos/code/icons.png
deleted file mode 100644
index 7cced7a7ea..0000000000
Binary files a/demos/code/icons.png and /dev/null differ
diff --git a/demos/code/index.html b/demos/code/index.html
deleted file mode 100644
index 747aaa2532..0000000000
--- a/demos/code/index.html
+++ /dev/null
@@ -1,376 +0,0 @@
-
-
-
-
-
- Blockly Demo:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ...
-
- JavaScript
-
- Python
-
- PHP
-
- Dart
-
- XML
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10
-
-
-
-
-
-
-
- 1
-
-
-
-
- 10
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
- 1
-
-
-
-
- 1
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 45
-
-
-
-
-
-
-
- 0
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 3.1
-
-
-
-
-
-
-
- 64
-
-
-
-
- 10
-
-
-
-
-
-
- 50
-
-
-
-
- 1
-
-
-
-
- 100
-
-
-
-
-
-
- 1
-
-
-
-
- 100
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- abc
-
-
-
-
-
-
-
-
-
-
-
-
-
- text
-
-
-
-
- abc
-
-
-
-
-
-
- text
-
-
-
-
-
-
- text
-
-
-
-
-
-
- abc
-
-
-
-
-
-
- abc
-
-
-
-
-
-
- abc
-
-
-
-
-
-
- abc
-
-
-
-
-
-
-
-
-
-
-
-
- 5
-
-
-
-
-
-
-
-
- list
-
-
-
-
-
-
- list
-
-
-
-
-
-
- list
-
-
-
-
-
-
- list
-
-
-
-
-
-
- ,
-
-
-
-
-
-
-
-
-
-
- 100
-
-
-
-
- 50
-
-
-
-
- 0
-
-
-
-
-
-
- #ff0000
-
-
-
-
- #3333ff
-
-
-
-
- 0.5
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/code/msg/ar.js b/demos/code/msg/ar.js
deleted file mode 100644
index 952198b21d..0000000000
--- a/demos/code/msg/ar.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "كود",
- blocks: "البلوكات",
- linkTooltip: "احفظ ووصلة إلى البلوكات.",
- runTooltip: "شغل البرنامج المعرف بواسطة البلوكات في مساحة العمل.",
- badCode: "خطأ في البرنامج:\n %1",
- timeout: "تم تجاوز الحد الأقصى لتكرارات التنفيذ .",
- trashTooltip: "تجاهل كل البلوكات.",
- catLogic: "منطق",
- catLoops: "الحلقات",
- catMath: "رياضيات",
- catText: "نص",
- catLists: "قوائم",
- catColour: "لون",
- catVariables: "متغيرات",
- catFunctions: "إجراءات",
- listVariable: "قائمة",
- textVariable: "نص",
- httpRequestError: "كانت هناك مشكلة مع هذا الطلب.",
- linkAlert: "مشاركة كود بلوكلي الخاص بك مع هذا الرابط:\n %1",
- hashError: "عذراً،ال '%1' لا تتوافق مع أي برنامج تم حفظه.",
- xmlError: "تعذر تحميل الملف المحفوظة الخاصة بك. ربما تم إنشاؤه باستخدام إصدار مختلف من بلوكلي؟",
- badXml: "خطأ في توزيع ال \"XML\":\n %1\n\nحدد 'موافق' للتخلي عن التغييرات أو 'إلغاء الأمر' لمواصلة تحرير ال\"XML\"."
-};
diff --git a/demos/code/msg/be-tarask.js b/demos/code/msg/be-tarask.js
deleted file mode 100644
index 5c8d72264a..0000000000
--- a/demos/code/msg/be-tarask.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Код",
- blocks: "Блёкі",
- linkTooltip: "Захаваць і зьвязаць з блёкамі.",
- runTooltip: "Запусьціце праграму, вызначаную блёкамі ў працоўнай вобласьці.",
- badCode: "Памылка праграмы:\n%1",
- timeout: "Перавышана максымальная колькасьць ітэрацыяў.",
- trashTooltip: "Выдаліць усе блёкі.",
- catLogic: "Лёгіка",
- catLoops: "Петлі",
- catMath: "Матэматычныя формулы",
- catText: "Тэкст",
- catLists: "Сьпісы",
- catColour: "Колер",
- catVariables: "Зьменныя",
- catFunctions: "Функцыі",
- listVariable: "сьпіс",
- textVariable: "тэкст",
- httpRequestError: "Узьнікла праблема з запытам.",
- linkAlert: "Падзяліцца Вашым блёкам праз гэтую спасылку:\n\n%1",
- hashError: "Прабачце, '%1' не адпавядае ніводнай захаванай праграме.",
- xmlError: "Не атрымалася загрузіць захаваны файл. Магчыма, ён быў створаны з іншай вэрсіяй Блёклі?",
- badXml: "Памылка сынтаксічнага аналізу XML:\n%1\n\nАбярыце \"ОК\", каб адмовіцца ад зьменаў ці \"Скасаваць\" для далейшага рэдагаваньня XML."
-};
diff --git a/demos/code/msg/br.js b/demos/code/msg/br.js
deleted file mode 100644
index 321e28e58d..0000000000
--- a/demos/code/msg/br.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kod",
- blocks: "Bloc'hoù",
- linkTooltip: "Enrollañ ha liammañ d'ar bloc'hadoù.",
- runTooltip: "Lañsañ ar programm termenet gant ar bloc'hadoù en takad labour.",
- badCode: "Fazi programm :\n%1",
- timeout: "Tizhet eo bet an niver brasañ a iteradurioù seveniñ aotreet.",
- trashTooltip: "Disteurel an holl vloc'hoù.",
- catLogic: "Poell",
- catLoops: "Boukloù",
- catMath: "Matematik",
- catText: "Testenn",
- catLists: "Rolloù",
- catColour: "Liv",
- catVariables: "Argemmennoù",
- catFunctions: "Arc'hwelioù",
- listVariable: "roll",
- textVariable: "testenn",
- httpRequestError: "Ur gudenn zo gant ar reked.",
- linkAlert: "Rannañ ho ploc'hoù gant al liamm-mañ :\n\n%1",
- hashError: "Digarezit. \"%1\" ne glot gant programm enrollet ebet.",
- xmlError: "Ne c'haller ket kargañ ho restr enrollet. Marteze e oa bet krouet gant ur stumm disheñvel eus Blockly ?",
- badXml: "Fazi dielfennañ XML :\n%1\n\nDibabit \"Mat eo\" evit dilezel ar c'hemmoù-se pe \"Nullañ\" evit kemmañ an XML c'hoazh."
-};
diff --git a/demos/code/msg/ca.js b/demos/code/msg/ca.js
deleted file mode 100644
index 2b6d9738ee..0000000000
--- a/demos/code/msg/ca.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Codi",
- blocks: "Blocs",
- linkTooltip: "Desa i enllaça als blocs.",
- runTooltip: "Executa el programa definit pels blocs de l'àrea de treball.",
- badCode: "Error de programa:\n %1",
- timeout: "S'ha superat el nombre màxim d'iteracions d'execució.",
- trashTooltip: "Descarta tots els blocs.",
- catLogic: "Lògica",
- catLoops: "Bucles",
- catMath: "Matemàtiques",
- catText: "Text",
- catLists: "Llistes",
- catColour: "Color",
- catVariables: "Variables",
- catFunctions: "Procediments",
- listVariable: "llista",
- textVariable: "text",
- httpRequestError: "Hi ha hagut un problema amb la sol·licitud.",
- linkAlert: "Comparteix els teus blocs amb aquest enllaç: %1",
- hashError: "Ho sentim, '%1' no es correspon amb cap fitxer desat de Blockly.",
- xmlError: "No s'ha pogut carregar el teu fitxer desat. Potser va ser creat amb una versió diferent de Blockly?",
- badXml: "Error d'anàlisi XML:\n%1\n\nSeleccioneu 'Acceptar' per abandonar els vostres canvis, o 'Cancel·lar' per continuar editant l'XML."
-};
diff --git a/demos/code/msg/cs.js b/demos/code/msg/cs.js
deleted file mode 100644
index 1026511e3a..0000000000
--- a/demos/code/msg/cs.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kód",
- blocks: "Bloky",
- linkTooltip: "Ulož a spoj bloky..",
- runTooltip: "",
- badCode: "Chyba programu:\n%1",
- timeout: "Maximum execution iterations exceeded.",
- trashTooltip: "Zahodit všechny bloky.",
- catLogic: "Logika",
- catLoops: "Smyčky",
- catMath: "Matematika",
- catText: "Text",
- catLists: "Seznamy",
- catColour: "Barva",
- catVariables: "Proměnné",
- catFunctions: "Procedury",
- listVariable: "seznam",
- textVariable: "text",
- httpRequestError: "Došlo k potížím s požadavkem.",
- linkAlert: "Sdílej bloky tímto odkazem: \n\n%1",
- hashError: "Omlouváme se, '%1' nesouhlasí s žádným z uložených souborů.",
- xmlError: "Nepodařilo se uložit vás soubor. Pravděpodobně byl vytvořen jinou verzí Blockly?",
- badXml: "Chyba parsování XML:\n%1\n\nVybrat \"OK\" pro zahození vašich změn nebo 'Cancel' k dalšímu upravování XML."
-};
diff --git a/demos/code/msg/da.js b/demos/code/msg/da.js
deleted file mode 100644
index 089450b2db..0000000000
--- a/demos/code/msg/da.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kode",
- blocks: "Blokke",
- linkTooltip: "Gem og link til blokke.",
- runTooltip: "Kør programmet, der er defineret af blokkene i arbejdsområdet.",
- badCode: "Programfejl:\n%1",
- timeout: "Maksimale antal udførelsesgentagelser overskredet.",
- trashTooltip: "Kassér alle blokke.",
- catLogic: "Logik",
- catLoops: "Løkker",
- catMath: "Matematik",
- catText: "Tekst",
- catLists: "Lister",
- catColour: "Farve",
- catVariables: "Variabler",
- catFunctions: "Funktioner",
- listVariable: "liste",
- textVariable: "tekst",
- httpRequestError: "Der var et problem med forespørgslen.",
- linkAlert: "Del dine blokke med dette link:\n\n%1",
- hashError: "Beklager, '%1' passer ikke med nogen gemt Blockly fil.",
- xmlError: "Kunne ikke hente din gemte fil. Måske er den lavet med en anden udgave af Blockly?",
- badXml: "Fejl under fortolkningen af XML:\n%1\n\nVælg 'OK' for at opgive dine ændringer eller 'Afbryd' for at redigere XML-filen yderligere."
-};
diff --git a/demos/code/msg/de.js b/demos/code/msg/de.js
deleted file mode 100644
index 422f9187d1..0000000000
--- a/demos/code/msg/de.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Code",
- blocks: "Bausteine",
- linkTooltip: "Speichern und auf Bausteine verlinken.",
- runTooltip: "Das Programm ausführen, das von den Bausteinen im Arbeitsbereich definiert ist.",
- badCode: "Programmfehler:\n%1",
- timeout: "Die maximalen Ausführungswiederholungen wurden überschritten.",
- trashTooltip: "Alle Bausteine verwerfen.",
- catLogic: "Logik",
- catLoops: "Schleifen",
- catMath: "Mathematik",
- catText: "Text",
- catLists: "Listen",
- catColour: "Farbe",
- catVariables: "Variablen",
- catFunctions: "Funktionen",
- listVariable: "Liste",
- textVariable: "Text",
- httpRequestError: "Mit der Anfrage gab es ein Problem.",
- linkAlert: "Teile deine Bausteine mit diesem Link:\n\n%1",
- hashError: "„%1“ stimmt leider mit keinem gespeicherten Programm überein.",
- xmlError: "Deine gespeicherte Datei konnte nicht geladen werden. Vielleicht wurde sie mit einer anderen Version von Blockly erstellt.",
- badXml: "Fehler beim Parsen von XML:\n%1\n\nWähle 'OK' zum Verwerfen deiner Änderungen oder 'Abbrechen' zum weiteren Bearbeiten des XML."
-};
diff --git a/demos/code/msg/el.js b/demos/code/msg/el.js
deleted file mode 100644
index c63b7a6851..0000000000
--- a/demos/code/msg/el.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Κώδικας",
- blocks: "Μπλοκ",
- linkTooltip: "Αποθηκεύει και συνδέει σε μπλοκ.",
- runTooltip: "Εκτελεί το πρόγραμμα που ορίζεται από τα μπλοκ στον χώρο εργασίας.",
- badCode: "Σφάλμα προγράμματος:\n%1",
- timeout: "Υπέρβαση μέγιστου αριθμού επαναλήψεων.",
- trashTooltip: "Απόρριψη όλων των μπλοκ.",
- catLogic: "Λογική",
- catLoops: "Επαναλήψεις",
- catMath: "Μαθηματικά",
- catText: "Κείμενο",
- catLists: "Λίστες",
- catColour: "Χρώμα",
- catVariables: "Μεταβλητές",
- catFunctions: "Συναρτήσεις",
- listVariable: "λίστα",
- textVariable: "κείμενο",
- httpRequestError: "Υπήρξε πρόβλημα με το αίτημα.",
- linkAlert: "Κοινοποίησε τα μπλοκ σου με αυτόν τον σύνδεσμο:\n\n%1",
- hashError: "Λυπάμαι, το «%1» δεν αντιστοιχεί σε κανένα αποθηκευμένο πρόγραμμα.",
- xmlError: "Δεν μπορώ να φορτώσω το αποθηκευμένο αρχείο σου. Μήπως δημιουργήθηκε από μία παλιότερη έκδοση του Blockly;",
- badXml: "Σφάλμα ανάλυσης XML:\n%1\n\nΕπίλεξε «Εντάξει» για να εγκαταλείψεις τις αλλαγές σου ή «Ακύρωση» για να επεξεργαστείς το XML κι άλλο."
-};
diff --git a/demos/code/msg/en.js b/demos/code/msg/en.js
deleted file mode 100644
index 8d52cf3330..0000000000
--- a/demos/code/msg/en.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Code",
- blocks: "Blocks",
- linkTooltip: "Save and link to blocks.",
- runTooltip: "Run the program defined by the blocks in the workspace.",
- badCode: "Program error:\n%1",
- timeout: "Maximum execution iterations exceeded.",
- trashTooltip: "Discard all blocks.",
- catLogic: "Logic",
- catLoops: "Loops",
- catMath: "Math",
- catText: "Text",
- catLists: "Lists",
- catColour: "Colour",
- catVariables: "Variables",
- catFunctions: "Functions",
- listVariable: "list",
- textVariable: "text",
- httpRequestError: "There was a problem with the request.",
- linkAlert: "Share your blocks with this link:\n\n%1",
- hashError: "Sorry, '%1' doesn't correspond with any saved program.",
- xmlError: "Could not load your saved file. Perhaps it was created with a different version of Blockly?",
- badXml: "Error parsing XML:\n%1\n\nSelect 'OK' to abandon your changes or 'Cancel' to further edit the XML."
-};
diff --git a/demos/code/msg/es.js b/demos/code/msg/es.js
deleted file mode 100644
index 24358e368d..0000000000
--- a/demos/code/msg/es.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Código",
- blocks: "Bloques",
- linkTooltip: "Guarda conexión a los bloques.",
- runTooltip: "Ejecute el programa definido por los bloques en el área de trabajo.",
- badCode: "Error del programa:\n%1",
- timeout: "Se excedio el máximo de iteraciones ejecutadas permitidas.",
- trashTooltip: "Descartar todos los bloques.",
- catLogic: "Lógica",
- catLoops: "Secuencias",
- catMath: "Matemáticas",
- catText: "Texto",
- catLists: "Listas",
- catColour: "Color",
- catVariables: "Variables",
- catFunctions: "Funciones",
- listVariable: "lista",
- textVariable: "texto",
- httpRequestError: "Hubo un problema con la petición.",
- linkAlert: "Comparte tus bloques con este enlace:\n\n%1",
- hashError: "«%1» no corresponde con ningún programa guardado.",
- xmlError: "No se pudo cargar el archivo guardado. ¿Quizá fue creado con otra versión de Blockly?",
- badXml: "Error de análisis XML:\n%1\n\nSelecciona OK para abandonar tus cambios o Cancelar para seguir editando el XML."
-};
diff --git a/demos/code/msg/fa.js b/demos/code/msg/fa.js
deleted file mode 100644
index 96dd966b00..0000000000
--- a/demos/code/msg/fa.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "کد",
- blocks: "بلوکها",
- linkTooltip: "ذخیره و پیوند به بلوکها.",
- runTooltip: "اجرای برنامهٔ تعریفشده توسط بلوکها در فضای کار.",
- badCode: "خطای برنامه:\n%1",
- timeout: "حداکثر تکرارهای اجرا رد شدهاست.",
- trashTooltip: "دورریختن همهٔ بلوکها.",
- catLogic: "منطق",
- catLoops: "حلقهها",
- catMath: "ریاضی",
- catText: "متن",
- catLists: "فهرستها",
- catColour: "رنگ",
- catVariables: "متغییرها",
- catFunctions: "توابع",
- listVariable: "فهرست",
- textVariable: "متن",
- httpRequestError: "مشکلی با درخواست وجود داشت.",
- linkAlert: "اشتراکگذاری بلاکهایتان با این پیوند:\n\n%1",
- hashError: "شرمنده، «%1» با هیچ برنامهٔ ذخیرهشدهای تطبیق پیدا نکرد.",
- xmlError: "نتوانست پروندهٔ ذخیرهٔ شما بارگیری شود. احتمالاً با نسخهٔ متفاوتی از بلوکی درست شدهاست؟",
- badXml: "خطای تجزیهٔ اکسامال:\n%1\n\n«باشد» را برای ذخیره و «فسخ» را برای ویرایش بیشتر اکسامال انتخاب کنید."
-};
diff --git a/demos/code/msg/fr.js b/demos/code/msg/fr.js
deleted file mode 100644
index 60181131ad..0000000000
--- a/demos/code/msg/fr.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Code",
- blocks: "Blocs",
- linkTooltip: "Sauvegarder et lier aux blocs.",
- runTooltip: "Lancer le programme défini par les blocs dans l’espace de travail.",
- badCode: "Erreur du programme :\n%1",
- timeout: "Nombre maximum d’itérations d’exécution dépassé.",
- trashTooltip: "Jeter tous les blocs.",
- catLogic: "Logique",
- catLoops: "Boucles",
- catMath: "Math",
- catText: "Texte",
- catLists: "Listes",
- catColour: "Couleur",
- catVariables: "Variables",
- catFunctions: "Fonctions",
- listVariable: "liste",
- textVariable: "texte",
- httpRequestError: "Il y a eu un problème avec la demande.",
- linkAlert: "Partagez vos blocs grâce à ce lien:\n\n%1",
- hashError: "Désolé, '%1' ne correspond à aucun programme sauvegardé.",
- xmlError: "Impossible de charger le fichier de sauvegarde. Peut être a t-il été créé avec une autre version de Blockly?",
- badXml: "Erreur d’analyse du XML :\n%1\n\nSélectionner 'OK' pour abandonner vos modifications ou 'Annuler' pour continuer à modifier le XML."
-};
diff --git a/demos/code/msg/he.js b/demos/code/msg/he.js
deleted file mode 100644
index dc2826620d..0000000000
--- a/demos/code/msg/he.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "קוד",
- blocks: "קטעי קוד",
- linkTooltip: "שמירה וקישור לקטעי קוד.",
- runTooltip: "הרצת התכנית שהוגדרה על ידי קטעי הקוד שבמרחב העבודה.",
- badCode: "שגיאה בתכנית: %1",
- timeout: "חריגה ממספר פעולות חוזרות אפשריות.",
- trashTooltip: "השלך את כל קטעי הקוד.",
- catLogic: "לוגיקה",
- catLoops: "לולאות",
- catMath: "מתמטיקה",
- catText: "טקסט",
- catLists: "רשימות",
- catColour: "צבע",
- catVariables: "משתנים",
- catFunctions: "פונקציות",
- listVariable: "רשימה",
- textVariable: "טקסט",
- httpRequestError: "הבקשה נכשלה.",
- linkAlert: "ניתן לשתף את קטעי הקוד שלך באמצעות קישור זה:\n\n%1",
- hashError: "לצערנו, '%1' איננו מתאים לאף אחת מהתוכניות השמורות",
- xmlError: "נסיון הטעינה של הקובץ השמור שלך נכשל. האם ייתכן שהוא נוצר בגרסא שונה של בלוקלי?",
- badXml: "תקלה בפענוח XML:\n\n%1\n\nנא לבחור 'אישור' כדי לנטוש את השינויים שלך או 'ביטול' כדי להמשיך ולערוך את ה־XML."
-};
diff --git a/demos/code/msg/hrx.js b/demos/code/msg/hrx.js
deleted file mode 100644
index ca0dea3fc3..0000000000
--- a/demos/code/msg/hrx.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Code",
- blocks: "Bausten",
- linkTooltip: "Speichre und auf Bausten verlinke.",
- runTooltip: "Das Programm ausfüahre, das von den Bausten im Oorweitsbereich definiert ist.",
- badCode: "Programmfehler:\n%1",
- timeout: "Die maximale Ausführungswiederholunge woore üwerschritt.",
- trashTooltip: "All Bausten verwerfe.",
- catLogic: "Logik",
- catLoops: "Schleife",
- catMath: "Mathematik",
- catText: "Text",
- catLists: "Liste",
- catColour: "Farreb",
- catVariables: "Variable",
- catFunctions: "Funktione",
- listVariable: "List",
- textVariable: "Text",
- httpRequestError: "Mit der Oonfroch hots en Problem geb.",
- linkAlert: "Tel von dein Bausten mit dem Link:\n\n%1",
- hashError: "„%1“ stimmt leider mit kenem üweren gespeicherte Programm.",
- xmlError: "Dein gespeicherte Datei könnt net gelood sin. Vielleicht woard se mit ener annre Version von Blockly erstellt.",
- badXml: "Fehler beim Parse von XML:\n%1\n\nWähle 'OK' zum Verwerfe von deiner Ändrunge orrer 'Abbreche' zum XML weiter beoorbeite."
-};
diff --git a/demos/code/msg/hu.js b/demos/code/msg/hu.js
deleted file mode 100644
index eee1868634..0000000000
--- a/demos/code/msg/hu.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kódszerkesztő",
- blocks: "Blokkok",
- linkTooltip: "Hivatkozás létrehozása",
- runTooltip: "Program futtatása.",
- badCode: "Program hiba:\n%1",
- timeout: "A program elérte a maximális végrehajtási időt.",
- trashTooltip: "Összes blokk törlése.",
- catLogic: "Logikai műveletek",
- catLoops: "Ciklusok",
- catMath: "Matematikai műveletek",
- catText: "Sztring műveletek",
- catLists: "Listakezelés",
- catColour: "Színek",
- catVariables: "Változók",
- catFunctions: "Eljárások",
- listVariable: "lista",
- textVariable: "szöveg",
- httpRequestError: "A kéréssel kapcsolatban probléma merült fel.",
- linkAlert: "Ezzel a hivatkozással tudod megosztani a programodat:\n\n%1",
- hashError: "Sajnos a '%1' hivatkozás nem tartozik egyetlen programhoz sem.",
- xmlError: "A programodat nem lehet betölteni. Elképzelhető, hogy a Blockly egy másik verziójában készült?",
- badXml: "Hiba az XML feldolgozásakor:\n%1\n\nVáltozások elvetése?"
-};
diff --git a/demos/code/msg/ia.js b/demos/code/msg/ia.js
deleted file mode 100644
index 5938458bb9..0000000000
--- a/demos/code/msg/ia.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Codice",
- blocks: "Blocos",
- linkTooltip: "Salveguardar e ligar a blocos.",
- runTooltip: "Executar le programma definite per le blocos in le spatio de travalio.",
- badCode: "Error del programma:\n%1",
- timeout: "Le numero de iterationes executate ha excedite le maximo.",
- trashTooltip: "Abandonar tote le blocos.",
- catLogic: "Logica",
- catLoops: "Buclas",
- catMath: "Mathematica",
- catText: "Texto",
- catLists: "Listas",
- catColour: "Color",
- catVariables: "Variabiles",
- catFunctions: "Functiones",
- listVariable: "lista",
- textVariable: "texto",
- httpRequestError: "Il habeva un problema con le requesta.",
- linkAlert: "Divide tu blocos con iste ligamine:\n\n%1",
- hashError: "Infelicemente, '%1' non corresponde a alcun programma salveguardate.",
- xmlError: "Impossibile cargar le file salveguardate. Pote esser que illo ha essite create con un altere version de Blockly?",
- badXml: "Error de analyse del XML:\n%1\n\nSelige 'OK' pro abandonar le modificationes o 'Cancellar' pro continuar a modificar le codice XML."
-};
diff --git a/demos/code/msg/is.js b/demos/code/msg/is.js
deleted file mode 100644
index fa5c40d80a..0000000000
--- a/demos/code/msg/is.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kóði",
- blocks: "Kubbar",
- linkTooltip: "Vista og tengja við kubba.",
- runTooltip: "Keyra forritið sem kubbarnir á vinnusvæðinu mynda.",
- badCode: "Villa í forriti:\n%1",
- timeout: "Forritið hefur endurtekið sig of oft.",
- trashTooltip: "Fleygja öllum kubbum.",
- catLogic: "Rökvísi",
- catLoops: "Lykkjur",
- catMath: "Reikningur",
- catText: "Texti",
- catLists: "Listar",
- catColour: "Litir",
- catVariables: "Breytur",
- catFunctions: "Stefjur",
- listVariable: "listi",
- textVariable: "texti",
- httpRequestError: "Það kom upp vandamál með beiðnina.",
- linkAlert: "Deildu kubbunum þínum með þessari krækju:",
- hashError: "Því miður, '%1' passar ekki við neitt vistað forrit.",
- xmlError: "Gat ekki hlaðið vistuðu skrána þína. Var hún kannske búin til í annarri útgáfu af Blockly?",
- badXml: "Villa við úrvinnslu XML:\n%1\n\nVeldu 'Í lagi' til að sleppa breytingum eða 'Hætta við' til að halda áfram með XML."
-};
diff --git a/demos/code/msg/it.js b/demos/code/msg/it.js
deleted file mode 100644
index cd69b66b31..0000000000
--- a/demos/code/msg/it.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Codice",
- blocks: "Blocchi",
- linkTooltip: "Salva e collega ai blocchi.",
- runTooltip: "Esegui il programma definito dai blocchi nell'area di lavoro.",
- badCode: "Errore programma:\n%1",
- timeout: "È stato superato il numero massimo consentito di interazioni eseguite.",
- trashTooltip: "Elimina tutti i blocchi.",
- catLogic: "Logica",
- catLoops: "Cicli",
- catMath: "Matematica",
- catText: "Testo",
- catLists: "Elenchi",
- catColour: "Colore",
- catVariables: "Variabili",
- catFunctions: "Funzioni",
- listVariable: "elenco",
- textVariable: "testo",
- httpRequestError: "La richiesta non è stata soddisfatta.",
- linkAlert: "Condividi i tuoi blocchi con questo collegamento:\n\n%1",
- hashError: "Mi spiace, '%1' non corrisponde ad alcun programma salvato.",
- xmlError: "Non è stato possibile caricare il documento. Forse è stato creato con una versione diversa di Blockly?",
- badXml: "Errore durante l'analisi XML:\n%1\n\nSeleziona 'OK' per abbandonare le modifiche o 'Annulla' per continuare a modificare l'XML."
-};
diff --git a/demos/code/msg/ja.js b/demos/code/msg/ja.js
deleted file mode 100644
index 6d50419825..0000000000
--- a/demos/code/msg/ja.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "コード",
- blocks: "ブロック",
- linkTooltip: "ブロックの状態を保存してリンクを取得します。",
- runTooltip: "ブロックで作成したプログラムを実行します。",
- badCode: "プログラムのエラー:\n%1",
- timeout: "命令の実行回数が制限値を超えました。",
- trashTooltip: "すべてのブロックを消します。",
- catLogic: "論理",
- catLoops: "繰り返し",
- catMath: "数学",
- catText: "テキスト",
- catLists: "リスト",
- catColour: "色",
- catVariables: "変数",
- catFunctions: "関数",
- listVariable: "リスト",
- textVariable: "テキスト",
- httpRequestError: "ネットワーク接続のエラーです。",
- linkAlert: "ブロックの状態をこのリンクで共有できます:\n\n%1",
- hashError: "すみません。「%1」という名前のプログラムは保存されていません。",
- xmlError: "保存されたファイルを読み込めませんでした。別のバージョンのブロックリーで作成された可能性があります。",
- badXml: "XML のエラーです:\n%1\n\nXML の変更をやめるには「OK」、編集を続けるには「キャンセル」を選んでください。"
-};
diff --git a/demos/code/msg/ko.js b/demos/code/msg/ko.js
deleted file mode 100644
index 990cb6f236..0000000000
--- a/demos/code/msg/ko.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "코드",
- blocks: "블록",
- linkTooltip: "블록을 저장하고 링크를 가져옵니다.",
- runTooltip: "작업 공간에서 블록으로 정의된 프로그램을 실행합니다.",
- badCode: "프로그램 오류:\n%1",
- timeout: "최대 실행 반복을 초과했습니다.",
- trashTooltip: "모든 블록을 버립니다.",
- catLogic: "논리",
- catLoops: "반복",
- catMath: "수학",
- catText: "텍스트",
- catLists: "목록",
- catColour: "색",
- catVariables: "변수",
- catFunctions: "기능",
- listVariable: "목록",
- textVariable: "텍스트",
- httpRequestError: "요청에 문제가 있습니다.",
- linkAlert: "다음 링크로 블록을 공유하세요:\n\n%1",
- hashError: "죄송하지만 '%1'은 어떤 저장된 프로그램으로 일치하지 않습니다.",
- xmlError: "저장된 파일을 불러올 수 없습니다. 혹시 블록리의 다른 버전으로 만들었습니까?",
- badXml: "XML 구문 분석 오류:\n%1\n\n바뀜을 포기하려면 '확인'을 선택하고 XML을 더 편집하려면 '취소'를 선택하세요."
-};
diff --git a/demos/code/msg/mk.js b/demos/code/msg/mk.js
deleted file mode 100644
index cc344a69ca..0000000000
--- a/demos/code/msg/mk.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Код",
- blocks: "Блокчиња",
- linkTooltip: "Зачувај и стави врска до блокчињата.",
- runTooltip: "Пушти го програмот определен од блокчињата во работниот простор.",
- badCode: "Грешка во програмот:\n%1",
- timeout: "Го надминавте допуштениот број на повторувања во извршувањето.",
- trashTooltip: "Отстрани ги сите блокчиња.",
- catLogic: "Логика",
- catLoops: "Јамки",
- catMath: "Математика",
- catText: "Текст",
- catLists: "Списоци",
- catColour: "Боја",
- catVariables: "Променливи",
- catFunctions: "Функции",
- listVariable: "список",
- textVariable: "текст",
- httpRequestError: "Се појави проблем во барањето.",
- linkAlert: "Споделете ги вашите блокчиња со оваа врска:\n\n%1",
- hashError: "„%1“ не одговара на ниеден зачуван програм.",
- xmlError: "Не можев да ја вчитам зачуваната податотека. Да не сте ја создале со друга верзија на Blockly?",
- badXml: "Грешка при расчленувањето на XML:\n%1\n\nСтиснете на „ОК“ за да ги напуштите промените или на „Откажи“ ако сакате уште да ја уредувате XML-податотеката."
-};
diff --git a/demos/code/msg/ms.js b/demos/code/msg/ms.js
deleted file mode 100644
index f3801cd0bb..0000000000
--- a/demos/code/msg/ms.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kod",
- blocks: "Blok",
- linkTooltip: "Simpan dan pautkan kepada blok.",
- runTooltip: "Jalankan aturcara yang ditetapkan oleh blok-blok di dalam ruang kerja.",
- badCode: "Ralat atur cara:\n%1",
- timeout: "Takat maksimum lelaran pelaksanaan dicecah.",
- trashTooltip: "Buang semua Blok.",
- catLogic: "Logik",
- catLoops: "Gelung",
- catMath: "Matematik",
- catText: "Teks",
- catLists: "Senarai",
- catColour: "Warna",
- catVariables: "Pemboleh ubah",
- catFunctions: "Fungsi",
- listVariable: "senarai",
- textVariable: "teks",
- httpRequestError: "Permintaan itu terdapat masalah.",
- linkAlert: "Kongsikan blok-blok anda dengan pautan ini:\n\n%1",
- hashError: "Maaf, '%1' tidak berpadanan dengan sebarang aturcara yang disimpan.",
- xmlError: "Fail simpanan anda tidak dapat dimuatkan. Jangan-jangan ia dicipta dengan versi Blockly yang berlainan?",
- badXml: "Ralat ketika menghuraian XML:\n%1\n\nPilih 'OK' untuk melucutkan suntingan anda atau 'Batal' untuk bersambung menyunting XML-nya."
-};
diff --git a/demos/code/msg/nb.js b/demos/code/msg/nb.js
deleted file mode 100644
index 60ac0394d6..0000000000
--- a/demos/code/msg/nb.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kode",
- blocks: "Blokker",
- linkTooltip: "Lagre og lenke til blokker.",
- runTooltip: "Kjør programmet definert av blokken i arbeidsområdet.",
- badCode: "Programfeil:\n%1",
- timeout: "Det maksimale antallet utførte looper er oversteget.",
- trashTooltip: "Fjern alle blokker",
- catLogic: "Logikk",
- catLoops: "Looper",
- catMath: "Matte",
- catText: "Tekst",
- catLists: "Lister",
- catColour: "Farge",
- catVariables: "Variabler",
- catFunctions: "Funksjoner",
- listVariable: "Liste",
- textVariable: "Tekst",
- httpRequestError: "Det oppsto et problem med forespørselen din",
- linkAlert: "Del dine blokker med denne lenken:\n\n%1",
- hashError: "Beklager, '%1' samsvarer ikke med noe lagret program.",
- xmlError: "Kunne ikke laste inn filen. Kanskje den ble laget med en annen versjon av Blockly?",
- badXml: "Feil ved parsering av XML:\n%1\n\nVelg 'OK' for å avbryte endringene eller 'Cancel' for å fortsette å redigere XML-koden."
-};
diff --git a/demos/code/msg/nl.js b/demos/code/msg/nl.js
deleted file mode 100644
index 339a5e8827..0000000000
--- a/demos/code/msg/nl.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Code",
- blocks: "Blokken",
- linkTooltip: "Opslaan en koppelen naar blokken.",
- runTooltip: "Voer het programma uit dat met de blokken in de werkruimte is gemaakt.",
- badCode: "Programmafout:\n%1",
- timeout: "Het maximale aantal iteraties is overschreden.",
- trashTooltip: "Alle blokken verwijderen",
- catLogic: "Logica",
- catLoops: "Lussen",
- catMath: "Formules",
- catText: "Tekst",
- catLists: "Lijsten",
- catColour: "Kleur",
- catVariables: "Variabelen",
- catFunctions: "Functies",
- listVariable: "lijst",
- textVariable: "tekst",
- httpRequestError: "Er is een probleem opgetreden tijdens het verwerken van het verzoek.",
- linkAlert: "Deel uw blokken via deze koppeling:\n\n%1",
- hashError: "\"%1\" komt helaas niet overeen met een opgeslagen bestand.",
- xmlError: "Uw opgeslagen bestand kan niet geladen worden. Is het misschien gemaakt met een andere versie van Blockly?",
- badXml: "Fout tijdens het verwerken van de XML:\n%1\n\nSelecteer \"OK\" om uw wijzigingen te negeren of \"Annuleren\" om de XML verder te bewerken."
-};
diff --git a/demos/code/msg/oc.js b/demos/code/msg/oc.js
deleted file mode 100644
index c184ac8fb2..0000000000
--- a/demos/code/msg/oc.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Còde",
- blocks: "Blòts",
- linkTooltip: "Salva e liga als blòts.",
- runTooltip: "Aviar lo programa definit pels blòts dins l’espaci de trabalh.",
- badCode: "Error del programa :\n%1",
- timeout: "Nombre maximum d’iteracions d’execucion depassat.",
- trashTooltip: "Getar totes los blòts.",
- catLogic: "Logic",
- catLoops: "Boclas",
- catMath: "Math",
- catText: "Tèxte",
- catLists: "Listas",
- catColour: "Color",
- catVariables: "Variablas",
- catFunctions: "Foncions",
- listVariable: "lista",
- textVariable: "tèxte",
- httpRequestError: "I a agut un problèma amb la demanda.",
- linkAlert: "Partejatz vòstres blòts gràcia a aqueste ligam :\n\n%1",
- hashError: "O planhèm, '%1' correspond pas a un fichièr Blockly salvament.",
- xmlError: "Impossible de cargar lo fichièr de salvament. Benlèu qu'es estat creat amb una autra version de Blockly ?",
- badXml: "Error d’analisi del XML :\n%1\n\nSeleccionar 'D'acòrdi' per abandonar vòstras modificacions o 'Anullar' per modificar encara lo XML."
-};
diff --git a/demos/code/msg/pl.js b/demos/code/msg/pl.js
deleted file mode 100644
index 83bb05d2f9..0000000000
--- a/demos/code/msg/pl.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kod",
- blocks: "Bloki",
- linkTooltip: "Zapisz i podlinkuj do bloków",
- runTooltip: "Uruchom program zdefinowany przez bloki w obszarze roboczym",
- badCode: "Błąd programu:\n%1",
- timeout: "Maksymalna liczba iteracji wykonywań przekroczona",
- trashTooltip: "Odrzuć wszystkie bloki.",
- catLogic: "Logika",
- catLoops: "Pętle",
- catMath: "Matematyka",
- catText: "Tekst",
- catLists: "Listy",
- catColour: "Kolor",
- catVariables: "Zmienne",
- catFunctions: "Funkcje",
- listVariable: "lista",
- textVariable: "tekst",
- httpRequestError: "Wystąpił problem z żądaniem.",
- linkAlert: "Udpostępnij swoje bloki korzystając z poniższego linku : \n\n\n%1",
- hashError: "Przepraszamy, \"%1\" nie odpowiada żadnemu zapisanemu programowi.",
- xmlError: "Nie można załadować zapisanego pliku. Być może został utworzony za pomocą innej wersji Blockly?",
- badXml: "Błąd parsowania XML : \n%1\n\nZaznacz 'OK' aby odrzucić twoje zmiany lub 'Cancel', żeby w przyszłości edytować XML."
-};
diff --git a/demos/code/msg/pms.js b/demos/code/msg/pms.js
deleted file mode 100644
index a7704f764d..0000000000
--- a/demos/code/msg/pms.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Còdes",
- blocks: "Blòch",
- linkTooltip: "Argistré e lijé ai blòch.",
- runTooltip: "Fé andé ël programa definì dai blòch ant lë spassi ëd travaj.",
- badCode: "Eror dël programa:\n%1",
- timeout: "Nùmer màssim d'arpetission d'esecussion sorpassà.",
- trashTooltip: "Scarté tuti ij blòch.",
- catLogic: "Lògica",
- catLoops: "Liasse",
- catMath: "Matemàtica",
- catText: "Test",
- catLists: "Liste",
- catColour: "Color",
- catVariables: "Variàbij",
- catFunctions: "Fonsion",
- listVariable: "lista",
- textVariable: "test",
- httpRequestError: "A-i é staje un problema con l'arcesta.",
- linkAlert: "Ch'a partagia ij sò blòch grassie a sta liura: %1",
- hashError: "An dëspias, '%1 a corëspond a gnun programa salvà.",
- xmlError: "A l'é nen podusse carié so archivi salvà. Miraco a l'é stàit creà con na version diferenta ëd Blockly?",
- badXml: "Eror d'anàlisi dl'XML:\n%1\n\nSelessioné 'Va bin' për lassé perde toe modìfiche o 'Anulé' për modifiché ancora l'XML."
-};
diff --git a/demos/code/msg/pt-br.js b/demos/code/msg/pt-br.js
deleted file mode 100644
index 9b18d63600..0000000000
--- a/demos/code/msg/pt-br.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Código",
- blocks: "Blocos",
- linkTooltip: "Salvar e ligar aos blocos.",
- runTooltip: "Execute o programa definido pelos blocos na área de trabalho.",
- badCode: "Erro no programa:\n%1",
- timeout: "Máximo de iterações de execução excedido.",
- trashTooltip: "Descartar todos os blocos.",
- catLogic: "Lógica",
- catLoops: "Laços",
- catMath: "Matemática",
- catText: "Texto",
- catLists: "Listas",
- catColour: "Cor",
- catVariables: "Variáveis",
- catFunctions: "Funções",
- listVariable: "lista",
- textVariable: "texto",
- httpRequestError: "Houve um problema com a requisição.",
- linkAlert: "Compartilhe seus blocos com este link:\n\n%1",
- hashError: "Desculpe, '%1' não corresponde a um programa salvo.",
- xmlError: "Não foi possível carregar seu arquivo salvo. Talvez ele tenha sido criado com uma versão diferente do Blockly?",
- badXml: "Erro de análise XML:\n%1\n\nSelecione 'OK' para abandonar suas mudanças ou 'Cancelar' para editar o XML."
-};
diff --git a/demos/code/msg/ro.js b/demos/code/msg/ro.js
deleted file mode 100644
index cd3de11d79..0000000000
--- a/demos/code/msg/ro.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Cod",
- blocks: "Blocuri",
- linkTooltip: "Salvează și adaugă la blocuri.",
- runTooltip: "Execută programul definit de către blocuri în spațiul de lucru.",
- badCode: "Eroare de program:\n%1",
- timeout: "Numărul maxim de iterații a fost depășit.",
- trashTooltip: "Șterge toate blocurile.",
- catLogic: "Logic",
- catLoops: "Bucle",
- catMath: "Matematică",
- catText: "Text",
- catLists: "Liste",
- catColour: "Culoare",
- catVariables: "Variabile",
- catFunctions: "Funcții",
- listVariable: "listă",
- textVariable: "text",
- httpRequestError: "A apărut o problemă la solicitare.",
- linkAlert: "Distribuie-ți blocurile folosind această legătură:\n\n%1",
- hashError: "Scuze, „%1” nu corespunde nici unui program salvat.",
- xmlError: "Sistemul nu a putut încărca fișierul salvat. Poate că a fost creat cu o altă versiune de Blockly?",
- badXml: "Eroare de parsare XML:\n%1\n\nAlege „OK” pentru a renunța la modificările efectuate sau „Revocare” pentru a modifica în continuare fișierul XML."
-};
diff --git a/demos/code/msg/ru.js b/demos/code/msg/ru.js
deleted file mode 100644
index 389e9052a3..0000000000
--- a/demos/code/msg/ru.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Код",
- blocks: "Блоки",
- linkTooltip: "Сохранить и показать ссылку на блоки.",
- runTooltip: "Запустить программу, заданную блоками в рабочей области.",
- badCode: "Ошибка программы:\n%1",
- timeout: "Превышено максимальное количество итераций.",
- trashTooltip: "Удалить все блоки.",
- catLogic: "Логические",
- catLoops: "Циклы",
- catMath: "Математика",
- catText: "Текст",
- catLists: "Списки",
- catColour: "Цвет",
- catVariables: "Переменные",
- catFunctions: "Функции",
- listVariable: "список",
- textVariable: "текст",
- httpRequestError: "Произошла проблема при запросе.",
- linkAlert: "Поделитесь своими блоками по этой ссылке:\n\n%1",
- hashError: "К сожалению, «%1» не соответствует ни одному сохраненному файлу Блокли.",
- xmlError: "Не удалось загрузить ваш сохраненный файл. Возможно, он был создан в другой версии Блокли?",
- badXml: "Ошибка синтаксического анализа XML:\n%1\n\nВыберите 'ОК', чтобы отказаться от изменений или 'Cancel' для дальнейшего редактирования XML."
-};
diff --git a/demos/code/msg/sc.js b/demos/code/msg/sc.js
deleted file mode 100644
index 82f94d61d0..0000000000
--- a/demos/code/msg/sc.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Còdixi",
- blocks: "Brocus",
- linkTooltip: "Sarva e alliòngia a is brocus.",
- runTooltip: "Arròllia su programa cumpostu de is brocus in s'àrea de traballu.",
- badCode: "Errori in su Programa:\n%1",
- timeout: "Giai lòmpius a su màssimu numeru de repicus.",
- trashTooltip: "Boganci totu is brocus.",
- catLogic: "Lògica",
- catLoops: "Lòrigas",
- catMath: "Matemàtica",
- catText: "Testu",
- catLists: "Lista",
- catColour: "Colori",
- catVariables: "Variabilis",
- catFunctions: "Funtzionis",
- listVariable: "lista",
- textVariable: "testu",
- httpRequestError: "Ddui fut unu problema cun sa pregunta",
- linkAlert: "Poni is brocus tuus in custu acàpiu:\n\n%1",
- hashError: "Mi dispraxit, '%1' non torrat a pari cun nimancu unu de is programas sarvaus.",
- xmlError: "Non potzu carrigai su file sarvau. Fortzis est stètiu fatu cun d-una versioni diferenti de Blockly?",
- badXml: "Errori in s'anàlisi XML:\n%1\n\nCraca 'OK' po perdi is mudàntzias 'Anudda' po sighì a scriri su XML."
-};
diff --git a/demos/code/msg/sk.js b/demos/code/msg/sk.js
deleted file mode 100644
index 3917df293e..0000000000
--- a/demos/code/msg/sk.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kód",
- blocks: "Bloky",
- linkTooltip: "Uložiť a zdieľať odkaz na tento program.",
- runTooltip: "Spustiť program, zložený z dielcov na pracovnej ploche.",
- badCode: "Chyba v programe:\n%1",
- timeout: "Bol prekročený maximálny počet opakovaní.",
- trashTooltip: "Zahodiť všetky dielce.",
- catLogic: "Logika",
- catLoops: "Cykly",
- catMath: "Matematické",
- catText: "Text",
- catLists: "Zoznamy",
- catColour: "Farby",
- catVariables: "Premenné",
- catFunctions: "Funkcie",
- listVariable: "zoznam",
- textVariable: "text",
- httpRequestError: "Problém so spracovaním požiadavky.",
- linkAlert: "Zdieľať tento program skopírovaním odkazu\n\n%1",
- hashError: "Prepáč, '%1' nie je meno žiadnemu uloženému programu.",
- xmlError: "Nebolo možné načítať uložený súbor. Možno bol vytvorený v inej verzii Blocky.",
- badXml: "Chyba pri parsovaní XML:\n%1\n\nStlačte 'OK' ak chcete zrušiť zmeny alebo 'Zrušiť' pre pokračovanie v úpravách XML."
-};
diff --git a/demos/code/msg/sr.js b/demos/code/msg/sr.js
deleted file mode 100644
index f9a026ca08..0000000000
--- a/demos/code/msg/sr.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Кôд",
- blocks: "Блокови",
- linkTooltip: "Сачувајте и повежите са блоковима.",
- runTooltip: "Покрените програм заснован на блоковима у радном простору.",
- badCode: "Грешка у програму:\n%1",
- timeout: "Достигнут је максималан број понављања у извршавању.",
- trashTooltip: "Одбаците све блокове.",
- catLogic: "Логика",
- catLoops: "Петље",
- catMath: "Математика",
- catText: "Текст",
- catLists: "Спискови",
- catColour: "Боја",
- catVariables: "Променљиве",
- catFunctions: "Процедуре",
- listVariable: "списак",
- textVariable: "текст",
- httpRequestError: "Дошло је до проблема у захтеву.",
- linkAlert: "Делите своје блокове овом везом:\n\n%1",
- hashError: "„%1“ не одговара ниједном сачуваном програму.",
- xmlError: "Не могу да учитам сачувану датотеку. Можда је направљена другом верзијом Blockly-ја.",
- badXml: "Грешка при рашчлањивању XML-а:\n%1\n\nПритисните „У реду“ да напустите измене или „Откажи“ да наставите са уређивањем XML датотеке."
-};
diff --git a/demos/code/msg/sv.js b/demos/code/msg/sv.js
deleted file mode 100644
index 4134a4c7dc..0000000000
--- a/demos/code/msg/sv.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kod",
- blocks: "Block",
- linkTooltip: "Spara och länka till block.",
- runTooltip: "Kör programmet som definierats av blocken i arbetsytan.",
- badCode: "Programfel:\n%1",
- timeout: "Det maximala antalet utförda loopar har överskridits.",
- trashTooltip: "Släng alla block.",
- catLogic: "Logik",
- catLoops: "Loopar",
- catMath: "Matematik",
- catText: "Text",
- catLists: "Listor",
- catColour: "Färg",
- catVariables: "Variabler",
- catFunctions: "Funktioner",
- listVariable: "lista",
- textVariable: "text",
- httpRequestError: "Det uppstod ett problem med begäran.",
- linkAlert: "Dela dina block med denna länk: \n\n%1",
- hashError: "Tyvärr, '%1' överensstämmer inte med något sparat program.",
- xmlError: "Kunde inte läsa din sparade fil. Den skapades kanske med en annan version av Blockly?",
- badXml: "Fel vid parsning av XML:\n%1\n\nKlicka på 'OK' för att strunta i dina ändringar eller 'Avbryt' för att fortsätta redigera XML-koden."
-};
diff --git a/demos/code/msg/ta.js b/demos/code/msg/ta.js
deleted file mode 100644
index ff472e2b7e..0000000000
--- a/demos/code/msg/ta.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "கணினி நிரல்", //Code
- blocks: "நிரல் துண்டு", //block
- linkTooltip: "சேமித்து நிரல் துண்டிற்கு இணைக்க", //save and link to block
- runTooltip: "பணிமனை நினைவகத்தில் இயக்குக", //Run the program defined by the blocks in the workspace.
- badCode: "கணினி நிரல் கோளாறு:\n%1",
- timeout: "அதிகபட்ச அடுக்கின் அளவை மீரியது", //max iters reached/exceeded
- trashTooltip: "நீக்கு",
- catLogic: "தர்க வகை",
- catLoops: "மடக்கு வாக்கியம்",
- catMath: "கணிதம்",
- catText: "உரை",
- catLists: "பட்டியல்",
- catColour: "வண்ணம்",
- catVariables: "மாறிகள்",
- catFunctions: "சார்புகள்",
- listVariable: "பட்டியல் மாறி",
- textVariable: "உரை சரம்",
- httpRequestError: "இந்த செயலை இயக்குவதில் கோளாறு ஏற்பட்டது",
- linkAlert: "இந்த சுட்டி வழியாக நிரல் துண்டுகளை பகிரவும்:\n\n%1",
- hashError: "'%1' : இது சேமித்த நிரலாக தெரியவில்லை.",
- xmlError: "உங்களது நிரலை காணவில்லை; வேறு Blockly அத்தியாயத்தில் சேமித்தீரா?",
- badXml: "XML பகுப்பதில் கோளாறு:\n%1\n\nOK' கிளிக் செய்தால் மாற்றங்கள் இழப்பீர்கள்; பிழைகளுடன் தொடர 'Cancel' கிளிக் செய்யவும்."
-};
diff --git a/demos/code/msg/th.js b/demos/code/msg/th.js
deleted file mode 100644
index 1ac36245bd..0000000000
--- a/demos/code/msg/th.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "เขียนโปรแกรม",
- blocks: "บล็อก",
- linkTooltip: "บันทึกและสร้างลิงก์มายังบล็อกเหล่านี้",
- runTooltip: "เรียกใช้โปรแกรมตามที่กำหนดไว้ด้วยบล็อกที่อยู่ในพื้นที่ทำงาน",
- badCode: "โปรแกรมเกิดข้อผิดพลาด:\n%1",
- timeout: "โปรแกรมทำงานซ้ำคำสั่งเดิมมากเกินไป",
- trashTooltip: "ยกเลิกบล็อกทั้งหมด",
- catLogic: "ตรรกะ",
- catLoops: "การวนซ้ำ",
- catMath: "คณิตศาสตร์",
- catText: "ข้อความ",
- catLists: "รายการ",
- catColour: "สี",
- catVariables: "ตัวแปร",
- catFunctions: "ฟังก์ชัน",
- listVariable: "รายการ",
- textVariable: "ข้อความ",
- httpRequestError: "มีปัญหาเกี่ยวกับการร้องขอ",
- linkAlert: "แบ่งปันบล็อกของคุณด้วยลิงก์นี้:\n\n%1",
- hashError: "เสียใจด้วย '%1' ไม่ตรงกับโปรแกรมใดๆ ที่เคยบันทึกเอาไว้เลย",
- xmlError: "ไม่สามารถโหลดไฟล์ที่บันทึกไว้ของคุณได้ บางทีมันอาจจะถูกสร้างขึ้นด้วย Blockly รุ่นอื่นที่แตกต่างกัน?",
- badXml: "เกิดข้อผิดพลาดในการแยกวิเคราะห์ XML:\n%1\n\nเลือก 'ตกลง' เพื่อละทิ้งการเปลี่ยนแปลงต่างๆ ที่ทำไว้ หรือเลือก 'ยกเลิก' เพื่อแก้ไข XML ต่อไป"
-};
diff --git a/demos/code/msg/tlh.js b/demos/code/msg/tlh.js
deleted file mode 100644
index 8d0c056f93..0000000000
--- a/demos/code/msg/tlh.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "ngoq",
- blocks: "ngoghmey",
- linkTooltip: "",
- runTooltip: "",
- badCode: "Qagh:\n%1",
- timeout: "tlhoy nI'qu' poH.",
- trashTooltip: "",
- catLogic: "meq",
- catLoops: "vIHtaHbogh ghomey",
- catMath: "mI'QeD",
- catText: "ghItlhHommey",
- catLists: "tetlhmey",
- catColour: "rItlh",
- catVariables: "lIwmey",
- catFunctions: "mIwmey",
- listVariable: "tetlh",
- textVariable: "ghItlhHom",
- httpRequestError: "Qapbe' tlhobmeH QIn.",
- linkAlert: "latlhvaD ngoghmeylIj DangeHmeH Quvvam yIlo':\n\n%1",
- hashError: "Do'Ha', ngogh nab pollu'pu'bogh 'oHbe'law' \"%1\"'e'.",
- xmlError: "ngogh nablIj pollu'pu'bogh chu'qa'laHbe' vay'. chaq pollu'pu'DI' ghunmeH ngogh pIm lo'lu'pu'.",
- badXml: "XML yajchu'laHbe' vay':\n%1\n\nchoHmeylIj DalonmeH \"ruch\" yIwIv pagh XML DachoHqa'meH \"qIl\" yIwIv."
-};
diff --git a/demos/code/msg/tr.js b/demos/code/msg/tr.js
deleted file mode 100644
index 1448300922..0000000000
--- a/demos/code/msg/tr.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Kod",
- blocks: "Bloklar",
- linkTooltip: "Blokları ve bağlantı adresini kaydet.",
- runTooltip: "Çalışma alanında bloklar tarafından tanımlanan programını çalıştırın.",
- badCode: "Program hatası:\n %1",
- timeout: "Maksimum yürütme yinelemeleri aşıldı.",
- trashTooltip: "Bütün blokları at.",
- catLogic: "Mantık",
- catLoops: "Döngüler",
- catMath: "Matematik",
- catText: "Metin",
- catLists: "Listeler",
- catColour: "Renk",
- catVariables: "Değişkenler",
- catFunctions: "İşlevler",
- listVariable: "liste",
- textVariable: "metin",
- httpRequestError: "İstek ile ilgili bir problem var.",
- linkAlert: "Bloklarını bu bağlantı ile paylaş:\n\n%1",
- hashError: "Üzgünüz, '%1' hiç bir kaydedilmiş program ile uyuşmuyor.",
- xmlError: "Kaydedilen dosyanız yüklenemiyor\nBlockly'nin önceki sürümü ile kaydedilmiş olabilir mi?",
- badXml: "XML ayrıştırma hatası:\n%1\n\nDeğişikliklerden vazgeçmek için 'Tamam'ı, düzenlemeye devam etmek için 'İptal' seçeneğini seçiniz."
-};
diff --git a/demos/code/msg/uk.js b/demos/code/msg/uk.js
deleted file mode 100644
index 6b34627215..0000000000
--- a/demos/code/msg/uk.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Код",
- blocks: "Блоки",
- linkTooltip: "Зберегти і пов'язати з блоками.",
- runTooltip: "Запустіть програму, визначену блоками у робочій області.",
- badCode: "Помилка програми:\n%1",
- timeout: "Максимальне виконання ітерацій перевищено.",
- trashTooltip: "Відкинути всі блоки.",
- catLogic: "Логіка",
- catLoops: "Петлі",
- catMath: "Математика",
- catText: "Текст",
- catLists: "Списки",
- catColour: "Колір",
- catVariables: "Змінні",
- catFunctions: "Функції",
- listVariable: "список",
- textVariable: "текст",
- httpRequestError: "Виникла проблема із запитом.",
- linkAlert: "Поділитися вашим блоками через посилання:\n\n%1",
- hashError: "На жаль, \"%1\" не відповідає жодній збереженій програмі.",
- xmlError: "Не вдалося завантажити ваш збережений файл. Можливо, він був створений з іншої версії Blockly?",
- badXml: "Помилка синтаксичного аналізу XML:\n%1\n\nВиберіть \"Гаразд\", щоб відмовитися від змін або 'Скасувати' для подальшого редагування XML."
-};
diff --git a/demos/code/msg/vi.js b/demos/code/msg/vi.js
deleted file mode 100644
index 952d024b4c..0000000000
--- a/demos/code/msg/vi.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "Chương trình",
- blocks: "Các mảnh",
- linkTooltip: "Lưu và lấy địa chỉ liên kết.",
- runTooltip: "Chạy chương trình.",
- badCode: "'Lỗi chương trình:\n%1",
- timeout: "Đã vượt quá số lần lặp cho phép.",
- trashTooltip: "Xóa tất cả mọi mảnh.",
- catLogic: "Logic",
- catLoops: "Vòng lặp",
- catMath: "Công thức toán",
- catText: "Văn bản",
- catLists: "Danh sách",
- catColour: "Màu",
- catVariables: "Biến",
- catFunctions: "Hàm",
- listVariable: "danh sách",
- textVariable: "văn bản",
- httpRequestError: "Hoạt động bị trục trặc, không thực hiện được yêu cầu của bạn.",
- linkAlert: "Chia sẻ chương trình của bạn với liên kết sau:\n\n %1",
- hashError: "Không tìm thấy chương trình được lưu ở '%1'.",
- xmlError: "Không mở được chương trình của bạn. Có thể nó nằm trong một phiên bản khác của Blockly?",
- badXml: "Lỗi sử lý XML:\n %1\n\nChọn 'OK' để từ bỏ các thay đổi hoặc 'Hủy' để tiếp tục chỉnh sửa các XML."
-};
diff --git a/demos/code/msg/zh-hans.js b/demos/code/msg/zh-hans.js
deleted file mode 100644
index abf8a6589b..0000000000
--- a/demos/code/msg/zh-hans.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "代码",
- blocks: "块",
- linkTooltip: "保存模块并生成链接。",
- runTooltip: "于工作区中运行块所定义的程式。",
- badCode: "程序错误:\n%1",
- timeout: "超过最大执行行数。",
- trashTooltip: "放弃所有块。",
- catLogic: "逻辑",
- catLoops: "循环",
- catMath: "数学",
- catText: "文本",
- catLists: "列表",
- catColour: "颜色",
- catVariables: "变量",
- catFunctions: "函数",
- listVariable: "列表",
- textVariable: "文本",
- httpRequestError: "请求存在问题。",
- linkAlert: "通过这个链接分享您的模块:\n\n%1",
- hashError: "对不起,没有任何已保存的程序对应'%1' 。",
- xmlError: "无法载入您保存的文件。您是否使用其他版本的Blockly创建该文件的?",
- badXml: "XML解析错误:\n%1\n\n选择“确定”以取消您对XML的修改,或选择“取消”以继续编辑XML。"
-};
diff --git a/demos/code/msg/zh-hant.js b/demos/code/msg/zh-hant.js
deleted file mode 100644
index 48a8b52727..0000000000
--- a/demos/code/msg/zh-hant.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var MSG = {
- title: "程式碼",
- blocks: "積木",
- linkTooltip: "儲存積木組並提供連結。",
- runTooltip: "於工作區中執行積木組所定義的程式。",
- badCode: "程式錯誤:\n%1",
- timeout: "超過最大執行數。",
- trashTooltip: "捨棄所有積木。",
- catLogic: "邏輯",
- catLoops: "迴圈",
- catMath: "數學式",
- catText: "文字",
- catLists: "列表",
- catColour: "顏色",
- catVariables: "變量",
- catFunctions: "流程",
- listVariable: "列表",
- textVariable: "文字",
- httpRequestError: "命令出現錯誤。",
- linkAlert: "透過此連結分享您的積木組:\n\n%1",
- hashError: "對不起,「%1」並未對應任何已保存的程式。",
- xmlError: "未能載入您保存的檔案。或許它是由其他版本的Blockly創建?",
- badXml: "解析 XML 時出現錯誤:\n%1\n\n選擇'確定'以放棄您的更改,或選擇'取消'以進一步編輯 XML。"
-};
diff --git a/demos/code/style.css b/demos/code/style.css
deleted file mode 100644
index e05664f24e..0000000000
--- a/demos/code/style.css
+++ /dev/null
@@ -1,163 +0,0 @@
-html, body {
- height: 100%;
-}
-
-body {
- background-color: #fff;
- font-family: sans-serif;
- margin: 0;
- overflow: hidden;
-}
-
-.farSide {
- text-align: right;
-}
-
-html[dir="RTL"] .farSide {
- text-align: left;
-}
-
-/* Buttons */
-button {
- margin: 5px;
- padding: 10px;
- border-radius: 4px;
- border: 1px solid #ddd;
- font-size: large;
- background-color: #eee;
- color: #000;
-}
-button.primary {
- border: 1px solid #dd4b39;
- background-color: #dd4b39;
- color: #fff;
-}
-button.primary>img {
- opacity: 1;
-}
-button>img {
- opacity: 0.6;
- vertical-align: text-bottom;
-}
-button:hover>img {
- opacity: 1;
-}
-button:active {
- border: 1px solid #888 !important;
-}
-button:hover {
- box-shadow: 2px 2px 5px #888;
-}
-button.disabled:hover>img {
- opacity: 0.6;
-}
-button.disabled {
- display: none;
-}
-button.notext {
- font-size: 10%;
-}
-
-h1 {
- font-weight: normal;
- font-size: 140%;
- margin-left: 5px;
- margin-right: 5px;
-}
-
-/* Tabs */
-#tabRow>td {
- border: 1px solid #ccc;
- border-bottom: none;
-}
-td.tabon {
- border-bottom-color: #ddd !important;
- background-color: #ddd;
- padding: 5px 19px;
-}
-td.taboff {
- cursor: pointer;
- padding: 5px 19px;
-}
-td.taboff:hover {
- background-color: #eee;
-}
-td.tabmin {
- border-top-style: none !important;
- border-left-style: none !important;
- border-right-style: none !important;
-}
-td.tabmax {
- border-top-style: none !important;
- border-left-style: none !important;
- border-right-style: none !important;
- width: 99%;
- padding-left: 10px;
- padding-right: 10px;
- text-align: right;
-}
-html[dir=rtl] td.tabmax {
- text-align: left;
-}
-
-table {
- border-collapse: collapse;
- margin: 0;
- padding: 0;
- border: none;
-}
-td {
- padding: 0;
- vertical-align: top;
-}
-.content {
- visibility: hidden;
- margin: 0;
- padding: 1ex;
- position: absolute;
- direction: ltr;
-}
-pre.content {
- border: 1px solid #ccc;
- overflow: scroll;
-}
-#content_blocks {
- padding: 0;
-}
-.blocklySvg {
- border-top: none !important;
-}
-#content_xml {
- resize: none;
- outline: none;
- border: 1px solid #ccc;
- font-family: monospace;
- overflow: scroll;
-}
-#languageMenu {
- vertical-align: top;
- margin-top: 15px;
- margin-right: 15px;
-}
-
-/* Buttons */
-button {
- padding: 1px 10px;
- margin: 1px 5px;
-}
-
-/* Sprited icons. */
-.icon21 {
- height: 21px;
- width: 21px;
- background-image: url(icons.png);
-}
-.trash {
- background-position: 0px 0px;
-}
-.link {
- background-position: -21px 0px;
-}
-.run {
- background-position: -42px 0px;
-}
diff --git a/demos/fixed/icon.png b/demos/fixed/icon.png
deleted file mode 100644
index 1158acf017..0000000000
Binary files a/demos/fixed/icon.png and /dev/null differ
diff --git a/demos/fixed/index.html b/demos/fixed/index.html
deleted file mode 100644
index c3805bee1c..0000000000
--- a/demos/fixed/index.html
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
- Blockly Demo: Fixed Blockly
-
-
-
-
-
-
-
-
- This is a simple demo of injecting Blockly into a fixed-sized 'div' element.
-
- → More info on injecting fixed-sized Blockly ...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/generator/icon.png b/demos/generator/icon.png
deleted file mode 100644
index 132016e3f2..0000000000
Binary files a/demos/generator/icon.png and /dev/null differ
diff --git a/demos/generator/index.html b/demos/generator/index.html
deleted file mode 100644
index 593eb4ba46..0000000000
--- a/demos/generator/index.html
+++ /dev/null
@@ -1,145 +0,0 @@
-
-
-
-
- Blockly Demo: Generating JavaScript
-
-
-
-
-
-
-
- Blockly >
- Demos > Generating JavaScript
-
- This is a simple demo of generating code from blocks.
-
- → More info on Code Generators ...
-
-
- Show JavaScript
- Run JavaScript
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- EQ
-
-
- MULTIPLY
-
-
- 6
-
-
-
-
- 7
-
-
-
-
-
-
- 42
-
-
-
-
-
-
-
-
- Don't panic
-
-
-
-
-
-
-
-
- Panic
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/graph/icon.png b/demos/graph/icon.png
deleted file mode 100644
index ad8b582f69..0000000000
Binary files a/demos/graph/icon.png and /dev/null differ
diff --git a/demos/graph/index.html b/demos/graph/index.html
deleted file mode 100644
index 83a6b71e25..0000000000
--- a/demos/graph/index.html
+++ /dev/null
@@ -1,344 +0,0 @@
-
-
-
-
- Blockly Demo: Graph
-
-
-
-
-
-
-
-
-
-
- This is a demo of giving instant feedback as blocks are changed.
-
- → More info on Realtime generation ...
-
-
-
-
-
- ...
-
-
-
-
-
-
-
-
- 1
-
-
-
-
- 1
-
-
-
-
-
-
- 9
-
-
-
-
-
-
- 45
-
-
-
-
-
-
-
- 0
-
-
-
-
-
-
- 3.1
-
-
-
-
-
-
- 64
-
-
-
-
- 10
-
-
-
-
-
-
- 50
-
-
-
-
- 1
-
-
-
-
- 100
-
-
-
-
-
-
- 1
-
-
-
-
- 100
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- POWER
-
-
-
- 1
-
-
-
-
- 2
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/headless/icon.png b/demos/headless/icon.png
deleted file mode 100644
index af9ebe7142..0000000000
Binary files a/demos/headless/icon.png and /dev/null differ
diff --git a/demos/headless/index.html b/demos/headless/index.html
deleted file mode 100644
index c3bad9d3bb..0000000000
--- a/demos/headless/index.html
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
- Blockly Demo: Headless
-
-
-
-
-
-
-
-
-
- This is a simple demo of generating Python code from XML with no graphics.
- This might be useful for server-side code generation.
-
-
-
-
- Generate Python ⤴
-
-
-
-
-
-
diff --git a/demos/index.html b/demos/index.html
deleted file mode 100644
index 8af600662e..0000000000
--- a/demos/index.html
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
- Blockly Demos
-
-
-
-
-
- These demos are intended for developers who want to integrate Blockly with
- their own applications.
-
-
-
-
-
-
-
-
-
-
- Inject Blockly into a page as a fixed element.
-
-
-
-
-
-
-
-
-
-
-
- Inject Blockly into a page as a resizable element.
-
-
-
-
-
-
-
-
-
-
-
- Organize blocks into categories for the user.
-
-
-
-
-
-
-
-
-
-
-
- Limit the total number of blocks allowed (for academic exercises).
-
-
-
-
-
-
-
-
-
-
-
- Turn blocks into code and execute it.
-
-
-
-
-
-
-
-
-
-
-
- Generate code from XML without graphics.
-
-
-
-
-
-
-
-
-
-
-
- Step by step execution in JavaScript.
-
-
-
-
-
-
-
-
-
-
-
- Instant updates when blocks are changed.
-
-
-
-
-
-
-
-
-
-
-
- See what Blockly looks like in right-to-left mode (for Arabic and Hebrew).
-
-
-
-
-
-
-
-
-
-
-
- Using Closure Templates to support 35 languages.
-
-
-
-
-
-
-
-
-
-
-
- Save and load blocks with App Engine.
-
-
-
-
-
-
-
-
-
-
-
- Export a Blockly program into JavaScript, Python, PHP, Dart or XML.
-
-
-
-
-
-
-
-
-
-
-
- Build custom blocks using Blockly.
-
-
-
-
-
diff --git a/demos/interpreter/acorn_interpreter.js b/demos/interpreter/acorn_interpreter.js
deleted file mode 100755
index b039ad3901..0000000000
--- a/demos/interpreter/acorn_interpreter.js
+++ /dev/null
@@ -1,130 +0,0 @@
-var mod$$inline_58=function(a){function b(a){n=a||{};for(var b in Ua)Object.prototype.hasOwnProperty.call(n,b)||(n[b]=Ua[b]);wa=n.sourceFile||null}function c(a,b){var c=Ab(k,a);b+=" ("+c.line+":"+c.column+")";var e=new SyntaxError(b);e.pos=a;e.loc=c;e.raisedAt=g;throw e;}function d(a){function b(a){if(1==a.length)return c+="return str === "+JSON.stringify(a[0])+";";c+="switch(str){";for(var va=0;vaa)++g;else if(47===a)if(a=k.charCodeAt(g+1),42===a){var a=n.onComment&&n.locations&&new f,b=g,e=k.indexOf("*/",g+=2);-1===e&&c(g-2,"Unterminated comment");
-g=e+2;if(n.locations){Y.lastIndex=b;for(var d=void 0;(d=Y.exec(k))&&d.index=a?a=P(!0):(++g,a=e(xa)),a;case 40:return++g,e(J);case 41:return++g,e(F);case 59:return++g,e(K);case 44:return++g,e(L);case 91:return++g,e(ja);
-case 93:return++g,e(ka);case 123:return++g,e(Z);case 125:return++g,e(T);case 58:return++g,e(aa);case 63:return++g,e(ya);case 48:if(a=k.charCodeAt(g+1),120===a||88===a)return g+=2,a=l(16),null==a&&c(y+2,"Expected hexadecimal number"),la(k.charCodeAt(g))&&c(g,"Identifier directly after number"),a=e(ba,a);case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:return P(!1);case 34:case 39:a:{g++;for(var b="";;){g>=S&&c(y,"Unterminated string constant");var f=k.charCodeAt(g);if(f===a){++g;
-a=e(da,b);break a}if(92===f){var f=k.charCodeAt(++g),d=/^[0-7]+/.exec(k.slice(g,g+3));for(d&&(d=d[0]);d&&255=S)return e(pa);var b=k.charCodeAt(g);if(la(b)||92===b)return Ya();a=r(b);if(!1===a){b=String.fromCharCode(b);if("\\"===b||Za.test(b))return Ya();c(g,"Unexpected character '"+b+"'")}return a}function u(a,b){var c=k.slice(g,g+b);g+=b;e(a,c)}function D(){for(var a="",b,f,d=g;;){g>=
-S&&c(d,"Unterminated regular expression");a=k.charAt(g);na.test(a)&&c(d,"Unterminated regular expression");if(b)b=!1;else{if("["===a)f=!0;else if("]"===a&&f)f=!1;else if("/"===a&&!f)break;b="\\"===a}++g}a=k.slice(d,g);++g;(b=$a())&&!/^[gmsiy]*$/.test(b)&&c(d,"Invalid regexp flag");return e(Ba,new RegExp(a,b))}function l(a,b){for(var c=g,f=0,e=0,d=null==b?Infinity:b;e=h?h-48:Infinity;if(h>=a)break;++g;f=f*a+h}return g===c||null!=
-b&&g-c!==b?null:f}function P(a){var b=g,f=!1,d=48===k.charCodeAt(g);a||null!==l(10)||c(b,"Invalid number");46===k.charCodeAt(g)&&(++g,l(10),f=!0);a=k.charCodeAt(g);if(69===a||101===a)a=k.charCodeAt(++g),43!==a&&45!==a||++g,null===l(10)&&c(b,"Invalid number"),f=!0;la(k.charCodeAt(g))&&c(g,"Identifier directly after number");a=k.slice(b,g);var h;f?h=parseFloat(a):d&&1!==a.length?/[89]/.test(a)||C?c(b,"Invalid number"):h=parseInt(a,8):h=parseInt(a,10);return e(ba,h)}function ma(a){a=l(16,a);null===a&&
-c(y,"Bad character escape sequence");return a}function $a(){ca=!1;for(var a,b=!0,f=g;;){var e=k.charCodeAt(g);if(ab(e))ca&&(a+=k.charAt(g)),++g;else if(92===e){ca||(a=k.slice(f,g));ca=!0;117!=k.charCodeAt(++g)&&c(g,"Expecting Unicode escape sequence \\uXXXX");++g;var e=ma(4),d=String.fromCharCode(e);d||c(g-1,"Invalid Unicode escape");(b?la(e):ab(e))||c(g-4,"Invalid Unicode escape");a+=d}else break;b=!1}return ca?a:k.slice(f,g)}function Ya(){var a=$a(),b=V;ca||(Lb(a)?b=Ca[a]:(n.forbidReserved&&(3===
-n.ecmaVersion?Mb:Nb)(a)||C&&bb(a))&&c(y,"The keyword '"+a+"' is reserved"));return e(b,a)}function t(){Da=y;M=X;Ea=ia;A()}function Fa(a){C=a;g=M;if(n.locations)for(;gb){var e=Q(a);e.left=a;e.operator=I;a=p;t();e.right=Ra(Sa(),f,c);f=q(e,a===Va||a===Wa?"LogicalExpression":"BinaryExpression");return Ra(f,b,c)}return a}function Sa(){if(p.prefix){var a=z(),b=p.isUpdate;a.operator=I;R=a.prefix=!0;t();a.argument=
-Sa();b?ra(a.argument):C&&"delete"===a.operator&&"Identifier"===a.argument.type&&c(a.start,"Deleting local variable in strict mode");return q(a,b?"UpdateExpression":"UnaryExpression")}for(b=ha(ua());p.postfix&&!qa();)a=Q(b),a.operator=I,a.prefix=!1,a.argument=b,ra(b),t(),b=q(a,"UpdateExpression");return b}function ha(a,b){if(v(xa)){var c=Q(a);c.object=a;c.property=O(!0);c.computed=!1;return ha(q(c,"MemberExpression"),b)}return v(ja)?(c=Q(a),c.object=a,c.property=B(),c.computed=!0,w(ka),ha(q(c,"MemberExpression"),
-b)):!b&&v(J)?(c=Q(a),c.callee=a,c.arguments=Ta(F,!1),ha(q(c,"CallExpression"),b)):a}function ua(){switch(p){case ub:var a=z();t();return q(a,"ThisExpression");case V:return O();case ba:case da:case Ba:return a=z(),a.value=I,a.raw=k.slice(y,X),t(),q(a,"Literal");case vb:case wb:case xb:return a=z(),a.value=p.atomValue,a.raw=p.keyword,t(),q(a,"Literal");case J:var a=oa,b=y;t();var f=B();f.start=b;f.end=X;n.locations&&(f.loc.start=a,f.loc.end=ia);n.ranges&&(f.range=[b,X]);w(F);return f;case ja:return a=
-z(),t(),a.elements=Ta(ka,!0,!0),q(a,"ArrayExpression");case Z:a=z();b=!0;f=!1;a.properties=[];for(t();!v(T);){if(b)b=!1;else if(w(L),n.allowTrailingCommas&&v(T))break;var e={key:p===ba||p===da?ua():O(!0)},d=!1,h;v(aa)?(e.value=B(!0),h=e.kind="init"):5<=n.ecmaVersion&&"Identifier"===e.key.type&&("get"===e.key.name||"set"===e.key.name)?(d=f=!0,h=e.kind=e.key.name,e.key=p===ba||p===da?ua():O(!0),p!==J&&N(),e.value=Na(z(),!1)):N();if("Identifier"===e.key.type&&(C||f))for(var g=0;gf?a.id:a.params[f],(bb(e.name)||sa(e.name))&&c(e.start,"Defining '"+e.name+"' in strict mode"),0<=f)for(var d=0;da?36===a:91>a?!0:97>a?95===a:123>a?!0:170<=a&&Za.test(String.fromCharCode(a))},ab=a.isIdentifierChar=function(a){return 48>a?36===a:58>a?!0:65>a?!1:91>a?!0:97>a?95===a:123>a?!0:170<=a&&Pb.test(String.fromCharCode(a))},ca,Ia={kind:"loop"},Ob={kind:"switch"}};
-"object"==typeof exports&&"object"==typeof module?mod$$inline_58(exports):"function"==typeof define&&define.amd?define(["exports"],mod$$inline_58):mod$$inline_58(this.acorn||(this.acorn={}));
-var Interpreter=function(a,b){this.initFunc_=b;this.UNDEFINED=this.createPrimitive(void 0);this.ast=acorn.parse(a);this.paused_=!1;var c=this.createScope(this.ast,null);this.stateStack=[{node:this.ast,scope:c,thisExpression:c}]};Interpreter.prototype.step=function(){if(!this.stateStack.length)return!1;if(this.paused_)return!0;var a=this.stateStack[0];this["step"+a.node.type]();return!0};Interpreter.prototype.run=function(){for(;!this.paused_&&this.step(););return this.paused_};
-Interpreter.prototype.initGlobalScope=function(a){this.setProperty(a,"Infinity",this.createPrimitive(Infinity),!0);this.setProperty(a,"NaN",this.createPrimitive(NaN),!0);this.setProperty(a,"undefined",this.UNDEFINED,!0);this.setProperty(a,"window",a,!0);this.setProperty(a,"self",a,!1);this.initFunction(a);this.initObject(a);a.parent=this.OBJECT;this.initArray(a);this.initNumber(a);this.initString(a);this.initBoolean(a);this.initDate(a);this.initMath(a);this.initRegExp(a);this.initJSON(a);var b=this,
-c;c=function(a){a=a||b.UNDEFINED;return b.createPrimitive(isNaN(a.toNumber()))};this.setProperty(a,"isNaN",this.createNativeFunction(c));c=function(a){a=a||b.UNDEFINED;return b.createPrimitive(isFinite(a.toNumber()))};this.setProperty(a,"isFinite",this.createNativeFunction(c));c=function(a){a=a||b.UNDEFINED;return b.createPrimitive(parseFloat(a.toNumber()))};this.setProperty(a,"parseFloat",this.createNativeFunction(c));c=function(a,c){a=a||b.UNDEFINED;c=c||b.UNDEFINED;return b.createPrimitive(parseInt(a.toString(),
-c.toNumber()))};this.setProperty(a,"parseInt",this.createNativeFunction(c));c=this.createObject(this.FUNCTION);c.eval=!0;this.setProperty(c,"length",this.createPrimitive(1),!0);this.setProperty(a,"eval",c);for(var d="escape unescape decodeURI decodeURIComponent encodeURI encodeURIComponent".split(" "),f=0;fa?Math.max(this.length+a,0):Math.min(a,this.length);e=c(e,Infinity);e=Math.min(e,this.length-a);for(var m=b.createObject(b.ARRAY),r=a;r=a;r--)this.properties[r+arguments.length-2]=this.properties[r];this.length+=arguments.length-2;for(r=2;rm&&(m=this.length+m);var m=
-Math.max(0,Math.min(m,this.length)),r=c(e,this.length);0>r&&(r=this.length+r);for(var r=Math.max(0,Math.min(r,this.length)),A=0;mh&&(h=this.length+h);for(h=Math.max(0,Math.min(h,
-this.length));hh&&(h=this.length+h);for(h=Math.max(0,Math.min(h,this.length));0<=h;h--){var m=b.getProperty(this,h);if(0==b.comp(m,a))return b.createPrimitive(h)}return b.createPrimitive(-1)};this.setProperty(this.ARRAY.properties.prototype,
-"lastIndexOf",this.createNativeFunction(d),!1,!0);d=function(){for(var a=[],c=0;cb?1:0};Interpreter.prototype.arrayIndex=function(a){a=Number(a);return!isFinite(a)||a!=Math.floor(a)||0>a?NaN:a};
-Interpreter.prototype.createPrimitive=function(a){if(void 0===a&&this.UNDEFINED)return this.UNDEFINED;if(a instanceof RegExp)return this.createRegExp(this.createObject(this.REGEXP),a);var b=typeof a;a={data:a,isPrimitive:!0,type:b,toBoolean:function(){return Boolean(this.data)},toNumber:function(){return Number(this.data)},toString:function(){return String(this.data)},valueOf:function(){return this.data}};"number"==b?a.parent=this.NUMBER:"string"==b?a.parent=this.STRING:"boolean"==b&&(a.parent=this.BOOLEAN);
-return a};
-Interpreter.prototype.createObject=function(a){a={isPrimitive:!1,type:"object",parent:a,fixed:Object.create(null),nonenumerable:Object.create(null),properties:Object.create(null),toBoolean:function(){return!0},toNumber:function(){return 0},toString:function(){return"["+this.type+"]"},valueOf:function(){return this}};this.isa(a,this.FUNCTION)&&(a.type="function",this.setProperty(a,"prototype",this.createObject(this.OBJECT||null)));this.isa(a,this.ARRAY)&&(a.length=0,a.toString=function(){for(var a=[],
-c=0;c>="==b.operator)b=f>>e;else if(">>>="==b.operator)b=f>>>e;else if("&="==b.operator)b=f&e;else if("^="==b.operator)b=f^e;else if("|="==b.operator)b=f|e;else throw"Unknown assignment expression: "+b.operator;b=this.createPrimitive(b)}this.setValue(c,b);this.stateStack[0].value=b}else a.doneRight=!0,a.leftSide=a.value,this.stateStack.unshift({node:b.right});else a.doneLeft=!0,this.stateStack.unshift({node:b.left,components:!0})};
-Interpreter.prototype.stepBinaryExpression=function(){var a=this.stateStack[0],b=a.node;if(a.doneLeft)if(a.doneRight){this.stateStack.shift();var c=a.leftValue,a=a.value,d=this.comp(c,a);if("=="==b.operator||"!="==b.operator)c=c.isPrimitive&&a.isPrimitive?c.data==a.data:0===d,"!="==b.operator&&(c=!c);else if("==="==b.operator||"!=="==b.operator)c=c.isPrimitive&&a.isPrimitive?c.data===a.data:c===a,"!=="==b.operator&&(c=!c);else if(">"==b.operator)c=1==d;else if(">="==b.operator)c=1==d||0===d;else if("<"==
-b.operator)c=-1==d;else if("<="==b.operator)c=-1==d||0===d;else if("+"==b.operator)"string"==c.type||"string"==a.type?(c=c.toString(),a=a.toString()):(c=c.toNumber(),a=a.toNumber()),c+=a;else if("in"==b.operator)c=this.hasProperty(a,c);else if(c=c.toNumber(),a=a.toNumber(),"-"==b.operator)c-=a;else if("*"==b.operator)c*=a;else if("/"==b.operator)c/=a;else if("%"==b.operator)c%=a;else if("&"==b.operator)c&=a;else if("|"==b.operator)c|=a;else if("^"==b.operator)c^=a;else if("<<"==b.operator)c<<=a;else if(">>"==
-b.operator)c>>=a;else if(">>>"==b.operator)c>>>=a;else throw"Unknown binary operator: "+b.operator;this.stateStack[0].value=this.createPrimitive(c)}else a.doneRight=!0,a.leftValue=a.value,this.stateStack.unshift({node:b.right});else a.doneLeft=!0,this.stateStack.unshift({node:b.left})};
-Interpreter.prototype.stepBreakStatement=function(){var a=this.stateStack.shift(),a=a.node,b=null;a.label&&(b=a.label.name);for(a=this.stateStack.shift();a&&"callExpression"!=a.node.type;){if(b?b==a.label:a.isLoop||a.isSwitch)return;a=this.stateStack.shift()}throw new SyntaxError("Illegal break statement");};Interpreter.prototype.stepBlockStatement=function(){var a=this.stateStack[0],b=a.node,c=a.n_||0;b.body[c]?(a.n_=c+1,this.stateStack.unshift({node:b.body[c]})):this.stateStack.shift()};
-Interpreter.prototype.stepCallExpression=function(){var a=this.stateStack[0],b=a.node;if(a.doneCallee_){if(a.func_)c=a.n_,a.arguments.length!=b.arguments.length&&(a.arguments[c-1]=a.value);else{if("function"==a.value.type)a.func_=a.value;else if(a.member_=a.value[0],a.func_=this.getValue(a.value),!a.func_||"function"!=a.func_.type)throw new TypeError((a.func_&&a.func_.type)+" is not a function");"NewExpression"==a.node.type?(a.funcThis_=this.createObject(a.func_),a.isConstructor_=!0):a.funcThis_=
-a.value.length?a.value[0]:this.stateStack[this.stateStack.length-1].thisExpression;a.arguments=[];var c=0}if(b.arguments[c])a.n_=c+1,this.stateStack.unshift({node:b.arguments[c]});else if(a.doneExec)this.stateStack.shift(),this.stateStack[0].value=a.isConstructor_?a.funcThis_:a.value;else{a.doneExec=!0;if(a.func_.node&&("FunctionApply_"==a.func_.node.type||"FunctionCall_"==a.func_.node.type)){a.funcThis_=a.arguments.shift();if("FunctionApply_"==a.func_.node.type){var d=a.arguments.shift();if(d&&this.isa(d,
-this.ARRAY))for(a.arguments=[],b=0;bb?a.arguments[b]:this.UNDEFINED;this.setProperty(c,d,f)}d=this.createObject(this.ARRAY);for(b=0;b
-
-
-
- Blockly Demo: JS Interpreter
-
-
-
-
-
-
-
-
-
-
- This is a simple demo of executing code with a sandboxed JavaScript interpreter.
-
- → More info on JS Interpreter ...
-
-
- Parse JavaScript
- Step JavaScript
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- n
-
-
- 1
-
-
-
-
-
-
- 4
-
-
-
-
- n
-
-
- MULTIPLY
-
-
- n
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
- n
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/maxBlocks/icon.png b/demos/maxBlocks/icon.png
deleted file mode 100644
index 13bf65a9c8..0000000000
Binary files a/demos/maxBlocks/icon.png and /dev/null differ
diff --git a/demos/maxBlocks/index.html b/demos/maxBlocks/index.html
deleted file mode 100644
index fd6b7eab07..0000000000
--- a/demos/maxBlocks/index.html
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
-
- Blockly Demo: Maximum Block Limit
-
-
-
-
-
-
- Blockly >
- Demos > Maximum Block Limit
-
- This is a demo of Blockly which has been restricted to a maximum of
- five blocks.
-
- You have block(s) left.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10
-
-
-
-
-
- i
-
-
- 1
-
-
-
-
- 10
-
-
-
-
- 1
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/demos/plane/README.txt b/demos/plane/README.txt
deleted file mode 100644
index 6da3fa65d6..0000000000
--- a/demos/plane/README.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-This Blockly demo uses Closure Templates to create a multilingual application.
-Any changes to the template.soy file require a recompile. Here is the command
-to generate a quick English version for debugging:
-
-java -jar soy/SoyToJsSrcCompiler.jar --outputPathFormat generated/en.js --srcs template.soy
-
-To generate a full set of language translations, first extract all the strings
-from template.soy using this command:
-
-java -jar soy/SoyMsgExtractor.jar --outputFile xlf/extracted_msgs.xlf template.soy
-
-This generates xlf/extracted_msgs.xlf, which may then be used by any
-XLIFF-compatible translation console to generate a set of files with the
-translated strings. These should be placed in the xlf directory.
-
-Finally, generate all the language versions wih this command:
-
-java -jar soy/SoyToJsSrcCompiler.jar --locales ar,be-tarask,br,ca,da,de,el,en,es,fa,fr,he,hrx,hu,ia,is,it,ja,ko,ms,nb,nl,pl,pms,pt-br,ro,ru,sc,sv,th,tr,uk,vi,zh-hans,zh-hant --messageFilePathFormat xlf/translated_msgs_{LOCALE}.xlf --outputPathFormat "generated/{LOCALE}.js" template.soy
-
-This is the process that Google uses for maintaining Blockly Games in 40+
-languages. The XLIFF fromat is simple enough that it is trival to write a
-Python script to reformat it into some other format (such as JSON) for
-compatability with other translation consoles.
-
-For more information, see message translation for Closure Templates:
-https://developers.google.com/closure/templates/docs/translation
diff --git a/demos/plane/blocks.js b/demos/plane/blocks.js
deleted file mode 100644
index 18be29c5dd..0000000000
--- a/demos/plane/blocks.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Blockly Demos: Plane Seat Calculator Blocks
- *
- * Copyright 2013 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 Blocks for Blockly's Plane Seat Calculator application.
- * @author fraser@google.com (Neil Fraser)
- */
-'use strict';
-
-Blockly.Blocks['plane_set_seats'] = {
- // Block seat variable setter.
- init: function() {
- this.setHelpUrl(Blockly.Msg.VARIABLES_SET_HELPURL);
- this.setColour(330);
- this.appendValueInput('VALUE')
- .appendField(Plane.getMsg('Plane_setSeats'));
- this.setTooltip(Blockly.Msg.VARIABLES_SET_TOOLTIP);
- this.setDeletable(false);
- }
-};
-
-Blockly.JavaScript['plane_set_seats'] = function(block) {
- // Generate JavaScript for seat variable setter.
- var argument0 = Blockly.JavaScript.valueToCode(block, 'VALUE',
- Blockly.JavaScript.ORDER_ASSIGNMENT) || 'NaN';
- return argument0 + ';';
-};
-
-Blockly.Blocks['plane_get_rows'] = {
- // Block for row variable getter.
- init: function() {
- this.setHelpUrl(Blockly.Msg.VARIABLES_GET_HELPURL);
- this.setColour(330);
- this.appendDummyInput()
- .appendField(Plane.getMsg('Plane_getRows'), 'title');
- this.setOutput(true, 'Number');
- },
- customUpdate: function() {
- this.setFieldValue(
- Plane.getMsg('Plane_getRows').replace('%1', Plane.rows1st), 'title');
- }
-};
-
-Blockly.JavaScript['plane_get_rows'] = function(block) {
- // Generate JavaScript for row variable getter.
- return ['Plane.rows1st', Blockly.JavaScript.ORDER_MEMBER];
-};
-
-Blockly.Blocks['plane_get_rows1st'] = {
- // Block for first class row variable getter.
- init: function() {
- this.setHelpUrl(Blockly.Msg.VARIABLES_GET_HELPURL);
- this.setColour(330);
- this.appendDummyInput()
- .appendField(Plane.getMsg('Plane_getRows1'), 'title');
- this.setOutput(true, 'Number');
- },
- customUpdate: function() {
- this.setFieldValue(
- Plane.getMsg('Plane_getRows1').replace('%1', Plane.rows1st), 'title');
- }
-};
-
-Blockly.JavaScript['plane_get_rows1st'] = function(block) {
- // Generate JavaScript for first class row variable getter.
- return ['Plane.rows1st', Blockly.JavaScript.ORDER_MEMBER];
-};
-
-Blockly.Blocks['plane_get_rows2nd'] = {
- // Block for second class row variable getter.
- init: function() {
- this.setHelpUrl(Blockly.Msg.VARIABLES_GET_HELPURL);
- this.setColour(330);
- this.appendDummyInput()
- .appendField(Plane.getMsg('Plane_getRows2'), 'title');
- this.setOutput(true, 'Number');
- },
- customUpdate: function() {
- this.setFieldValue(
- Plane.getMsg('Plane_getRows2').replace('%1', Plane.rows2nd), 'title');
- }
-};
-
-Blockly.JavaScript['plane_get_rows2nd'] = function(block) {
- // Generate JavaScript for second class row variable getter.
- return ['Plane.rows2nd', Blockly.JavaScript.ORDER_MEMBER];
-};
diff --git a/demos/plane/generated/ar.js b/demos/plane/generated/ar.js
deleted file mode 100644
index 1310949049..0000000000
--- a/demos/plane/generated/ar.js
+++ /dev/null
@@ -1,37 +0,0 @@
-// This file was automatically generated from template.soy.
-// Please don't edit this file by hand.
-
-if (typeof planepage == 'undefined') { var planepage = {}; }
-
-
-planepage.messages = function(opt_data, opt_ignored, opt_ijData) {
- return 'الصفوف: %1 الصفوف (%1) صفوف الطبقة الأولى: %1 صفوف الطبقة الأولى (%1) صفوف الفئة الثانية: %1 صفوف الفئة الثانية: (%1) المقاعد: %1 ؟ المقاعد =
';
-};
-
-
-planepage.start = function(opt_data, opt_ignored, opt_ijData) {
- var output = planepage.messages(null, null, opt_ijData) + '
-
-
-
-
-
-