diff --git a/core/flyout_base.js b/core/flyout_base.js
index 52c9367a1..2a999cf1f 100644
--- a/core/flyout_base.js
+++ b/core/flyout_base.js
@@ -34,13 +34,13 @@ const Tooltip = goog.require('Blockly.Tooltip');
const Variables = goog.require('Blockly.Variables');
const WorkspaceSvg = goog.require('Blockly.WorkspaceSvg');
const Xml = goog.require('Blockly.Xml');
+const blocks = goog.require('Blockly.serialization.blocks');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const idGenerator = goog.require('Blockly.utils.idGenerator');
const object = goog.require('Blockly.utils.object');
const toolbox = goog.require('Blockly.utils.toolbox');
const utils = goog.require('Blockly.utils');
-const utilsXml = goog.require('Blockly.utils.xml');
/** @suppress {extraRequire} */
goog.require('Blockly.blockRendering');
/** @suppress {extraRequire} */
@@ -155,6 +155,13 @@ const Flyout = function(workspaceOptions) {
* @package
*/
this.targetWorkspace = null;
+
+ /**
+ * A list of blocks that can be reused.
+ * @type {!Array}
+ * @private
+ */
+ this.recycledBlocks_ = [];
};
object.inherits(Flyout, DeleteArea);
@@ -560,6 +567,7 @@ Flyout.prototype.show = function(flyoutDef) {
this.reflowWrapper_ = this.reflow.bind(this);
this.workspace_.addChangeListener(this.reflowWrapper_);
+ this.emptyRecycledBlocks_();
};
/**
@@ -591,15 +599,10 @@ Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
switch (contentInfo['kind'].toUpperCase()) {
case 'BLOCK': {
- const blockInfo = /** @type {!toolbox.BlockInfo} */ (contentInfo);
- const blockXml = this.getBlockXml_(blockInfo);
- const block = this.createBlock_(blockXml);
- // This is a deprecated method for adding gap to a block.
- //
- const gap =
- parseInt(blockInfo['gap'] || blockXml.getAttribute('gap'), 10);
- gaps.push(isNaN(gap) ? defaultGap : gap);
+ var blockInfo = /** @type {!toolbox.BlockInfo} */ (contentInfo);
+ var block = this.createFlyoutBlock_(blockInfo);
contents.push({type: 'block', block: block});
+ this.addBlockGap_(blockInfo, gaps, defaultGap);
break;
}
case 'SEP': {
@@ -630,7 +633,8 @@ Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
/**
* Gets the flyout definition for the dynamic category.
* @param {string} categoryName The name of the dynamic category.
- * @return {!Array} The array of flyout items.
+ * @return {!Blockly.utils.toolbox.FlyoutDefinition} The definition of the
+ * flyout in one of its many forms.
* @private
*/
Flyout.prototype.getDynamicCategoryContents_ = function(categoryName) {
@@ -643,12 +647,7 @@ Flyout.prototype.getDynamicCategoryContents_ = function(categoryName) {
'Couldn\'t find a callback function when opening' +
' a toolbox category.');
}
- const flyoutDef = fnToApply(this.workspace_.targetWorkspace);
- if (!Array.isArray(flyoutDef)) {
- throw new TypeError(
- 'Result of toolbox category callback must be an array.');
- }
- return flyoutDef;
+ return fnToApply(this.workspace_.targetWorkspace);
};
/**
@@ -674,50 +673,77 @@ Flyout.prototype.createButton_ = function(btnInfo, isLabel) {
/**
* Create a block from the xml and permanently disable any blocks that were
* defined as disabled.
- * @param {!Element} blockXml The xml of the block.
+ * @param {!toolbox.BlockInfo} blockInfo The info of the block.
* @return {!BlockSvg} The block created from the blockXml.
- * @protected
+ * @private
*/
-Flyout.prototype.createBlock_ = function(blockXml) {
- const curBlock =
- /** @type {!BlockSvg} */ (Xml.domToBlock(blockXml, this.workspace_));
- if (!curBlock.isEnabled()) {
+Flyout.prototype.createFlyoutBlock_ = function(blockInfo) {
+ let block;
+ if (blockInfo['blockxml']) {
+ const xml = typeof blockInfo['blockxml'] === 'string' ?
+ Xml.textToDom(blockInfo['blockxml']) :
+ blockInfo['blockxml'];
+ block = this.getRecycledBlock_(xml.getAttribute('type'));
+ if (!block) {
+ block = Xml.domToBlock(xml, this.workspace_);
+ }
+ } else {
+ block = this.getRecycledBlock_(blockInfo['type']);
+ if (!block) {
+ if (blockInfo['enabled'] === undefined) {
+ blockInfo['enabled'] =
+ blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
+ }
+ block = blocks.load(
+ /** @type {blocks.State} */ (blockInfo),this.workspace_);
+ }
+ }
+
+ if (!block.isEnabled()) {
// Record blocks that were initially disabled.
// Do not enable these blocks as a result of capacity filtering.
- this.permanentlyDisabled_.push(curBlock);
+ this.permanentlyDisabled_.push(block);
}
- return curBlock;
+ return /** @type {!BlockSvg} */ (block);
};
/**
- * Get the xml from the block info object.
- * @param {!toolbox.BlockInfo} blockInfo The object holding
- * information about a block.
- * @return {!Element} The xml for the block.
- * @throws {Error} if the xml is not a valid block definition.
+ * Returns a block from the array of recycled blocks with the given type, or
+ * undefined if one cannot be found.
+ * @param {string} blockType The type of the block to try to recycle.
+ * @return {(!BlockSvg|undefined)} The recycled block, or undefined if
+ * one could not be recycled.
* @private
*/
-Flyout.prototype.getBlockXml_ = function(blockInfo) {
- let blockElement = null;
- const blockXml = blockInfo['blockxml'];
-
- if (blockXml && typeof blockXml != 'string') {
- blockElement = blockXml;
- } else if (blockXml && typeof blockXml == 'string') {
- blockElement = Xml.textToDom(blockXml);
- blockInfo['blockxml'] = blockElement;
- } else if (blockInfo['type']) {
- blockElement = utilsXml.createElement('xml');
- blockElement.setAttribute('type', blockInfo['type']);
- blockElement.setAttribute('disabled', blockInfo['disabled']);
- blockInfo['blockxml'] = blockElement;
+Flyout.prototype.getRecycledBlock_ = function(blockType) {
+ let index = -1;
+ for (let i = 0; i < this.recycledBlocks_.length; i++) {
+ if (this.recycledBlocks_[i].type == blockType) {
+ index = i;
+ break;
+ }
}
+ return index == -1 ? undefined : this.recycledBlocks_.splice(index, 1)[0];
+};
- if (!blockElement) {
- throw Error(
- 'Error: Invalid block definition. Block definition must have blockxml or type.');
+/**
+ * Adds a gap in the flyout based on block info.
+ * @param {!toolbox.BlockInfo} blockInfo Information about a block.
+ * @param {!Array} gaps The list of gaps between items in the flyout.
+ * @param {number} defaultGap The default gap between one element and the next.
+ * @private
+ */
+Flyout.prototype.addBlockGap_ = function(blockInfo, gaps, defaultGap) {
+ let gap;
+ if (blockInfo['gap']) {
+ gap = parseInt(blockInfo['gap'], 10);
+ } else if (blockInfo['blockxml']) {
+ const xml = typeof blockInfo['blockxml'] === 'string' ?
+ Xml.textToDom(blockInfo['blockxml']) :
+ blockInfo['blockxml'];
+ gap = parseInt(xml.getAttribute('gap'), 10);
}
- return blockElement;
+ gaps.push(isNaN(gap) ? defaultGap : gap);
};
/**
@@ -745,13 +771,15 @@ Flyout.prototype.addSeparatorGap_ = function(sepInfo, gaps, defaultGap) {
/**
* Delete blocks, mats and buttons from a previous showing of the flyout.
- * @protected
+ * @private
*/
Flyout.prototype.clearOldBlocks_ = function() {
// Delete any blocks from a previous showing.
const oldBlocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = oldBlocks[i]); i++) {
- if (block.workspace == this.workspace_) {
+ if (this.blockIsRecyclable_(block)) {
+ this.recycleBlock_(block);
+ } else {
block.dispose(false, false);
}
}
@@ -774,6 +802,41 @@ Flyout.prototype.clearOldBlocks_ = function() {
this.workspace_.getPotentialVariableMap().clear();
};
+/**
+ * Empties all of the recycled blocks, properly disposing of them.
+ * @private
+ */
+Flyout.prototype.emptyRecycledBlocks_ = function() {
+ for (let i = 0; i < this.recycledBlocks_.length; i++) {
+ this.recycledBlocks_[i].dispose();
+ }
+ this.recycledBlocks_ = [];
+};
+
+/**
+ * Returns whether the given block can be recycled or not.
+ * @param {!BlockSvg} _block The block to check for recyclability.
+ * @return {boolean} True if the block can be recycled. False otherwise.
+ * @protected
+ */
+Flyout.prototype.blockIsRecyclable_ = function(_block) {
+ // By default, recycling is disabled.
+ return false;
+};
+
+/**
+ * Puts a previously created block into the recycle bin and moves it to the
+ * top of the workspace. Used during large workspace swaps to limit the number
+ * of new DOM elements we need to create.
+ * @param {!BlockSvg} block The block to recycle.
+ * @private
+ */
+Flyout.prototype.recycleBlock_ = function(block) {
+ const xy = block.getRelativeToSurfaceXY();
+ block.moveBy(-xy.x, -xy.y);
+ this.recycledBlocks_.push(block);
+};
+
/**
* Add listeners to a block that has been added to the flyout.
* @param {!SVGElement} root The root node of the SVG group the block is in.
@@ -1010,20 +1073,21 @@ Flyout.prototype.placeNewBlock_ = function(oldBlock) {
throw Error('oldBlock is not rendered.');
}
- // Create the new block by cloning the block in the flyout (via XML).
- // This cast assumes that the oldBlock can not be an insertion marker.
- const xml = /** @type {!Element} */ (Xml.blockToDom(oldBlock, true));
- // The target workspace would normally resize during domToBlock, which will
- // lead to weird jumps. Save it for terminateDrag.
- targetWorkspace.setResizesEnabled(false);
-
- // Using domToBlock instead of domToWorkspace means that the new block will be
- // placed at position (0, 0) in main workspace units.
- const block = /** @type {!BlockSvg} */
- (Xml.domToBlock(xml, targetWorkspace));
- const svgRootNew = block.getSvgRoot();
- if (!svgRootNew) {
- throw Error('block is not rendered.');
+ let block;
+ if (oldBlock.mutationToDom && !oldBlock.saveExtraState) {
+ // Create the new block by cloning the block in the flyout (via XML).
+ // This cast assumes that the oldBlock can not be an insertion marker.
+ const xml = /** @type {!Element} */ (Xml.blockToDom(oldBlock, true));
+ // The target workspace would normally resize during domToBlock, which will
+ // lead to weird jumps. Save it for terminateDrag.
+ targetWorkspace.setResizesEnabled(false);
+ // Using domToBlock instead of domToWorkspace means that the new block will be
+ // placed at position (0, 0) in main workspace units.
+ block = /** @type {!BlockSvg} */ (Xml.domToBlock(xml, targetWorkspace));
+ } else {
+ const json = /** @type {!blocks.State} */ (blocks.save(oldBlock));
+ targetWorkspace.setResizesEnabled(false);
+ block = /** @type {!BlockSvg} */ (blocks.load(json, targetWorkspace));
}
// The offset in pixels between the main workspace's origin and the upper left
diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js
index 7955a1c01..903a996e8 100644
--- a/core/serialization/blocks.js
+++ b/core/serialization/blocks.js
@@ -46,14 +46,17 @@ exports.ConnectionState = ConnectionState;
* Represents the state of a given block.
* @typedef {{
* type: string,
- * id: string,
+ * id: (string|undefined),
* x: (number|undefined),
* y: (number|undefined),
* collapsed: (boolean|undefined),
- * disabled: (boolean|undefined),
+ * enabled: (boolean|undefined),
+ * editable: (boolean|undefined),
+ * deletable: (boolean|undefined),
+ * movable: (boolean|undefined),
* inline: (boolean|undefined),
* data: (string|undefined),
- * extra-state: *,
+ * extra-state: (*|undefined),
* icons: (!Object|undefined),
* fields: (!Object|undefined),
* inputs: (!Object|undefined),
diff --git a/core/utils/toolbox.js b/core/utils/toolbox.js
index 34208bf13..157addeb0 100644
--- a/core/utils/toolbox.js
+++ b/core/utils/toolbox.js
@@ -16,6 +16,8 @@
*/
goog.module('Blockly.utils.toolbox');
+/* eslint-disable-next-line no-unused-vars */
+const {ConnectionState} = goog.requireType('Blockly.serialization.blocks');
/* eslint-disable-next-line no-unused-vars */
const ToolboxCategory = goog.requireType('Blockly.ToolboxCategory');
/* eslint-disable-next-line no-unused-vars */
@@ -25,12 +27,28 @@ const userAgent = goog.require('Blockly.utils.userAgent');
/**
* The information needed to create a block in the toolbox.
+ * Note that disabled has a different type for backwards compatibility.
* @typedef {{
* kind:string,
* blockxml:(string|!Node|undefined),
* type:(string|undefined),
* gap:(string|number|undefined),
- * disabled: (string|boolean|undefined)
+ * disabled: (string|boolean|undefined),
+ * enabled: (boolean|undefined),
+ * id: (string|undefined),
+ * x: (number|undefined),
+ * y: (number|undefined),
+ * collapsed: (boolean|undefined),
+ * editable: (boolean|undefined),
+ * deletable: (boolean|undefined),
+ * movable: (boolean|undefined),
+ * inline: (boolean|undefined),
+ * data: (string|undefined),
+ * extra-state: (*|undefined),
+ * icons: (!Object|undefined),
+ * fields: (!Object|undefined),
+ * inputs: (!Object|undefined),
+ * next: (!ConnectionState|undefined)
* }}
*/
let BlockInfo;
diff --git a/core/workspace_svg.js b/core/workspace_svg.js
index 2da56275f..a6c81e741 100644
--- a/core/workspace_svg.js
+++ b/core/workspace_svg.js
@@ -200,11 +200,12 @@ const WorkspaceSvg = function(
this.markerManager_ = new MarkerManager(this);
/**
- * Map from function names to callbacks, for deciding what to do when a custom
- * toolbox category is opened.
- * @type {!Object>}
- * @private
- */
+ * Map from function names to callbacks, for deciding what to do when a custom
+ * toolbox category is opened.
+ * @type {!Object}
+ * @private
+ */
this.toolboxCategoryCallbacks_ = Object.create(null);
/**
@@ -2547,7 +2548,7 @@ WorkspaceSvg.prototype.removeButtonCallback = function(key) {
* custom toolbox categories in this workspace. See the variable and procedure
* categories as an example.
* @param {string} key The name to use to look up this function.
- * @param {function(!Workspace):!Array} func The function to
+ * @param {function(!Workspace): !toolbox.FlyoutDefinition} func The function to
* call when the given toolbox category is opened.
*/
WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key, func) {
@@ -2561,7 +2562,7 @@ WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key, func) {
* Get the callback function associated with a given key, for populating
* custom toolbox categories in this workspace.
* @param {string} key The name to use to look up the function.
- * @return {?function(!Workspace):!Array} The function
+ * @return {?function(!Workspace): !toolbox.FlyoutDefinition} The function
* corresponding to the given key for this workspace, or null if no function
* is registered.
*/
diff --git a/tests/deps.js b/tests/deps.js
index 9e2eab19c..b7ff00900 100644
--- a/tests/deps.js
+++ b/tests/deps.js
@@ -78,7 +78,7 @@ goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Bloc
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.dialog', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
-goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
+goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.serialization.blocks', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_button.js', ['Blockly.FlyoutButton'], ['Blockly.Css', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.style'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.registry', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_metrics_manager.js', ['Blockly.FlyoutMetricsManager'], ['Blockly.MetricsManager', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
diff --git a/tests/deps.mocha.js b/tests/deps.mocha.js
index 450d1e4f3..17485d31f 100644
--- a/tests/deps.mocha.js
+++ b/tests/deps.mocha.js
@@ -78,7 +78,7 @@ goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Bloc
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.dialog', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.internalConstants', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
-goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'});
+goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.Variables', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.serialization.blocks', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_button.js', ['Blockly.FlyoutButton'], ['Blockly.Css', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.style'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.registry', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/flyout_metrics_manager.js', ['Blockly.FlyoutMetricsManager'], ['Blockly.MetricsManager', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
@@ -353,7 +353,7 @@ goog.addDependency('../../tests/mocha/serializer_test.js', ['Blockly.test.serial
goog.addDependency('../../tests/mocha/shortcut_registry_test.js', ['Blockly.test.shortcutRegistry'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/test_helpers.js', ['Blockly.test.helpers'], ['Blockly.utils.KeyCodes'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/theme_test.js', ['Blockly.test.theme'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
-goog.addDependency('../../tests/mocha/toolbox_helper.js', ['Blockly.test.toolboxHelpers'], [], {'lang': 'es5', 'module': 'goog'});
+goog.addDependency('../../tests/mocha/toolbox_helper.js', ['Blockly.test.toolboxHelpers'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/toolbox_test.js', ['Blockly.test.toolbox'], ['Blockly.test.helpers', 'Blockly.test.toolboxHelpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/tooltip_test.js', ['Blockly.test.tooltip'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/trashcan_test.js', ['Blockly.test.trashcan'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js
index e860727d8..5faf8bf84 100644
--- a/tests/mocha/flyout_test.js
+++ b/tests/mocha/flyout_test.js
@@ -7,7 +7,7 @@
goog.module('Blockly.test.flyout');
const {defineStackBlock, sharedTestSetup, sharedTestTeardown, workspaceTeardown} = goog.require('Blockly.test.helpers');
-const {getBasicToolbox, getChildItem, getCollapsibleItem, getDeeplyNestedJSON, getInjectedToolbox, getNonCollapsibleItem, getSeparator, getSimpleJSON, getXmlArray} = goog.require('Blockly.test.toolboxHelpers');
+const {getBasicToolbox, getChildItem, getCollapsibleItem, getDeeplyNestedJSON, getInjectedToolbox, getNonCollapsibleItem, getProperSimpleJson, getSeparator, getSimpleJson, getXmlArray} = goog.require('Blockly.test.toolboxHelpers');
suite('Flyout', function() {
@@ -242,74 +242,345 @@ suite('Flyout', function() {
suite('createFlyoutInfo_', function() {
setup(function() {
- this.simpleToolboxJSON = getSimpleJSON();
this.flyout = this.workspace.getFlyout();
this.createFlyoutSpy = sinon.spy(this.flyout, 'createFlyoutInfo_');
-
});
- function checkLayoutContents(actual, expected, opt_message) {
- chai.assert.equal(actual.length, expected.length, opt_message);
- for (var i = 0; i < actual.length; i++) {
- chai.assert.equal(actual[i].type, expected[i].type, opt_message);
- if (actual[i].type == 'BLOCK') {
- chai.assert.typeOf(actual[i]['block'], 'Blockly.Block');
- } else if (actual[i].type == 'BUTTON' || actual[i].type == 'LABEL') {
- chai.assert.typeOf(actual[i]['block'], 'Blockly.FlyoutButton');
- }
- }
- }
-
function checkFlyoutInfo(flyoutSpy) {
- var expectedContents = [
- {type: "block"},
- {type: "button"},
- {type: "button"}
- ];
- var expectedGaps = [20, 24, 24];
var flyoutInfo = flyoutSpy.returnValues[0];
var contents = flyoutInfo.contents;
var gaps = flyoutInfo.gaps;
+
+ var expectedGaps = [20, 24, 24];
chai.assert.deepEqual(gaps, expectedGaps);
- checkLayoutContents(contents, expectedContents, 'Contents');
+
+ chai.assert.equal(contents.length, 3, 'Contents');
+
+ chai.assert.equal(contents[0].type, 'block', 'Contents');
+ var block = contents[0]['block'];
+ chai.assert.instanceOf(block, Blockly.BlockSvg);
+ chai.assert.equal(block.getFieldValue('OP'), 'NEQ');
+ var childA = block.getInputTargetBlock('A');
+ var childB = block.getInputTargetBlock('B');
+ chai.assert.isTrue(childA.isShadow());
+ chai.assert.isFalse(childB.isShadow());
+ chai.assert.equal(childA.getFieldValue('NUM'), 1);
+ chai.assert.equal(childB.getFieldValue('NUM'), 2);
+
+ chai.assert.equal(contents[1].type, 'button', 'Contents');
+ chai.assert.instanceOf(contents[1]['button'], Blockly.FlyoutButton);
+
+ chai.assert.equal(contents[2].type, 'button', 'Contents');
+ chai.assert.instanceOf(contents[2]['button'], Blockly.FlyoutButton);
}
- test('Node', function() {
- this.flyout.show(this.toolboxXml);
- checkFlyoutInfo(this.createFlyoutSpy);
+ suite('Direct show', function() {
+ test('Node', function() {
+ this.flyout.show(this.toolboxXml);
+ checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ test('NodeList', function() {
+ var nodeList = document.getElementById('toolbox-simple').childNodes;
+ this.flyout.show(nodeList);
+ checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ test('Array of JSON', function() {
+ this.flyout.show(getSimpleJson());
+ checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ test('Array of Proper JSON', function() {
+ this.flyout.show(getProperSimpleJson());
+ checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ test('Array of XML', function() {
+ this.flyout.show(getXmlArray());
+ checkFlyoutInfo(this.createFlyoutSpy);
+ });
});
- test('NodeList', function() {
- var nodeList = document.getElementById('toolbox-simple').childNodes;
- this.flyout.show(nodeList);
- checkFlyoutInfo(this.createFlyoutSpy);
+
+ suite('Dynamic category', function() {
+ setup(function() {
+ this.stubAndAssert = function(val) {
+ sinon.stub(
+ this.flyout.workspace_.targetWorkspace,
+ 'getToolboxCategoryCallback')
+ .returns(function() { return val; });
+ this.flyout.show('someString');
+ checkFlyoutInfo(this.createFlyoutSpy);
+ };
+ });
+
+ test('No category available', function() {
+ chai.assert.throws(
+ function() {
+ this.flyout.show('someString');
+ }.bind(this),
+ 'Couldn\'t find a callback function when opening ' +
+ 'a toolbox category.');
+ });
+
+ test('Node', function() {
+ this.stubAndAssert(this.toolboxXml);
+ });
+
+ test('NodeList', function() {
+ this.stubAndAssert(
+ document.getElementById('toolbox-simple').childNodes);
+ });
+
+ test('Array of JSON', function() {
+ this.stubAndAssert(getSimpleJson());
+ });
+
+ test('Array of Proper JSON', function() {
+ this.stubAndAssert(getProperSimpleJson());
+ });
+
+ test('Array of XML', function() {
+ this.stubAndAssert(getXmlArray());
+ });
});
- test('Array of JSON', function() {
- this.flyout.show(this.simpleToolboxJSON);
- checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ suite('Creating blocks', function() {
+ suite('Enabled/Disabled', function() {
+ setup(function() {
+ this.flyout = this.workspace.getFlyout();
+
+ this.assertDisabled = function(disabled) {
+ var block = this.flyout.getWorkspace().getTopBlocks(false)[0];
+ chai.assert.equal(!block.isEnabled(), disabled);
+ };
+ });
+
+ suite('XML', function() {
+ test('True string', function() {
+ var xml = Blockly.Xml.textToDom(
+ '' +
+ '' +
+ ''
+ );
+ this.flyout.show(xml);
+ this.assertDisabled(true);
+ });
+
+ test('False string', function() {
+ var xml = Blockly.Xml.textToDom(
+ '' +
+ '' +
+ ''
+ );
+ this.flyout.show(xml);
+ this.assertDisabled(false);
+ });
+
+ test('Disabled string', function() {
+ // The XML system supports this for some reason!?
+ var xml = Blockly.Xml.textToDom(
+ '' +
+ '' +
+ ''
+ );
+ this.flyout.show(xml);
+ this.assertDisabled(true);
+ });
+
+ test('Different string', function() {
+ var xml = Blockly.Xml.textToDom(
+ '' +
+ '' +
+ ''
+ );
+ this.flyout.show(xml);
+ this.assertDisabled(false);
+ });
+ });
+
+ suite('JSON', function() {
+ test('All undefined', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Enabled true', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'enabled': true,
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Enabled false', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'enabled': false,
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(true);
+ });
+
+ test('Disabled true string', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': 'true'
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(true);
+ });
+
+ test('Disabled false string', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': 'false'
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Disabled string', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': 'disabled' // This is not respected by the JSON!
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Disabled true value', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': true
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(true);
+ });
+
+ test('Disabled false value', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': false
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Disabled different string', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': 'random'
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+
+ test('Disabled empty string', function() {
+ var json = [
+ {
+ 'kind': 'block',
+ 'type': 'text_print',
+ 'disabled': ''
+ }
+ ];
+ this.flyout.show(json);
+ this.assertDisabled(false);
+ });
+ });
});
- test('Array of xml', function() {
- this.flyout.show(getXmlArray());
- checkFlyoutInfo(this.createFlyoutSpy);
+ });
+
+ suite('Recycling', function() {
+ setup(function() {
+ this.flyout = this.workspace.getFlyout();
});
- test('Custom Toolbox: No Category Available', function() {
- chai.assert.throws(function() {
- this.flyout.show('someString');
- }.bind(this), 'Couldn\'t find a callback function when opening' +
- ' a toolbox category.');
+
+ test('Recycling disabled', function() {
+ this.flyout.show({
+ 'contents': [
+ {
+ 'kind': 'BLOCK',
+ 'type': 'math_number',
+ 'fields': {
+ 'NUM': 123
+ }
+ }
+ ]
+ });
+ this.flyout.show({
+ 'contents': [
+ {
+ 'kind': 'BLOCK',
+ 'type': 'math_number',
+ 'fields': {
+ 'NUM': 321
+ }
+ }
+ ]
+ });
+ const block = this.flyout.workspace_.getAllBlocks()[0];
+ chai.assert.equal(block.getFieldValue('NUM'), 321);
});
- test('Custom Toolbox: Function does not return array', function() {
- sinon.stub(this.flyout.workspace_.targetWorkspace,
- 'getToolboxCategoryCallback').returns(function(){return null;});
- chai.assert.throws(function() {
- this.flyout.show('someString');
- }.bind(this), 'Result of toolbox category callback must be an array.');
- });
- test('Custom Toolbox: Returns Array', function() {
- sinon.stub(this.flyout.workspace_.targetWorkspace,
- 'getToolboxCategoryCallback').returns(function(){return getXmlArray();});
- chai.assert.doesNotThrow(function() {
- this.flyout.show('someString');
- }.bind(this));
+
+ test('Recycling enabled', function() {
+ this.flyout.blockIsRecyclable_ = function() { return true; };
+ this.flyout.show({
+ 'contents': [
+ {
+ 'kind': 'BLOCK',
+ 'type': 'math_number',
+ 'fields': {
+ 'NUM': 123
+ }
+ }
+ ]
+ });
+ this.flyout.show({
+ 'contents': [
+ {
+ 'kind': 'BLOCK',
+ 'type': 'math_number',
+ 'fields': {
+ 'NUM': 321
+ }
+ }
+ ]
+ });
+ const block = this.flyout.workspace_.getAllBlocks()[0];
+ chai.assert.equal(block.getFieldValue('NUM'), 123);
});
});
});
diff --git a/tests/mocha/index.html b/tests/mocha/index.html
index 2bdddc2b5..ec5f778fd 100644
--- a/tests/mocha/index.html
+++ b/tests/mocha/index.html
@@ -113,12 +113,26 @@
+
-
+
+ NEQ
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
@@ -134,6 +148,7 @@
+
diff --git a/tests/mocha/toolbox_helper.js b/tests/mocha/toolbox_helper.js
index 1d1fc8d3d..cd3efb7a8 100644
--- a/tests/mocha/toolbox_helper.js
+++ b/tests/mocha/toolbox_helper.js
@@ -49,12 +49,24 @@ exports.getCategoryJSON = getCategoryJSON;
* @return {Blockly.utils.toolbox.ToolboxJson} The array holding information
* for a simple toolbox.
*/
-function getSimpleJSON() {
+function getSimpleJson() {
return {"contents":[
{
"kind":"BLOCK",
- "blockxml": "",
- "type":"logic_operation"
+ "blockxml":
+ `
+ NEQ
+
+
+ 1
+
+
+
+
+ 2
+
+
+ `,
},
{
"kind":"SEP",
@@ -71,7 +83,52 @@ function getSimpleJSON() {
}
]};
}
-exports.getSimpleJSON = getSimpleJSON;
+exports.getSimpleJson = getSimpleJson;
+
+function getProperSimpleJson() {
+ return {
+ "contents": [
+ {
+ "kind":"BLOCK",
+ "type": "logic_compare",
+ "fields": {
+ "OP": "NEQ",
+ },
+ "inputs": {
+ "A": {
+ "shadow": {
+ "type": "math_number",
+ "fields": {
+ "NUM": 1,
+ }
+ }
+ },
+ "B": {
+ "block": {
+ "type": "math_number",
+ "fields": {
+ "NUM": 2,
+ }
+ }
+ }
+ }
+ },
+ {
+ "kind":"SEP",
+ "gap":"20"
+ },
+ {
+ "kind":"BUTTON",
+ "text": "insert",
+ "callbackkey": "insertConnectionRows"
+ },
+ {
+ "kind":"LABEL",
+ "text":"tooltips"
+ }
+ ]};
+}
+exports.getProperSimpleJson = getProperSimpleJson;
/**
* Get JSON for a toolbox that contains categories that contain categories.
@@ -123,10 +180,20 @@ exports.getDeeplyNestedJSON = getDeeplyNestedJSON;
* @return {Array} Array holding xml elements for a toolbox.
*/
function getXmlArray() {
- // Need to use HTMLElement instead of Element so parser output is
- // consistent with other tests
- var block = document.createElement('block');
- block.setAttribute('type', 'logic_operation');
+ var block = Blockly.Xml.textToDom(
+ `
+ NEQ
+
+
+ 1
+
+
+
+
+ 2
+
+
+ `);
var separator = Blockly.Xml.textToDom('');
var button = Blockly.Xml.textToDom('');
var label = Blockly.Xml.textToDom('');
diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js
index c2cc2cece..35b25b293 100644
--- a/tests/mocha/toolbox_test.js
+++ b/tests/mocha/toolbox_test.js
@@ -7,7 +7,7 @@
goog.module('Blockly.test.toolbox');
const {defineStackBlock, sharedTestSetup, sharedTestTeardown} = goog.require('Blockly.test.helpers');
-const {getBasicToolbox, getCategoryJSON, getChildItem, getCollapsibleItem, getDeeplyNestedJSON, getInjectedToolbox, getNonCollapsibleItem, getSeparator, getSimpleJSON, getXmlArray} = goog.require('Blockly.test.toolboxHelpers');
+const {getBasicToolbox, getCategoryJSON, getChildItem, getCollapsibleItem, getDeeplyNestedJSON, getInjectedToolbox, getNonCollapsibleItem, getSeparator, getSimpleJson, getXmlArray} = goog.require('Blockly.test.toolboxHelpers');
suite('Toolbox', function() {
@@ -494,7 +494,7 @@ suite('Toolbox', function() {
suite('parseMethods', function() {
setup(function() {
this.categoryToolboxJSON = getCategoryJSON();
- this.simpleToolboxJSON = getSimpleJSON();
+ this.simpleToolboxJSON = getSimpleJson();
});
function checkValue(actual, expected, value) {
diff --git a/tests/playground.html b/tests/playground.html
index c94c05c2f..c797162dd 100644
--- a/tests/playground.html
+++ b/tests/playground.html
@@ -196,6 +196,19 @@ function setToolboxDropdown() {
}
function initToolbox(workspace) {
+ workspace.registerToolboxCategoryCallback('JSON', function() {
+ return [
+ {
+ 'kind': 'block',
+ 'type': 'lists_create_with_json'
+ },
+ {
+ 'kind': 'block',
+ 'type': 'lists_create_with_json',
+ 'extraState': {'itemCount': 2}
+ }
+ ]
+ });
var toolboxSuffix = getToolboxSuffix();
if (toolboxSuffix == 'test-blocks' &&
typeof window.toolboxTestBlocksInit !== 'undefined') {
@@ -447,6 +460,130 @@ var spaghettiXml = [
'next': { }
});
+
+Blockly.Blocks['lists_create_with_json'] = {
+ /**
+ * Block for creating a list with any number of elements of any type.
+ * @this {Blockly.Block}
+ */
+ init: function() {
+ this.setHelpUrl(Blockly.Msg['LISTS_CREATE_WITH_HELPURL']);
+ this.setStyle('list_blocks');
+ this.itemCount_ = 3;
+ this.updateShape_();
+ this.setOutput(true, 'Array');
+ this.setMutator(new Blockly.Mutator(['lists_create_with_item']));
+ this.setTooltip(Blockly.Msg['LISTS_CREATE_WITH_TOOLTIP']);
+ },
+ /**
+ * Returns the state of this block as a JSON serializable object.
+ * @return {{itemCount: number}} The state of this block, ie the item count.
+ */
+ saveExtraState: function() {
+ return {
+ 'itemCount': this.itemCount_,
+ };
+ },
+ /**
+ * Applies the given state to this block.
+ * @param {*} state The state to apply to this block, ie the item count.
+ */
+ loadExtraState: function(state) {
+ this.itemCount_ = state['itemCount'];
+ this.updateShape_();
+ },
+ /**
+ * Populate the mutator's dialog with this block's components.
+ * @param {!Blockly.Workspace} workspace Mutator's workspace.
+ * @return {!Blockly.Block} Root block in mutator.
+ * @this {Blockly.Block}
+ */
+ decompose: function(workspace) {
+ var containerBlock = workspace.newBlock('lists_create_with_container');
+ containerBlock.initSvg();
+ var connection = containerBlock.getInput('STACK').connection;
+ for (var i = 0; i < this.itemCount_; i++) {
+ var itemBlock = workspace.newBlock('lists_create_with_item');
+ itemBlock.initSvg();
+ connection.connect(itemBlock.previousConnection);
+ connection = itemBlock.nextConnection;
+ }
+ return containerBlock;
+ },
+ /**
+ * Reconfigure this block based on the mutator dialog's components.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this {Blockly.Block}
+ */
+ compose: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ // Count number of inputs.
+ var connections = [];
+ while (itemBlock && !itemBlock.isInsertionMarker()) {
+ connections.push(itemBlock.valueConnection_);
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ // Disconnect any children that don't belong.
+ for (var i = 0; i < this.itemCount_; i++) {
+ var connection = this.getInput('ADD' + i).connection.targetConnection;
+ if (connection && connections.indexOf(connection) == -1) {
+ connection.disconnect();
+ }
+ }
+ this.itemCount_ = connections.length;
+ this.updateShape_();
+ // Reconnect any child blocks.
+ for (var i = 0; i < this.itemCount_; i++) {
+ Blockly.Mutator.reconnect(connections[i], this, 'ADD' + i);
+ }
+ },
+ /**
+ * Store pointers to any connected child blocks.
+ * @param {!Blockly.Block} containerBlock Root block in mutator.
+ * @this {Blockly.Block}
+ */
+ saveConnections: function(containerBlock) {
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
+ var i = 0;
+ while (itemBlock) {
+ var input = this.getInput('ADD' + i);
+ itemBlock.valueConnection_ = input && input.connection.targetConnection;
+ i++;
+ itemBlock = itemBlock.nextConnection &&
+ itemBlock.nextConnection.targetBlock();
+ }
+ },
+ /**
+ * Modify this block to have the correct number of inputs.
+ * @private
+ * @this {Blockly.Block}
+ */
+ updateShape_: function() {
+ if (this.itemCount_ && this.getInput('EMPTY')) {
+ this.removeInput('EMPTY');
+ } else if (!this.itemCount_ && !this.getInput('EMPTY')) {
+ this.appendDummyInput('EMPTY')
+ .appendField(Blockly.Msg['LISTS_CREATE_EMPTY_TITLE']);
+ }
+ // Add new inputs.
+ for (var i = 0; i < this.itemCount_; i++) {
+ if (!this.getInput('ADD' + i)) {
+ var input = this.appendValueInput('ADD' + i)
+ .setAlign(Blockly.ALIGN_RIGHT);
+ if (i == 0) {
+ input.appendField(Blockly.Msg['LISTS_CREATE_WITH_INPUT_WITH']);
+ }
+ }
+ }
+ // Remove deleted inputs.
+ while (this.getInput('ADD' + i)) {
+ this.removeInput('ADD' + i);
+ i++;
+ }
+ }
+};
+