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 @@
+ + +