mirror of
https://github.com/google/blockly.git
synced 2026-01-11 10:57:07 +01:00
feat: add support for defining toolboxes using pure json (#5392)
* feat: add recycling to core * feat: add support for json block definitions in flyout * tests: reorganize tests * tests: add tests for generating contents * Fixup reycling * tests: add tests for recycling * fix: types * fix: lint * fix: PR comments * fix: creating blocks from flyout * test: add test block to playground * fix: types * feat: add support for enabled
This commit is contained in:
committed by
alschmiedt
parent
6d87b85e6c
commit
410365f4a1
@@ -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<!BlockSvg>}
|
||||
* @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.
|
||||
// <block type="math_arithmetic" gap="8"></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<!Element>} 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<number>} 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
|
||||
|
||||
@@ -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<string, *>|undefined),
|
||||
* fields: (!Object<string, *>|undefined),
|
||||
* inputs: (!Object<string, !ConnectionState>|undefined),
|
||||
|
||||
@@ -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<string, *>|undefined),
|
||||
* fields: (!Object<string, *>|undefined),
|
||||
* inputs: (!Object<string, !ConnectionState>|undefined),
|
||||
* next: (!ConnectionState|undefined)
|
||||
* }}
|
||||
*/
|
||||
let BlockInfo;
|
||||
|
||||
@@ -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<string, ?function(!Workspace):!Array<!Element>>}
|
||||
* @private
|
||||
*/
|
||||
* Map from function names to callbacks, for deciding what to do when a custom
|
||||
* toolbox category is opened.
|
||||
* @type {!Object<string, ?function(!Workspace):
|
||||
* !toolbox.FlyoutDefinition>}
|
||||
* @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<!Element>} 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<!Element>} The function
|
||||
* @return {?function(!Workspace): !toolbox.FlyoutDefinition} The function
|
||||
* corresponding to the given key for this workspace, or null if no function
|
||||
* is registered.
|
||||
*/
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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(
|
||||
'<xml>' +
|
||||
'<block type="text_print" disabled="true"></block>' +
|
||||
'</xml>'
|
||||
);
|
||||
this.flyout.show(xml);
|
||||
this.assertDisabled(true);
|
||||
});
|
||||
|
||||
test('False string', function() {
|
||||
var xml = Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="text_print" disabled="false"></block>' +
|
||||
'</xml>'
|
||||
);
|
||||
this.flyout.show(xml);
|
||||
this.assertDisabled(false);
|
||||
});
|
||||
|
||||
test('Disabled string', function() {
|
||||
// The XML system supports this for some reason!?
|
||||
var xml = Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="text_print" disabled="disabled"></block>' +
|
||||
'</xml>'
|
||||
);
|
||||
this.flyout.show(xml);
|
||||
this.assertDisabled(true);
|
||||
});
|
||||
|
||||
test('Different string', function() {
|
||||
var xml = Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="text_print" disabled="random"></block>' +
|
||||
'</xml>'
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,12 +113,26 @@
|
||||
</script>
|
||||
|
||||
<div id="blocklyDiv"></div>
|
||||
|
||||
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-simple" style="display: none">
|
||||
<block type="logic_operation"></block>
|
||||
<block type="logic_compare">
|
||||
<field name="OP">NEQ</field>
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<block type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<sep gap="20"></sep>
|
||||
<button text="insert" callbackkey="insertConnectionRows"></button>
|
||||
<label text="tooltips"></label>
|
||||
</xml>
|
||||
|
||||
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-categories" style="display: none">
|
||||
<category name="First" css-container="something">
|
||||
<block type="basic_block">
|
||||
@@ -134,6 +148,7 @@
|
||||
</block>
|
||||
</category>
|
||||
</xml>
|
||||
|
||||
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-test" style="display: none">
|
||||
<category name="First" expanded="true" categorystyle="logic_category">
|
||||
<sep gap="-1"></sep>
|
||||
|
||||
@@ -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": "<block type=\"logic_operation\"></block>",
|
||||
"type":"logic_operation"
|
||||
"blockxml":
|
||||
`<block type="logic_compare">
|
||||
<field name="OP">NEQ</field>
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<block type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>`,
|
||||
},
|
||||
{
|
||||
"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<Node>} 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(
|
||||
`<block type="logic_compare">
|
||||
<field name="OP">NEQ</field>
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<block type="math_number">
|
||||
<field name="NUM">2</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>`);
|
||||
var separator = Blockly.Xml.textToDom('<sep gap="20"></sep>');
|
||||
var button = Blockly.Xml.textToDom('<button text="insert" callbackkey="insertConnectionRows"></button>');
|
||||
var label = Blockly.Xml.textToDom('<label text="tooltips"></label>');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -922,6 +1059,8 @@ var spaghettiXml = [
|
||||
<sep></sep>
|
||||
<category name="Variables" categorystyle="variable_category" custom="VARIABLE"></category>
|
||||
<category name="Functions" categorystyle="procedure_category" custom="PROCEDURE"></category>
|
||||
<sep></sep>
|
||||
<category name="JSON" categorystyle="procedure_category" custom="JSON"></category>
|
||||
</xml>
|
||||
|
||||
<!-- toolbox-categories-typed-variables has a category menu and an
|
||||
|
||||
Reference in New Issue
Block a user