diff --git a/core/flyout_base.js b/core/flyout_base.js
index 84d6d3870f6..e9974f3b576 100644
--- a/core/flyout_base.js
+++ b/core/flyout_base.js
@@ -36,7 +36,6 @@ goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.Svg');
goog.require('Blockly.utils.toolbox');
-goog.require('Blockly.utils.xml');
goog.require('Blockly.WorkspaceSvg');
goog.require('Blockly.Xml');
@@ -148,6 +147,13 @@ Blockly.Flyout = function(workspaceOptions) {
* @package
*/
this.targetWorkspace = null;
+
+ /**
+ * A list of blocks that can be reused.
+ * @type {!Array}
+ * @private
+ */
+ this.recycledBlocks_ = [];
};
Blockly.utils.object.inherits(Blockly.Flyout, Blockly.DeleteArea);
@@ -555,6 +561,7 @@ Blockly.Flyout.prototype.show = function(flyoutDef) {
this.reflowWrapper_ = this.reflow.bind(this);
this.workspace_.addChangeListener(this.reflowWrapper_);
+ this.emptyRecycledBlocks_();
};
/**
@@ -586,13 +593,9 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
switch (contentInfo['kind'].toUpperCase()) {
case 'BLOCK':
var blockInfo = /** @type {!Blockly.utils.toolbox.BlockInfo} */ (contentInfo);
- var blockXml = this.getBlockXml_(blockInfo);
- var block = this.createBlock_(blockXml);
- // This is a deprecated method for adding gap to a block.
- //
- var gap = parseInt(blockInfo['gap'] || blockXml.getAttribute('gap'), 10);
- gaps.push(isNaN(gap) ? defaultGap : gap);
+ var block = this.createFlyoutBlock_(blockInfo);
contents.push({type: 'block', block: block});
+ this.addBlockGap_(blockInfo, gaps, defaultGap);
break;
case 'SEP':
var sepInfo = /** @type {!Blockly.utils.toolbox.SeparatorInfo} */ (contentInfo);
@@ -619,23 +622,18 @@ Blockly.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
*/
Blockly.Flyout.prototype.getDynamicCategoryContents_ = function(categoryName) {
- // Look up the correct category generation function and call that to get a
- // valid XML list.
- var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback(
- categoryName);
+ var fnToApply =
+ this.workspace_.targetWorkspace.getToolboxCategoryCallback(categoryName);
if (typeof fnToApply != 'function') {
throw TypeError('Couldn\'t find a callback function when opening' +
' a toolbox category.');
}
- var 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);
};
/**
@@ -660,49 +658,79 @@ Blockly.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 {!Blockly.utils.toolbox.BlockInfo} blockInfo The info of the block.
* @return {!Blockly.BlockSvg} The block created from the blockXml.
- * @protected
+ * @private
*/
-Blockly.Flyout.prototype.createBlock_ = function(blockXml) {
- var curBlock = /** @type {!Blockly.BlockSvg} */ (
- Blockly.Xml.domToBlock(blockXml, this.workspace_));
- if (!curBlock.isEnabled()) {
+Blockly.Flyout.prototype.createFlyoutBlock_ = function(blockInfo) {
+ var block;
+ if (blockInfo['blockxml']) {
+ var xml = typeof blockInfo['blockxml'] === 'string' ?
+ Blockly.Xml.textToDom(blockInfo['blockxml']) :
+ blockInfo['blockxml'];
+ block = this.getRecycledBlock_(xml.getAttribute('type'));
+ if (!block) {
+ block = Blockly.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 = Blockly.serialization.blocks.load(
+ /** @type {Blockly.serialization.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 {!Blockly.BlockSvg} */ (block);
};
/**
- * Get the xml from the block info object.
- * @param {!Blockly.utils.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 {(!Blockly.BlockSvg|undefined)} The recycled block, or undefined if
+ * one could not be recycled.
* @private
*/
-Blockly.Flyout.prototype.getBlockXml_ = function(blockInfo) {
- var blockElement = null;
- var blockXml = blockInfo['blockxml'];
-
- if (blockXml && typeof blockXml != 'string') {
- blockElement = blockXml;
- } else if (blockXml && typeof blockXml == 'string') {
- blockElement = Blockly.Xml.textToDom(blockXml);
- blockInfo['blockxml'] = blockElement;
- } else if (blockInfo['type']) {
- blockElement = Blockly.utils.xml.createElement('xml');
- blockElement.setAttribute('type', blockInfo['type']);
- blockElement.setAttribute('disabled', blockInfo['disabled']);
- blockInfo['blockxml'] = blockElement;
+Blockly.Flyout.prototype.getRecycledBlock_ = function(blockType) {
+ var index = -1;
+ for (var 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 {!Blockly.utils.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
+ */
+Blockly.Flyout.prototype.addBlockGap_ = function(blockInfo, gaps, defaultGap) {
+ var gap;
+ if (blockInfo['gap']) {
+ gap = parseInt(blockInfo['gap'], 10);
+ } else if (blockInfo['blockxml']) {
+ var xml = typeof blockInfo['blockxml'] === 'string' ?
+ Blockly.Xml.textToDom(blockInfo['blockxml']) :
+ blockInfo['blockxml'];
+ gap = parseInt(xml.getAttribute('gap'), 10);
}
- return blockElement;
+ gaps.push(isNaN(gap) ? defaultGap : gap);
};
/**
@@ -729,13 +757,15 @@ Blockly.Flyout.prototype.addSeparatorGap_ = function(sepInfo, gaps, defaultGap)
/**
* Delete blocks, mats and buttons from a previous showing of the flyout.
- * @protected
+ * @private
*/
Blockly.Flyout.prototype.clearOldBlocks_ = function() {
// Delete any blocks from a previous showing.
var oldBlocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; (block = oldBlocks[i]); i++) {
- if (block.workspace == this.workspace_) {
+ if (this.blockIsRecyclable_(block)) {
+ this.recycleBlock_(block);
+ } else {
block.dispose(false, false);
}
}
@@ -758,6 +788,41 @@ Blockly.Flyout.prototype.clearOldBlocks_ = function() {
this.workspace_.getPotentialVariableMap().clear();
};
+/**
+ * Empties all of the recycled blocks, properly disposing of them.
+ * @private
+ */
+Blockly.Flyout.prototype.emptyRecycledBlocks_ = function() {
+ for (var i = 0; i < this.recycledBlocks_.length; i++) {
+ this.recycledBlocks_[i].dispose();
+ }
+ this.recycledBlocks_ = [];
+};
+
+/**
+ * Returns whether the given block can be recycled or not.
+ * @param {!Blockly.BlockSvg} _block The block to check for recyclability.
+ * @return {boolean} True if the block can be recycled. False otherwise.
+ * @protected
+ */
+Blockly.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 {!Blockly.BlockSvg} block The block to recycle.
+ * @private
+ */
+Blockly.Flyout.prototype.recycleBlock_ = function(block) {
+ var 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.
@@ -996,20 +1061,23 @@ Blockly.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.
- var xml = /** @type {!Element} */ (Blockly.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.
- var block = /** @type {!Blockly.BlockSvg} */
- (Blockly.Xml.domToBlock(xml, targetWorkspace));
- var svgRootNew = block.getSvgRoot();
- if (!svgRootNew) {
- throw Error('block is not rendered.');
+ 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.
+ var xml = /** @type {!Element} */ (Blockly.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.
+ var block = /** @type {!Blockly.BlockSvg} */
+ (Blockly.Xml.domToBlock(xml, targetWorkspace));
+ } else {
+ var json = /** @type {!Blockly.serialization.blocks.State} */
+ (Blockly.serialization.blocks.save(oldBlock));
+ targetWorkspace.setResizesEnabled(false);
+ var block = /** @type {!Blockly.BlockSvg} */
+ (Blockly.serialization.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 650de5ce17f..fd076638692 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 e9a9d2b2a58..4f1c67d828c 100644
--- a/core/utils/toolbox.js
+++ b/core/utils/toolbox.js
@@ -26,12 +26,29 @@ goog.requireType('Blockly.ToolboxSeparator');
/**
* 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: (!Blockly.serialization.blocks.ConnectionState|undefined)
* }}
*/
Blockly.utils.toolbox.BlockInfo;
diff --git a/core/workspace_svg.js b/core/workspace_svg.js
index 21ee217ae77..6721722bbc6 100644
--- a/core/workspace_svg.js
+++ b/core/workspace_svg.js
@@ -167,7 +167,8 @@ Blockly.WorkspaceSvg = function(
/**
* Map from function names to callbacks, for deciding what to do when a custom
* toolbox category is opened.
- * @type {!Object>}
+ * @type {!Object}
* @private
*/
this.toolboxCategoryCallbacks_ = Object.create(null);
@@ -2498,8 +2499,9 @@ Blockly.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(!Blockly.Workspace):!Array} func The function to
- * call when the given toolbox category is opened.
+ * @param {function(!Blockly.Workspace):
+ * !Blockly.utils.toolbox.FlyoutDefinition} func The function to call when
+ * the given toolbox category is opened.
*/
Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key,
func) {
@@ -2513,9 +2515,9 @@ Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key,
* 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(!Blockly.Workspace):!Array} The function
- * corresponding to the given key for this workspace, or null if no function
- * is registered.
+ * @return {?function(!Blockly.Workspace):
+ * !Blockly.utils.toolbox.FlyoutDefinition} The function corresponding to
+ * the given key for this workspace, or null if no function is registered.
*/
Blockly.WorkspaceSvg.prototype.getToolboxCategoryCallback = function(key) {
return this.toolboxCategoryCallbacks_[key] || null;
diff --git a/tests/deps.js b/tests/deps.js
index 798a29444e7..2c961eb3c94 100644
--- a/tests/deps.js
+++ b/tests/deps.js
@@ -61,7 +61,7 @@ goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Bloc
goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry']);
goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent']);
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.constants', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object']);
-goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.IFlyout', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox', 'Blockly.utils.xml']);
+goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.IFlyout', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox']);
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': 'es5'});
goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.Block', 'Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox']);
goog.addDependency('../../core/flyout_vertical.js', ['Blockly.VerticalFlyout'], ['Blockly.Block', 'Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox']);
@@ -93,6 +93,7 @@ goog.addDependency('../../core/interfaces/i_positionable.js', ['Blockly.IPositio
goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistrable'], []);
goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], []);
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], []);
+goog.addDependency('../../core/interfaces/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_styleable.js', ['Blockly.IStyleable'], []);
goog.addDependency('../../core/interfaces/i_toolbox.js', ['Blockly.IToolbox'], []);
goog.addDependency('../../core/interfaces/i_toolbox_item.js', ['Blockly.ICollapsibleToolboxItem', 'Blockly.ISelectableToolboxItem', 'Blockly.IToolboxItem'], []);
@@ -106,7 +107,7 @@ goog.addDependency('../../core/menu.js', ['Blockly.Menu'], ['Blockly.browserEven
goog.addDependency('../../core/menuitem.js', ['Blockly.MenuItem'], ['Blockly.utils.IdGenerator', 'Blockly.utils.aria', 'Blockly.utils.dom']);
goog.addDependency('../../core/metrics_manager.js', ['Blockly.FlyoutMetricsManager', 'Blockly.MetricsManager'], ['Blockly.IMetricsManager', 'Blockly.registry', 'Blockly.utils.Size', 'Blockly.utils.toolbox'], {'lang': 'es5'});
goog.addDependency('../../core/msg.js', ['Blockly.Msg'], ['Blockly.utils.global']);
-goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml']);
+goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml']);
goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg', 'Blockly.constants']);
goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.registry', 'Blockly.utils.IdGenerator', 'Blockly.utils.Metrics', 'Blockly.utils.toolbox']);
goog.addDependency('../../core/positionable_helpers.js', ['Blockly.uiPosition'], ['Blockly.Scrollbar', 'Blockly.utils.Rect', 'Blockly.utils.toolbox']);
@@ -155,7 +156,6 @@ goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', '
goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Svg', 'Blockly.utils.dom']);
goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.inputTypes', 'Blockly.serialization.exceptions', 'Blockly.serialization.priorities', 'Blockly.serialization.registry', 'Blockly.utils.Size'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/exceptions.js', ['Blockly.serialization.exceptions'], [], {'lang': 'es6', 'module': 'goog'});
-goog.addDependency('../../core/serialization/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/priorities.js', ['Blockly.serialization.priorities'], [], {'module': 'goog'});
goog.addDependency('../../core/serialization/registry.js', ['Blockly.serialization.registry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events', 'Blockly.serialization.priorities', 'Blockly.serialization.registry'], {'lang': 'es6', 'module': 'goog'});
diff --git a/tests/mocha/.eslintrc.json b/tests/mocha/.eslintrc.json
index 6ee7cbf44f3..232e3e736a1 100644
--- a/tests/mocha/.eslintrc.json
+++ b/tests/mocha/.eslintrc.json
@@ -41,8 +41,9 @@
"getDeeplyNestedJSON": true,
"getInjectedToolbox": true,
"getNonCollapsibleItem": true,
+ "getProperSimpleJson": true,
"getSeparator": true,
- "getSimpleJSON": true,
+ "getSimpleJson": true,
"getXmlArray": true,
"sharedTestSetup": true,
"sharedTestTeardown": true,
diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js
index 6ccd08d900b..8f9b70cbda5 100644
--- a/tests/mocha/flyout_test.js
+++ b/tests/mocha/flyout_test.js
@@ -236,74 +236,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);
- });
- test('NodeList', function() {
- var nodeList = document.getElementById('toolbox-simple').childNodes;
- this.flyout.show(nodeList);
- 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('Array of JSON', function() {
- this.flyout.show(this.simpleToolboxJSON);
- 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 xml', function() {
- this.flyout.show(getXmlArray());
- 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('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.');
+ });
+
+ suite('Recycling', function() {
+ setup(function() {
+ this.flyout = this.workspace.getFlyout();
});
- 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('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: 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 d2ede9a2ab6..404ba1041b4 100644
--- a/tests/mocha/index.html
+++ b/tests/mocha/index.html
@@ -114,12 +114,26 @@
+
-
+
+ NEQ
+
+
+ 1
+
+
+
+
+ 2
+
+
+
+
@@ -135,6 +149,7 @@
+
diff --git a/tests/mocha/toolbox_helper.js b/tests/mocha/toolbox_helper.js
index 9d1f54e3354..16f99c386ee 100644
--- a/tests/mocha/toolbox_helper.js
+++ b/tests/mocha/toolbox_helper.js
@@ -45,12 +45,24 @@ function 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",
@@ -68,6 +80,50 @@ function 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"
+ }
+ ]};
+}
+
/**
* Get JSON for a toolbox that contains categories that contain categories.
* @return {Blockly.utils.toolbox.ToolboxJson} The array holding information
@@ -117,10 +173,20 @@ function 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 f494aacc5ca..602b079d440 100644
--- a/tests/mocha/toolbox_test.js
+++ b/tests/mocha/toolbox_test.js
@@ -487,7 +487,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 c7758bec933..6950899e5bb 100644
--- a/tests/playground.html
+++ b/tests/playground.html
@@ -197,6 +197,19 @@
}
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') {
@@ -448,6 +461,130 @@
'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++;
+ }
+ }
+};
+