Toolbox Rewrite (#4223)

Rewrite the toolbox in order to get rid of old closure code and make it easier to extend.

Co-authored-by: Maribeth Bottorff <maribethb@google.com>
This commit is contained in:
alschmiedt
2020-09-02 08:13:07 -07:00
committed by GitHub
parent 05c74d85d0
commit d01169fa79
30 changed files with 3504 additions and 2815 deletions
+12 -8
View File
@@ -31,11 +31,8 @@ goog.addDependency('../../core/blockly.js', ['Blockly'], ['Blockly.Events', 'Blo
goog.addDependency('../../core/blocks.js', ['Blockly.Blocks'], [], {});
goog.addDependency('../../core/bubble.js', ['Blockly.Bubble'], ['Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.Workspace', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/bubble_dragger.js', ['Blockly.BubbleDragger'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.CommentMove', 'Blockly.utils', 'Blockly.utils.Coordinate'], {});
goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.utils.deprecation', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/components/component.js', ['Blockly.Component', 'Blockly.Component.Error'], ['Blockly.utils.IdGenerator', 'Blockly.utils.dom', 'Blockly.utils.style'], {});
goog.addDependency('../../core/components/tree/basenode.js', ['Blockly.tree.BaseNode'], ['Blockly.Component', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.object', 'Blockly.utils.style'], {});
goog.addDependency('../../core/components/tree/treecontrol.js', ['Blockly.tree.TreeControl'], ['Blockly.tree.BaseNode', 'Blockly.tree.TreeNode', 'Blockly.utils.aria', 'Blockly.utils.object', 'Blockly.utils.style'], {});
goog.addDependency('../../core/components/tree/treenode.js', ['Blockly.tree.TreeNode'], ['Blockly.tree.BaseNode', 'Blockly.utils.KeyCodes', 'Blockly.utils.object'], {});
goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.Xml', 'Blockly.utils.deprecation'], {});
goog.addDependency('../../core/connection_checker.js', ['Blockly.ConnectionChecker'], ['Blockly.registry'], {});
goog.addDependency('../../core/connection_db.js', ['Blockly.ConnectionDB'], ['Blockly.RenderedConnection'], {});
@@ -48,7 +45,7 @@ goog.addDependency('../../core/dropdowndiv.js', ['Blockly.DropDownDiv'], ['Block
goog.addDependency('../../core/events.js', ['Blockly.Events'], ['Blockly.registry', 'Blockly.utils'], {});
goog.addDependency('../../core/events_abstract.js', ['Blockly.Events.Abstract'], ['Blockly.Events'], {});
goog.addDependency('../../core/extensions.js', ['Blockly.Extensions'], ['Blockly.utils'], {});
goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.dom', 'Blockly.utils.style', 'Blockly.utils.userAgent'], {'lang': 'es5'});
goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.deprecation', 'Blockly.utils.dom', 'Blockly.utils.style', 'Blockly.utils.userAgent'], {'lang': 'es5'});
goog.addDependency('../../core/field_angle.js', ['Blockly.FieldAngle'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/field_checkbox.js', ['Blockly.FieldCheckbox'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.Size', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
goog.addDependency('../../core/field_colour.js', ['Blockly.FieldColour'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.navigation', 'Blockly.utils.IdGenerator', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
@@ -61,7 +58,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.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object'], {});
goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutCursor', 'Blockly.Gesture', 'Blockly.Marker', 'Blockly.Scrollbar', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom'], {});
goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutCursor', 'Blockly.Gesture', 'Blockly.Marker', 'Blockly.Scrollbar', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/flyout_button.js', ['Blockly.FlyoutButton'], ['Blockly.Css', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom'], {'lang': 'es5'});
goog.addDependency('../../core/flyout_dragger.js', ['Blockly.FlyoutDragger'], ['Blockly.WorkspaceDragger', 'Blockly.utils.object'], {});
goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.Block', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.object'], {});
@@ -75,7 +72,9 @@ goog.addDependency('../../core/input.js', ['Blockly.Input'], ['Blockly.Connectio
goog.addDependency('../../core/insertion_marker_manager.js', ['Blockly.InsertionMarkerManager'], ['Blockly.Events', 'Blockly.blockAnimations'], {'lang': 'es5'});
goog.addDependency('../../core/interfaces/i_accessibility.js', ['Blockly.IASTNodeLocation', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IBlocklyActionable'], [], {});
goog.addDependency('../../core/interfaces/i_bounded_element.js', ['Blockly.IBoundedElement'], [], {});
goog.addDependency('../../core/interfaces/i_bubble.js', ['Blockly.IBubble'], [], {});
goog.addDependency('../../core/interfaces/i_connection_checker.js', ['Blockly.IConnectionChecker'], [], {});
goog.addDependency('../../core/interfaces/i_contextmenu.js', ['Blockly.IContextMenu'], [], {});
goog.addDependency('../../core/interfaces/i_copyable.js', ['Blockly.ICopyable'], [], {});
goog.addDependency('../../core/interfaces/i_deletable.js', ['Blockly.IDeletable'], [], {});
goog.addDependency('../../core/interfaces/i_deletearea.js', ['Blockly.IDeleteArea'], [], {});
@@ -85,6 +84,7 @@ goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistra
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], [], {});
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'], [], {});
goog.addDependency('../../core/keyboard_nav/action.js', ['Blockly.Action'], [], {});
goog.addDependency('../../core/keyboard_nav/ast_node.js', ['Blockly.ASTNode'], ['Blockly.utils.Coordinate'], {'lang': 'es5'});
goog.addDependency('../../core/keyboard_nav/basic_cursor.js', ['Blockly.BasicCursor'], ['Blockly.ASTNode', 'Blockly.Cursor'], {'lang': 'es5'});
@@ -103,7 +103,7 @@ goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg'], {}
goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.Xml', 'Blockly.registry', 'Blockly.user.keyMap', 'Blockly.utils.IdGenerator', 'Blockly.utils.Metrics', 'Blockly.utils.toolbox', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/procedures.js', ['Blockly.Procedures'], ['Blockly.Blocks', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.Names', 'Blockly.Workspace', 'Blockly.Xml', 'Blockly.constants', 'Blockly.utils.xml'], {});
goog.addDependency('../../core/registry.js', ['Blockly.registry'], [], {});
goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.Events', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.Events', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.deprecation', 'Blockly.utils.dom', 'Blockly.utils.object'], {});
goog.addDependency('../../core/renderers/common/block_rendering.js', ['Blockly.blockRendering'], ['Blockly.registry', 'Blockly.utils.object'], {});
goog.addDependency('../../core/renderers/common/constants.js', ['Blockly.blockRendering.ConstantProvider'], ['Blockly.utils', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.svgPaths', 'Blockly.utils.userAgent'], {'lang': 'es5'});
goog.addDependency('../../core/renderers/common/debugger.js', ['Blockly.blockRendering.Debug'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.InputRow', 'Blockly.blockRendering.Measurable', 'Blockly.blockRendering.RenderInfo', 'Blockly.blockRendering.Row', 'Blockly.blockRendering.SpacerRow', 'Blockly.blockRendering.TopRow', 'Blockly.blockRendering.Types'], {'lang': 'es5'});
@@ -153,7 +153,11 @@ goog.addDependency('../../core/theme/modern.js', ['Blockly.Themes.Modern'], ['Bl
goog.addDependency('../../core/theme/tritanopia.js', ['Blockly.Themes.Tritanopia'], ['Blockly.Theme'], {});
goog.addDependency('../../core/theme/zelos.js', ['Blockly.Themes.Zelos'], ['Blockly.Theme'], {});
goog.addDependency('../../core/theme_manager.js', ['Blockly.ThemeManager'], ['Blockly.Theme'], {});
goog.addDependency('../../core/toolbox.js', ['Blockly.Toolbox'], ['Blockly.Css', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.Touch', 'Blockly.navigation', 'Blockly.registry', 'Blockly.tree.TreeControl', 'Blockly.tree.TreeNode', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/toolbox/category.js', ['Blockly.ToolboxCategory'], ['Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {'lang': 'es5'});
goog.addDependency('../../core/toolbox/collapsible_category.js', ['Blockly.CollapsibleToolboxCategory'], ['Blockly.ToolboxCategory', 'Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/toolbox/separator.js', ['Blockly.ToolboxSeparator'], ['Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils.dom'], {'lang': 'es5'});
goog.addDependency('../../core/toolbox/toolbox.js', ['Blockly.Toolbox'], ['Blockly.CollapsibleToolboxCategory', 'Blockly.Css', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.ToolboxCategory', 'Blockly.ToolboxSeparator', 'Blockly.Touch', 'Blockly.navigation', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.dom'], {'lang': 'es5'});
goog.addDependency('../../core/toolbox/toolbox_item.js', ['Blockly.ToolboxItem'], [], {});
goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.utils.string'], {});
goog.addDependency('../../core/touch.js', ['Blockly.Touch'], ['Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.string'], {});
goog.addDependency('../../core/touch_gesture.js', ['Blockly.TouchGesture'], ['Blockly.Gesture', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.object'], {});
-895
View File
@@ -1,895 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Definition of the Blockly.tree.BaseNode class.
* This class is similar to Closure's goog.ui.tree.BaseNode class.
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.tree.BaseNode');
goog.require('Blockly.Component');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.KeyCodes');
goog.require('Blockly.utils.style');
/**
* An abstract base class for a node in the tree.
* Similar to goog.ui.tree.BaseNode
*
* @param {string} content The content of the node label treated as
* plain-text and will be HTML escaped.
* @param {!Blockly.tree.BaseNode.Config} config The configuration for the tree.
* @constructor
* @extends {Blockly.Component}
*/
Blockly.tree.BaseNode = function(content, config) {
Blockly.Component.call(this);
/**
* Text content of the node label.
* @type {string}
* @package
*/
this.content = content;
/**
* @type {string}
* @package
*/
this.iconClass;
/**
* @type {string}
* @package
*/
this.expandedIconClass;
/**
* The configuration for the tree.
* @type {!Blockly.tree.BaseNode.Config}
* @protected
*/
this.config_ = config;
/**
* @type {Blockly.tree.TreeControl}
* @protected
*/
this.tree;
/**
* @type {Blockly.tree.BaseNode}
* @private
*/
this.previousSibling_;
/**
* @type {Blockly.tree.BaseNode}
* @private
*/
this.nextSibling_;
/**
* Whether the tree item is selected.
* @type {boolean}
* @protected
*/
this.selected_ = false;
/**
* Whether the tree node is expanded.
* @type {boolean}
* @protected
*/
this.expanded_ = false;
/**
* Nesting depth of this node; cached result of getDepth.
* -1 if value has not been cached.
* @type {number}
* @private
*/
this.depth_ = -1;
};
Blockly.utils.object.inherits(Blockly.tree.BaseNode, Blockly.Component);
/**
* The config type for the tree.
* @typedef {{
* indentWidth:number,
* cssRoot:string,
* cssHideRoot:string,
* cssTreeRow:string,
* cssItemLabel:string,
* cssTreeIcon:string,
* cssExpandedFolderIcon:string,
* cssCollapsedFolderIcon:string,
* cssFileIcon:string,
* cssSelectedRow:string
* }}
*/
Blockly.tree.BaseNode.Config;
/**
* Map of nodes in existence. Needed to route events to the appropriate nodes.
* Nodes are added to the map at {@link #enterDocument} time and removed at
* {@link #exitDocument} time.
* @type {Object}
* @protected
*/
Blockly.tree.BaseNode.allNodes = {};
/** @override */
Blockly.tree.BaseNode.prototype.disposeInternal = function() {
Blockly.tree.BaseNode.superClass_.disposeInternal.call(this);
if (this.tree) {
this.tree = null;
}
this.setElementInternal(null);
};
/**
* Adds roles and states.
* @protected
*/
Blockly.tree.BaseNode.prototype.initAccessibility = function() {
var el = this.getElement();
if (el) {
// Set an id for the label
var label = this.getLabelElement();
if (label && !label.id) {
label.id = this.getId() + '.label';
}
Blockly.utils.aria.setRole(el, Blockly.utils.aria.Role.TREEITEM);
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.SELECTED, false);
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.LEVEL, this.getDepth());
if (label) {
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.LABELLEDBY, label.id);
}
var img = this.getIconElement();
if (img) {
Blockly.utils.aria.setRole(img, Blockly.utils.aria.Role.PRESENTATION);
}
var ce = this.getChildrenElement();
if (ce) {
Blockly.utils.aria.setRole(ce, Blockly.utils.aria.Role.GROUP);
// In case the children will be created lazily.
if (ce.hasChildNodes()) {
// Only set aria-expanded if the node has children (can be expanded).
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.EXPANDED, false);
// do setsize for each child
var count = this.getChildCount();
for (var i = 1; i <= count; i++) {
var child = /** @type {!Element} */ (this.getChildAt(i - 1).getElement());
Blockly.utils.aria.setState(child,
Blockly.utils.aria.State.SETSIZE, count);
Blockly.utils.aria.setState(child,
Blockly.utils.aria.State.POSINSET, i);
}
}
}
}
};
/** @override */
Blockly.tree.BaseNode.prototype.createDom = function() {
var element = document.createElement('div');
element.appendChild(this.toDom());
this.setElementInternal(/** @type {!HTMLElement} */ (element));
};
/** @override */
Blockly.tree.BaseNode.prototype.enterDocument = function() {
Blockly.tree.BaseNode.superClass_.enterDocument.call(this);
Blockly.tree.BaseNode.allNodes[this.getId()] = this;
this.initAccessibility();
};
/** @override */
Blockly.tree.BaseNode.prototype.exitDocument = function() {
Blockly.tree.BaseNode.superClass_.exitDocument.call(this);
delete Blockly.tree.BaseNode.allNodes[this.getId()];
};
/**
* The method assumes that the child doesn't have parent node yet.
* @override
*/
Blockly.tree.BaseNode.prototype.addChildAt = function(child, index) {
child = /** @type {Blockly.tree.BaseNode} */ (child);
var prevNode = this.getChildAt(index - 1);
var nextNode = this.getChildAt(index);
Blockly.tree.BaseNode.superClass_.addChildAt.call(this, child, index);
child.previousSibling_ = prevNode;
child.nextSibling_ = nextNode;
if (prevNode) {
prevNode.nextSibling_ = child;
}
if (nextNode) {
nextNode.previousSibling_ = child;
}
var tree = this.getTree();
if (tree) {
child.setTreeInternal(tree);
}
child.setDepth_(this.getDepth() + 1);
var el = this.getElement();
if (el) {
this.updateExpandIcon();
Blockly.utils.aria.setState(
el, Blockly.utils.aria.State.EXPANDED, this.expanded_);
if (this.expanded_) {
var childrenEl = this.getChildrenElement();
if (!child.getElement()) {
child.createDom();
}
var childElement = child.getElement();
var nextElement = nextNode && nextNode.getElement();
childrenEl.insertBefore(childElement, nextElement);
if (this.isInDocument()) {
child.enterDocument();
}
if (!nextNode) {
if (prevNode) {
prevNode.updateExpandIcon();
} else {
Blockly.utils.style.setElementShown(childrenEl, true);
this.setExpanded(this.expanded_);
}
}
}
}
};
/**
* Appends a node as a child to the current node.
* @param {Blockly.tree.BaseNode} child The child to add.
* @package
*/
Blockly.tree.BaseNode.prototype.add = function(child) {
if (child.getParent()) {
throw Error(Blockly.Component.Error.PARENT_UNABLE_TO_BE_SET);
}
this.addChildAt(child, this.getChildCount());
};
/**
* Returns the tree.
* @return {?Blockly.tree.TreeControl} tree
* @protected
*/
Blockly.tree.BaseNode.prototype.getTree = function() {
return null;
};
/**
* Returns the depth of the node in the tree. Should not be overridden.
* @return {number} The non-negative depth of this node (the root is zero).
* @protected
*/
Blockly.tree.BaseNode.prototype.getDepth = function() {
var depth = this.depth_;
if (depth < 0) {
var parent = this.getParent();
if (parent) {
depth = parent.getDepth() + 1;
} else {
depth = 0;
}
this.setDepth_(depth);
}
return depth;
};
/**
* Changes the depth of a node (and all its descendants).
* @param {number} depth The new nesting depth; must be non-negative.
* @private
*/
Blockly.tree.BaseNode.prototype.setDepth_ = function(depth) {
if (depth != this.depth_) {
this.depth_ = depth;
var row = this.getRowElement();
if (row) {
var indent = this.getPixelIndent_() + 'px';
if (this.rightToLeft_) {
row.style.paddingRight = indent;
} else {
row.style.paddingLeft = indent;
}
}
this.forEachChild(function(child) { child.setDepth_(depth + 1); });
}
};
/**
* Returns true if the node is a descendant of this node.
* @param {Blockly.Component} node The node to check.
* @return {boolean} True if the node is a descendant of this node, false
* otherwise.
* @protected
*/
Blockly.tree.BaseNode.prototype.contains = function(node) {
while (node) {
if (node == this) {
return true;
}
node = node.getParent();
}
return false;
};
/**
* This is re-defined here to indicate to the Closure Compiler the correct
* child return type.
* @param {number} index 0-based index.
* @return {Blockly.tree.BaseNode} The child at the given index; null if none.
* @protected
*/
Blockly.tree.BaseNode.prototype.getChildAt;
/**
* Returns the children of this node.
* @return {!Array.<!Blockly.tree.BaseNode>} The children.
* @package
*/
Blockly.tree.BaseNode.prototype.getChildren = function() {
var children = [];
this.forEachChild(function(child) { children.push(child); });
return children;
};
/**
* Returns the node's parent, if any.
* @return {?Blockly.tree.BaseNode} The parent node.
* @protected
*/
Blockly.tree.BaseNode.prototype.getParent = function() {
return /** @type {Blockly.tree.BaseNode} */ (
Blockly.tree.BaseNode.superClass_.getParent.call(this));
};
/**
* @return {Blockly.tree.BaseNode} The previous sibling of this node.
* @protected
*/
Blockly.tree.BaseNode.prototype.getPreviousSibling = function() {
return this.previousSibling_;
};
/**
* @return {Blockly.tree.BaseNode} The next sibling of this node.
* @protected
*/
Blockly.tree.BaseNode.prototype.getNextSibling = function() {
return this.nextSibling_;
};
/**
* @return {boolean} Whether the node is the last sibling.
* @protected
*/
Blockly.tree.BaseNode.prototype.isLastSibling = function() {
return !this.nextSibling_;
};
/**
* @return {boolean} Whether the node is selected.
* @protected
*/
Blockly.tree.BaseNode.prototype.isSelected = function() {
return this.selected_;
};
/**
* Selects the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.select = function() {
var tree = this.getTree();
if (tree) {
tree.setSelectedItem(this);
}
};
/**
* Called from the tree to instruct the node change its selection state.
* @param {boolean} selected The new selection state.
* @protected
*/
Blockly.tree.BaseNode.prototype.setSelected = function(selected) {
if (this.selected_ == selected) {
return;
}
this.selected_ = selected;
this.updateRow();
var el = this.getElement();
if (el) {
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.SELECTED, selected);
if (selected) {
var treeElement = /** @type {!Element} */ (this.getTree().getElement());
Blockly.utils.aria.setState(treeElement,
Blockly.utils.aria.State.ACTIVEDESCENDANT, this.getId());
}
}
};
/**
* Sets the node to be expanded.
* @param {boolean} expanded Whether to expand or close the node.
* @package
*/
Blockly.tree.BaseNode.prototype.setExpanded = function(expanded) {
var isStateChange = expanded != this.expanded_;
var ce;
this.expanded_ = expanded;
var tree = this.getTree();
var el = this.getElement();
if (this.hasChildren()) {
if (!expanded && tree && this.contains(tree.getSelectedItem())) {
this.select();
}
if (el) {
ce = this.getChildrenElement();
if (ce) {
Blockly.utils.style.setElementShown(ce, expanded);
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.EXPANDED, expanded);
// Make sure we have the HTML for the children here.
if (expanded && this.isInDocument() && !ce.hasChildNodes()) {
this.forEachChild(function(child) {
ce.appendChild(child.toDom());
});
this.forEachChild(function(child) { child.enterDocument(); });
}
}
this.updateExpandIcon();
}
} else {
ce = this.getChildrenElement();
if (ce) {
Blockly.utils.style.setElementShown(ce, false);
}
}
if (el) {
this.updateIcon_();
}
if (isStateChange) {
if (expanded) {
this.doNodeExpanded();
} else {
this.doNodeCollapsed();
}
}
};
/**
* Used to notify a node of that we have expanded it.
* Can be overridden by subclasses, see Blockly.tree.TreeNode.
* @protected
*/
Blockly.tree.BaseNode.prototype.doNodeExpanded = function() {
// NOP
};
/**
* Used to notify a node that we have collapsed it.
* Can be overridden by subclasses, see Blockly.tree.TreeNode.
* @protected
*/
Blockly.tree.BaseNode.prototype.doNodeCollapsed = function() {
// NOP
};
/**
* Toggles the expanded state of the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.toggle = function() {
this.setExpanded(!this.expanded_);
};
/**
* Creates HTML Element for the node.
* @return {!Element} HTML element
* @protected
*/
Blockly.tree.BaseNode.prototype.toDom = function() {
var nonEmptyAndExpanded = this.expanded_ && this.hasChildren();
var children = document.createElement('div');
children.style.backgroundPosition = this.getBackgroundPosition();
if (!nonEmptyAndExpanded) {
children.style.display = 'none';
}
if (nonEmptyAndExpanded) {
// children
this.forEachChild(function(child) { children.appendChild(child.toDom()); });
}
var node = document.createElement('div');
node.id = this.getId();
node.appendChild(this.getRowDom());
node.appendChild(children);
return node;
};
/**
* Calculates correct padding for each row. Nested categories are indented more.
* @return {number} The pixel indent of the row.
* @private
*/
Blockly.tree.BaseNode.prototype.getPixelIndent_ = function() {
return Math.max(0, (this.getDepth() - 1) * this.config_.indentWidth);
};
/**
* Creates row with icon and label dom.
* @return {!Element} The HTML element for the row.
* @protected
*/
Blockly.tree.BaseNode.prototype.getRowDom = function() {
var row = document.createElement('div');
row.className = this.getRowClassName();
row.style['padding-' + (this.rightToLeft_ ? 'right' : 'left')] =
this.getPixelIndent_() + 'px';
row.appendChild(this.getIconDom());
row.appendChild(this.getLabelDom());
return row;
};
/**
* Adds the selected class name to the default row class name if node is
* selected.
* @return {string} The class name for the row.
* @protected
*/
Blockly.tree.BaseNode.prototype.getRowClassName = function() {
var selectedClass = '';
if (this.isSelected()) {
selectedClass = ' ' + (this.config_.cssSelectedRow || '');
}
return this.config_.cssTreeRow + selectedClass;
};
/**
* @return {!Element} The HTML element for the label.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLabelDom = function() {
var label = document.createElement('span');
label.className = this.config_.cssItemLabel || '';
label.textContent = this.content;
return label;
};
/**
* @return {!Element} The HTML for the icon.
* @protected
*/
Blockly.tree.BaseNode.prototype.getIconDom = function() {
var icon = document.createElement('span');
icon.style.display = 'inline-block';
icon.className = this.getCalculatedIconClass();
return icon;
};
/**
* Gets the calculated icon class.
* @protected
*/
Blockly.tree.BaseNode.prototype.getCalculatedIconClass = function() {
throw Error(Blockly.Component.Error.ABSTRACT_METHOD);
};
/**
* Gets a string containing the x and y position of the node's background.
* @return {string} The background position style value.
* @protected
*/
Blockly.tree.BaseNode.prototype.getBackgroundPosition = function() {
return (this.isLastSibling() ? '-100' : (this.getDepth() - 1) *
this.config_.indentWidth) + 'px 0';
};
/**
* @return {HTMLElement} The element for the tree node.
* @override
*/
Blockly.tree.BaseNode.prototype.getElement = function() {
var el = Blockly.tree.BaseNode.superClass_.getElement.call(this);
if (!el) {
el = document.getElementById(this.getId());
this.setElementInternal(el);
}
return /** @type {!HTMLElement} */ (el);
};
/**
* @return {Element} The row is the div that is used to draw the node without
* the children.
* @package
*/
Blockly.tree.BaseNode.prototype.getRowElement = function() {
var el = this.getElement();
return el ? /** @type {Element} */ (el.firstChild) : null;
};
/**
* @return {Element} The icon element.
* @protected
*/
Blockly.tree.BaseNode.prototype.getIconElement = function() {
var el = this.getRowElement();
return el ? /** @type {Element} */ (el.firstChild) : null;
};
/**
* @return {Element} The label element.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLabelElement = function() {
var el = this.getRowElement();
return el && el.lastChild ?
/** @type {Element} */ (el.lastChild.previousSibling) :
null;
};
/**
* @return {Element} The div containing the children.
* @protected
*/
Blockly.tree.BaseNode.prototype.getChildrenElement = function() {
var el = this.getElement();
return el ? /** @type {Element} */ (el.lastChild) : null;
};
/**
* Updates the row styles.
* @protected
*/
Blockly.tree.BaseNode.prototype.updateRow = function() {
var rowEl = this.getRowElement();
if (rowEl) {
rowEl.className = this.getRowClassName();
}
};
/**
* Updates the expand icon of the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.updateExpandIcon = function() {
var cel = this.getChildrenElement();
if (cel) {
cel.style.backgroundPosition = this.getBackgroundPosition();
}
};
/**
* Updates the icon of the node. Assumes that this.getElement() is created.
* @private
*/
Blockly.tree.BaseNode.prototype.updateIcon_ = function() {
this.getIconElement().className = this.getCalculatedIconClass();
};
/**
* Handles a click event.
* @param {!Event} e The browser event.
* @protected
*/
Blockly.tree.BaseNode.prototype.onClick_ = function(e) {
e.preventDefault();
};
/**
* Handles a key down event.
* @param {!Event} e The browser event.
* @return {boolean} The handled value.
* @protected
*/
Blockly.tree.BaseNode.prototype.onKeyDown = function(e) {
var handled = true;
switch (e.keyCode) {
case Blockly.utils.KeyCodes.RIGHT:
handled = this.selectChild();
break;
case Blockly.utils.KeyCodes.LEFT:
handled = this.selectParent();
break;
case Blockly.utils.KeyCodes.DOWN:
handled = this.selectNext();
break;
case Blockly.utils.KeyCodes.UP:
handled = this.selectPrevious();
break;
case Blockly.utils.KeyCodes.ENTER:
case Blockly.utils.KeyCodes.SPACE:
this.toggle();
handled = true;
break;
default:
handled = false;
}
if (handled) {
e.preventDefault();
}
return handled;
};
/**
* Select the next node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectNext = function() {
var nextNode = this.getNextShownNode();
if (nextNode) {
nextNode.select();
}
return true;
};
/**
* Select the previous node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectPrevious = function() {
var previousNode = this.getPreviousShownNode();
if (previousNode) {
previousNode.select();
}
return true;
};
/**
* Select the parent node or collapse the current node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectParent = function() {
if (this.hasChildren() && this.expanded_) {
this.setExpanded(false);
} else {
var parent = this.getParent();
var tree = this.getTree();
// don't go to root if hidden
if (parent && (parent != tree)) {
parent.select();
}
}
return true;
};
/**
* Expand the current node if it's not already expanded, or select the
* child node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectChild = function() {
if (this.hasChildren()) {
if (!this.expanded_) {
this.setExpanded(true);
} else {
this.getChildAt(0).select();
}
return true;
}
return false;
};
/**
* @return {Blockly.tree.BaseNode} The last shown descendant.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLastShownDescendant = function() {
if (!this.expanded_ || !this.hasChildren()) {
return this;
}
// we know there is at least 1 child
return this.getChildAt(this.getChildCount() - 1).getLastShownDescendant();
};
/**
* @return {Blockly.tree.BaseNode} The next node to show or null if there isn't
* a next node to show.
* @protected
*/
Blockly.tree.BaseNode.prototype.getNextShownNode = function() {
if (this.hasChildren() && this.expanded_) {
return this.getChildAt(0);
}
var parent = this;
var next;
while (parent != this.getTree()) {
next = parent.getNextSibling();
if (next != null) {
return next;
}
parent = parent.getParent();
}
return null;
};
/**
* @return {Blockly.tree.BaseNode} The previous node to show.
* @protected
*/
Blockly.tree.BaseNode.prototype.getPreviousShownNode = function() {
var ps = this.getPreviousSibling();
if (ps != null) {
return ps.getLastShownDescendant();
}
var parent = this.getParent();
var tree = this.getTree();
if (parent == tree) {
return null;
}
// The root is the first node.
if (this == tree) {
return null;
}
return /** @type {Blockly.tree.BaseNode} */ (parent);
};
/**
* Internal method that is used to set the tree control on the node.
* @param {Blockly.tree.TreeControl} tree The tree control.
* @protected
*/
Blockly.tree.BaseNode.prototype.setTreeInternal = function(tree) {
if (this.tree != tree) {
this.tree = tree;
this.forEachChild(function(child) { child.setTreeInternal(tree); });
}
};
-332
View File
@@ -1,332 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Definition of the Blockly.tree.TreeControl class.
* This class is similar to Closure's goog.ui.tree.TreeControl class.
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.tree.TreeControl');
goog.require('Blockly.tree.TreeNode');
goog.require('Blockly.tree.BaseNode');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.style');
/**
* An extension of the TreeControl object in closure that provides
* a way to view a hierarchical set of data.
* Similar to Closure's goog.ui.tree.TreeControl
*
* @param {Blockly.Toolbox} toolbox The parent toolbox for this tree.
* @param {!Blockly.tree.BaseNode.Config} config The configuration for the tree.
* @constructor
* @extends {Blockly.tree.BaseNode}
*/
Blockly.tree.TreeControl = function(toolbox, config) {
this.toolbox_ = toolbox;
/**
* Click event data.
* @type {?Blockly.EventData}
* @private
*/
this.onClickWrapper_ = null;
/**
* Key down event data.
* @type {?Blockly.EventData}
* @private
*/
this.onKeydownWrapper_ = null;
Blockly.tree.BaseNode.call(this, '', config);
// The root is open and selected by default.
this.expanded_ = true;
this.selected_ = true;
/**
* Currently selected item.
* @type {Blockly.tree.BaseNode}
* @private
*/
this.selectedItem_ = this;
/**
* A handler that's triggered before a node is selected.
* @type {?function(Blockly.tree.BaseNode):boolean}
* @private
*/
this.onBeforeSelected_ = null;
/**
* A handler that's triggered before a node is selected.
* @type {?function(Blockly.tree.BaseNode, Blockly.tree.BaseNode):?}
* @private
*/
this.onAfterSelected_ = null;
};
Blockly.utils.object.inherits(Blockly.tree.TreeControl, Blockly.tree.BaseNode);
/**
* Returns the tree.
* @override
*/
Blockly.tree.TreeControl.prototype.getTree = function() {
return this;
};
/**
* Returns the associated toolbox.
* @return {Blockly.Toolbox} The toolbox.
* @package
*/
Blockly.tree.TreeControl.prototype.getToolbox = function() {
return this.toolbox_;
};
/**
* Return node depth.
* @override
*/
Blockly.tree.TreeControl.prototype.getDepth = function() {
return 0;
};
/** @override */
Blockly.tree.TreeControl.prototype.setExpanded = function(expanded) {
this.expanded_ = expanded;
};
/** @override */
Blockly.tree.TreeControl.prototype.getIconElement = function() {
var el = this.getRowElement();
return el ? /** @type {Element} */ (el.firstChild) : null;
};
/** @override */
Blockly.tree.TreeControl.prototype.updateExpandIcon = function() {
// no expand icon
};
/** @override */
Blockly.tree.TreeControl.prototype.getRowClassName = function() {
return Blockly.tree.TreeControl.superClass_.getRowClassName.call(this) +
' ' + this.config_.cssHideRoot;
};
/**
* Returns the source for the icon.
* @return {string} Src for the icon.
* @override
*/
Blockly.tree.TreeControl.prototype.getCalculatedIconClass = function() {
var expanded = this.expanded_;
if (expanded && this.expandedIconClass) {
return this.expandedIconClass;
}
var iconClass = this.iconClass;
if (!expanded && iconClass) {
return iconClass;
}
return '';
};
/**
* Sets the selected item.
* @param {Blockly.tree.BaseNode} node The item to select.
* @package
*/
Blockly.tree.TreeControl.prototype.setSelectedItem = function(node) {
if (node == this.selectedItem_) {
return;
}
if (this.onBeforeSelected_ &&
!this.onBeforeSelected_.call(this.toolbox_, node)) {
return;
}
var oldNode = this.getSelectedItem();
if (this.selectedItem_) {
this.selectedItem_.setSelected(false);
}
this.selectedItem_ = node;
if (node) {
node.setSelected(true);
}
if (this.onAfterSelected_) {
this.onAfterSelected_.call(this.toolbox_, oldNode, node);
}
};
/**
* Set the handler that's triggered before a node is selected.
* @param {function(Blockly.tree.BaseNode):boolean} fn The handler
* @package
*/
Blockly.tree.TreeControl.prototype.onBeforeSelected = function(fn) {
this.onBeforeSelected_ = fn;
};
/**
* Set the handler that's triggered after a node is selected.
* @param {function(
* Blockly.tree.BaseNode, Blockly.tree.BaseNode):?} fn The handler
* @package
*/
Blockly.tree.TreeControl.prototype.onAfterSelected = function(fn) {
this.onAfterSelected_ = fn;
};
/**
* Returns the selected item.
* @return {Blockly.tree.BaseNode} The currently selected item.
* @package
*/
Blockly.tree.TreeControl.prototype.getSelectedItem = function() {
return this.selectedItem_;
};
/**
* Add roles and states.
* @protected
* @override
*/
Blockly.tree.TreeControl.prototype.initAccessibility = function() {
Blockly.tree.TreeControl.superClass_.initAccessibility.call(this);
var el = /** @type {!Element} */ (this.getElement());
Blockly.utils.aria.setRole(el, Blockly.utils.aria.Role.TREE);
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.LABELLEDBY, this.getLabelElement().id);
};
/** @override */
Blockly.tree.TreeControl.prototype.enterDocument = function() {
Blockly.tree.TreeControl.superClass_.enterDocument.call(this);
var el = this.getElement();
el.className = this.config_.cssRoot;
el.setAttribute('hideFocus', 'true');
this.attachEvents_();
this.initAccessibility();
};
/** @override */
Blockly.tree.TreeControl.prototype.exitDocument = function() {
Blockly.tree.TreeControl.superClass_.exitDocument.call(this);
this.detachEvents_();
};
/**
* Adds the event listeners to the tree.
* @private
*/
Blockly.tree.TreeControl.prototype.attachEvents_ = function() {
var el = this.getElement();
el.tabIndex = 0;
this.onClickWrapper_ = Blockly.bindEventWithChecks_(el,
'click', this, this.handleMouseEvent_);
this.onKeydownWrapper_ = Blockly.bindEvent_(el,
'keydown', this, this.handleKeyEvent_);
};
/**
* Removes the event listeners from the tree.
* @private
*/
Blockly.tree.TreeControl.prototype.detachEvents_ = function() {
if (this.onClickWrapper_) {
Blockly.unbindEvent_(this.onClickWrapper_);
this.onClickWrapper_ = null;
}
if (this.onKeydownWrapper_) {
Blockly.unbindEvent_(this.onKeydownWrapper_);
this.onKeydownWrapper_ = null;
}
};
/**
* Handles mouse events.
* @param {!Event} e The browser event.
* @private
*/
Blockly.tree.TreeControl.prototype.handleMouseEvent_ = function(e) {
var node = this.getNodeFromEvent_(e);
if (node && e.type == 'click') {
node.onClick_(e);
}
};
/**
* Handles key down on the tree.
* @param {!Event} e The browser event.
* @return {boolean} The handled value.
* @private
*/
Blockly.tree.TreeControl.prototype.handleKeyEvent_ = function(e) {
// Handle navigation keystrokes.
var handled = !!(this.selectedItem_ && this.selectedItem_.onKeyDown(e));
if (handled) {
Blockly.utils.style.scrollIntoContainerView(
/** @type {!Element} */ (this.selectedItem_.getElement()),
/** @type {!Element} */ (this.getElement().parentNode));
e.preventDefault();
}
return handled;
};
/**
* Finds the containing node given an event.
* @param {!Event} e The browser event.
* @return {Blockly.tree.BaseNode} The containing node or null if no node is
* found.
* @private
*/
Blockly.tree.TreeControl.prototype.getNodeFromEvent_ = function(e) {
// find the right node
var node = null;
var target = e.target;
while (target) {
var id = target.id;
node = Blockly.tree.BaseNode.allNodes[id];
if (node) {
return node;
}
if (target == this.getElement()) {
break;
}
// Don't bubble if we hit a group. See issue #714.
if (target.getAttribute('role') == Blockly.utils.aria.Role.GROUP) {
return null;
}
target = target.parentNode;
}
return null;
};
/**
* Creates a new tree node using the same config as the root.
* @param {string=} opt_content The content of the node label.
* @return {!Blockly.tree.TreeNode} The new item.
* @package
*/
Blockly.tree.TreeControl.prototype.createNode = function(opt_content) {
return new Blockly.tree.TreeNode(
this.toolbox_, opt_content || '', this.config_);
};
-172
View File
@@ -1,172 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Definition of the Blockly.tree.TreeNode class.
* This class is similar to Closure's goog.ui.tree.TreeNode class.
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.tree.TreeNode');
goog.require('Blockly.tree.BaseNode');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.KeyCodes');
/**
* A single node in the tree, customized for Blockly's UI.
* Similar to Closure's goog.ui.tree.TreeNode
*
* @param {Blockly.Toolbox} toolbox The parent toolbox for this tree.
* @param {string} content The content of the node label treated as
* plain-text and will be HTML escaped.
* @param {!Blockly.tree.BaseNode.Config} config The configuration for the tree.
* @constructor
* @extends {Blockly.tree.BaseNode}
*/
Blockly.tree.TreeNode = function(toolbox, content, config) {
this.toolbox_ = toolbox;
Blockly.tree.BaseNode.call(this, content, config);
/**
* A handler that's triggered when the size of node has changed.
* @type {?function():?}
* @private
*/
this.onSizeChanged_ = null;
};
Blockly.utils.object.inherits(Blockly.tree.TreeNode, Blockly.tree.BaseNode);
/**
* Returns the tree.
* @return {?Blockly.tree.TreeControl} The tree.
* @override
*/
Blockly.tree.TreeNode.prototype.getTree = function() {
if (this.tree) {
return this.tree;
}
var parent = this.getParent();
if (parent) {
var tree = parent.getTree();
if (tree) {
this.setTreeInternal(tree);
return tree;
}
}
return null;
};
/**
* Returns the source for the icon.
* @return {string} Src for the icon.
* @override
*/
Blockly.tree.TreeNode.prototype.getCalculatedIconClass = function() {
var expanded = this.expanded_;
if (expanded && this.expandedIconClass) {
return this.expandedIconClass;
}
var iconClass = this.iconClass;
if (!expanded && iconClass) {
return iconClass;
}
// fall back on default icons
var config = this.config_;
if (this.hasChildren()) {
if (expanded && config.cssExpandedFolderIcon) {
return config.cssTreeIcon + ' ' + config.cssExpandedFolderIcon;
} else if (!expanded && config.cssCollapsedFolderIcon) {
return config.cssTreeIcon + ' ' + config.cssCollapsedFolderIcon;
}
} else {
if (config.cssFileIcon) {
return config.cssTreeIcon + ' ' + config.cssFileIcon;
}
}
return '';
};
/**
* Expand or collapse the node on mouse click.
* @param {!Event} _e The browser event.
* @override
*/
Blockly.tree.TreeNode.prototype.onClick_ = function(_e) {
// Expand icon.
if (this.hasChildren()) {
this.toggle();
this.select();
} else if (this.isSelected()) {
this.getTree().setSelectedItem(null);
} else {
this.select();
}
this.updateRow();
};
/**
* Remap event.keyCode in horizontalLayout so that arrow
* keys work properly and call original onKeyDown handler.
* @param {!Event} e The browser event.
* @return {boolean} The handled value.
* @override
* @private
*/
Blockly.tree.TreeNode.prototype.onKeyDown = function(e) {
if (this.tree.toolbox_.horizontalLayout_) {
var map = {};
var next = Blockly.utils.KeyCodes.DOWN;
var prev = Blockly.utils.KeyCodes.UP;
map[Blockly.utils.KeyCodes.RIGHT] = this.rightToLeft_ ? prev : next;
map[Blockly.utils.KeyCodes.LEFT] = this.rightToLeft_ ? next : prev;
map[Blockly.utils.KeyCodes.UP] = Blockly.utils.KeyCodes.LEFT;
map[Blockly.utils.KeyCodes.DOWN] = Blockly.utils.KeyCodes.RIGHT;
var newKeyCode = map[e.keyCode];
Object.defineProperties(e, {
keyCode: {value: newKeyCode || e.keyCode}
});
}
return Blockly.tree.TreeNode.superClass_.onKeyDown.call(this, e);
};
/**
* Set the handler that's triggered when the size of node has changed.
* @param {function():?} fn The handler
* @package
*/
Blockly.tree.TreeNode.prototype.onSizeChanged = function(fn) {
this.onSizeChanged_ = fn;
};
/**
* Trigger a size changed event if a handler exists.
* @private
*/
Blockly.tree.TreeNode.prototype.resizeToolbox_ = function() {
if (this.onSizeChanged_) {
this.onSizeChanged_.call(this.toolbox_);
}
};
/**
* Resize the toolbox when a node is expanded.
* @override
*/
Blockly.tree.TreeNode.prototype.doNodeExpanded =
Blockly.tree.TreeNode.prototype.resizeToolbox_;
/**
* Resize the toolbox when a node is collapsed.
* @override
*/
Blockly.tree.TreeNode.prototype.doNodeCollapsed =
Blockly.tree.TreeNode.prototype.resizeToolbox_;
+53 -30
View File
@@ -26,6 +26,7 @@ goog.require('Blockly.Touch');
goog.require('Blockly.utils');
goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.toolbox');
goog.require('Blockly.WorkspaceSvg');
goog.require('Blockly.Xml');
@@ -457,10 +458,9 @@ Blockly.Flyout.prototype.hide = function() {
/**
* Show and populate the flyout.
* @param {!Blockly.utils.toolbox.ToolboxDefinition|string} flyoutDef
* List of contents to display in the flyout as an array of xml an
* array of Nodes, a NodeList or a string with the name of the dynamic category.
* Variables and procedures have a custom set of blocks.
* @param {!Blockly.utils.toolbox.FlyoutDefinition|string} flyoutDef Contents to display
* in the flyout. This is either an array of Nodes, a NodeList, a
* toolbox definition, or a string with the name of the dynamic category.
*/
Blockly.Flyout.prototype.show = function(flyoutDef) {
this.workspace_.setResizesEnabled(false);
@@ -468,24 +468,13 @@ Blockly.Flyout.prototype.show = function(flyoutDef) {
this.clearOldBlocks_();
// Handle dynamic categories, represented by a name instead of a list.
// Look up the correct category generation function and call that to get a
// valid XML list.
if (typeof flyoutDef == 'string') {
var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback(
flyoutDef);
if (typeof fnToApply != 'function') {
throw TypeError('Couldn\'t find a callback function when opening' +
' a toolbox category.');
}
flyoutDef = fnToApply(this.workspace_.targetWorkspace);
if (!Array.isArray(flyoutDef)) {
throw TypeError('Result of toolbox category callback must be an array.');
}
flyoutDef = this.getDynamicCategoryContents_(flyoutDef);
}
this.setVisible(true);
// Parse the Array or NodeList passed in into an Array of
// Blockly.utils.toolbox.Toolbox.
var parsedContent = Blockly.utils.toolbox.convertToolboxToJSON(flyoutDef);
// Parse the Array, Node or NodeList into a a list of flyout items.
var parsedContent = Blockly.utils.toolbox.convertFlyoutDefToJsonArray(flyoutDef);
var flyoutInfo =
/** @type {{contents:!Array.<!Object>, gaps:!Array.<number>}} */ (
this.createFlyoutInfo_(parsedContent));
@@ -524,10 +513,10 @@ Blockly.Flyout.prototype.show = function(flyoutDef) {
/**
* Create the contents array and gaps array necessary to create the layout for
* the flyout.
* @param {Array.<Blockly.utils.toolbox.Toolbox>} parsedContent The array
* of objects to show in the flyout.
* @param {!Blockly.utils.toolbox.FlyoutItemInfoArray} parsedContent The array
* of objects to show in the flyout.
* @return {{contents:Array.<Object>, gaps:Array.<number>}} The list of contents
* and gaps needed to lay out the flyout.
* and gaps needed to lay out the flyout.
* @private
*/
Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
@@ -536,9 +525,20 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
this.permanentlyDisabled_.length = 0;
var defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
for (var i = 0, contentInfo; (contentInfo = parsedContent[i]); i++) {
if (contentInfo['custom']) {
var customInfo = /** @type {!Blockly.utils.toolbox.DynamicCategoryInfo} */ (contentInfo);
var categoryName = customInfo['custom'];
var flyoutDef = this.getDynamicCategoryContents_(categoryName);
var parsedDynamicContent = /** @type {!Blockly.utils.toolbox.FlyoutItemInfoArray} */
(Blockly.utils.toolbox.convertFlyoutDefToJsonArray(flyoutDef));
parsedContent.splice.apply(parsedContent, [i, 1].concat(parsedDynamicContent));
contentInfo = parsedContent[i];
}
switch (contentInfo['kind'].toUpperCase()) {
case 'BLOCK':
var blockInfo = /** @type {Blockly.utils.toolbox.Block} */ (contentInfo);
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.
@@ -548,19 +548,18 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
contents.push({type: 'block', block: block});
break;
case 'SEP':
var sepInfo = /** @type {Blockly.utils.toolbox.Separator} */ (contentInfo);
var sepInfo = /** @type {!Blockly.utils.toolbox.SeparatorInfo} */ (contentInfo);
this.addSeparatorGap_(sepInfo, gaps, defaultGap);
break;
case 'LABEL':
var labelInfo = /** @type {Blockly.utils.toolbox.Label} */ (contentInfo);
var labelInfo = /** @type {!Blockly.utils.toolbox.LabelInfo} */ (contentInfo);
// A label is a button with different styling.
// Rename this function.
var label = this.createButton_(labelInfo, /** isLabel */ true);
contents.push({type: 'button', button: label});
gaps.push(defaultGap);
break;
case 'BUTTON':
var buttonInfo = /** @type {Blockly.utils.toolbox.Button} */ (contentInfo);
var buttonInfo = /** @type {!Blockly.utils.toolbox.ButtonInfo} */ (contentInfo);
var button = this.createButton_(buttonInfo, /** isLabel */ false);
contents.push({type: 'button', button: button});
gaps.push(defaultGap);
@@ -570,9 +569,31 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
return {contents: contents, gaps: gaps};
};
/**
* 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.
* @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);
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;
};
/**
* Creates a flyout button or a flyout label.
* @param {!Blockly.utils.toolbox.Button|!Blockly.utils.toolbox.Label} btnInfo
* @param {!Blockly.utils.toolbox.ButtonOrLabelInfo} btnInfo
* The object holding information about a button or a label.
* @param {boolean} isLabel True if the button is a label, false otherwise.
* @return {!Blockly.FlyoutButton} The object used to display the button in the
@@ -609,7 +630,7 @@ Blockly.Flyout.prototype.createBlock_ = function(blockXml) {
/**
* Get the xml from the block info object.
* @param {!Blockly.utils.toolbox.Block} blockInfo The object holding
* @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.
@@ -623,10 +644,12 @@ Blockly.Flyout.prototype.getBlockXml_ = function(blockInfo) {
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;
}
if (!blockElement) {
@@ -637,7 +660,7 @@ Blockly.Flyout.prototype.getBlockXml_ = function(blockInfo) {
/**
* Add the necessary gap in the flyout for a separator.
* @param {!Blockly.utils.toolbox.Separator} sepInfo The object holding
* @param {!Blockly.utils.toolbox.SeparatorInfo} sepInfo The object holding
* information about a separator.
* @param {!Array.<number>} gaps The list gaps between items in the flyout.
* @param {number} defaultGap The default gap between the button and next element.
+16 -2
View File
@@ -23,7 +23,7 @@ goog.require('Blockly.utils.dom');
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!Blockly.utils.toolbox.Button|!Blockly.utils.toolbox.Label} json
* @param {!Blockly.utils.toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @constructor
@@ -88,7 +88,7 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, json, isLabel) {
/**
* The JSON specifying the label / button.
* @type {!Blockly.utils.toolbox.Button|!Blockly.utils.toolbox.Label}
* @type {!Blockly.utils.toolbox.ButtonOrLabelInfo}
*/
this.info = json;
};
@@ -227,6 +227,13 @@ Blockly.FlyoutButton.prototype.moveTo = function(x, y) {
this.updateTransform_();
};
/**
* @return {boolean} Whether or not the button is a label.
*/
Blockly.FlyoutButton.prototype.isLabel = function() {
return this.isLabel_;
};
/**
* Location of the button.
* @return {!Blockly.utils.Coordinate} x, y coordinates.
@@ -236,6 +243,13 @@ Blockly.FlyoutButton.prototype.getPosition = function() {
return this.position_;
};
/**
* @return {string} Text of the button.
*/
Blockly.FlyoutButton.prototype.getButtonText = function() {
return this.text_;
};
/**
* Get the button's target workspace.
* @return {!Blockly.WorkspaceSvg} The target workspace of the flyout where this
+8 -4
View File
@@ -13,7 +13,12 @@
goog.provide('Blockly.IFlyout');
goog.requireType('Blockly.BlockSvg');
goog.requireType('Blockly.IRegistrable');
goog.requireType('Blockly.utils.dom');
goog.requireType('Blockly.utils.Coordinate');
goog.requireType('Blockly.utils.toolbox');
goog.requireType('Blockly.WorkspaceSvg');
/**
@@ -130,10 +135,9 @@ Blockly.IFlyout.prototype.hide;
/**
* Show and populate the flyout.
* @param {!Blockly.utils.toolbox.ToolboxDefinition|string} flyoutDef
* List of contents to display in the flyout as an array of xml an
* array of Nodes, a NodeList or a string with the name of the dynamic category.
* Variables and procedures have a custom set of blocks.
* @param {!Blockly.utils.toolbox.FlyoutDefinition|string} flyoutDef Contents to
* display in the flyout. This is either an array of Nodes, a NodeList, a
* toolbox definition, or a string with the name of the dynamic category.
*/
Blockly.IFlyout.prototype.show;
+51 -20
View File
@@ -15,6 +15,9 @@ goog.provide('Blockly.IToolbox');
goog.requireType('Blockly.IFlyout');
goog.requireType('Blockly.IRegistrable');
goog.requireType('Blockly.IToolboxItem');
goog.requireType('Blockly.utils.toolbox');
goog.requireType('Blockly.WorkspaceSvg');
/**
@@ -31,44 +34,58 @@ Blockly.IToolbox = function() {};
Blockly.IToolbox.prototype.init;
/**
* Fill the toolbox with categories and blocks.
* @param {Array.<Blockly.utils.toolbox.Toolbox>} toolboxDef Array holding objects
* containing information on the contents of the toolbox.
* Fills the toolbox with new toolbox items and removes any old contents.
* @param {!Blockly.utils.toolbox.ToolboxInfo} toolboxDef Object holding information
* for creating a toolbox.
*/
Blockly.IToolbox.prototype.render;
/**
* Dispose of this toolbox.
* @return {void}
*/
Blockly.IToolbox.prototype.dispose;
/**
* Get the width of the toolbox.
* Gets the width of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.IToolbox.prototype.getWidth;
/**
* Get the height of the toolbox.
* Gets the height of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.IToolbox.prototype.getHeight;
/**
* Get the toolbox flyout.
* @return {Blockly.IFlyout} The toolbox flyout.
* Gets the toolbox flyout.
* @return {?Blockly.IFlyout} The toolbox flyout.
*/
Blockly.IToolbox.prototype.getFlyout;
/**
* Move the toolbox to the edge.
* Gets the workspace for the toolbox.
* @return {!Blockly.WorkspaceSvg} The parent workspace for the toolbox.
*/
Blockly.IToolbox.prototype.getWorkspace;
/**
* Gets whether or not the toolbox is horizontal.
* @return {boolean} True if the toolbox is horizontal, false if the toolbox is
* vertical.
*/
Blockly.IToolbox.prototype.isHorizontal;
/**
* Positions the toolbox based on whether it is a horizontal toolbox and whether
* the workspace is in rtl.
* @return {void}
*/
Blockly.IToolbox.prototype.position;
/**
* Unhighlight any previously specified option.
* Handles resizing the toolbox when a toolbox item resizes.
* @return {void}
*/
Blockly.IToolbox.prototype.handleToolboxItemResize;
/**
* Unhighlights any previously selected item.
* @return {void}
*/
Blockly.IToolbox.prototype.clearSelection;
@@ -80,7 +97,7 @@ Blockly.IToolbox.prototype.clearSelection;
Blockly.IToolbox.prototype.refreshTheme;
/**
* Update the flyout's contents without closing it. Should be used in response
* Updates the flyout's content without closing it. Should be used in response
* to a change in one of the dynamic categories, such as variables or
* procedures.
* @return {void}
@@ -88,13 +105,27 @@ Blockly.IToolbox.prototype.refreshTheme;
Blockly.IToolbox.prototype.refreshSelection;
/**
* Toggles the visibility of the toolbox.
* @param {boolean} isVisible True if the toolbox should be visible.
* Sets the visibility of the toolbox.
* @param {boolean} isVisible True if toolbox should be visible.
*/
Blockly.IToolbox.prototype.setVisible;
/**
* Select the first toolbox category if no category is selected.
* Selects the toolbox item by it's position in the list of toolbox items.
* @param {number} position The position of the item to select.
* @return {void}
*/
Blockly.IToolbox.prototype.selectFirstCategory;
Blockly.IToolbox.prototype.selectItemByPosition;
/**
* Gets the selected item.
* @return {?Blockly.IToolboxItem} The selected item, or null if no item is
* currently selected.
*/
Blockly.IToolbox.prototype.getSelectedItem;
/**
* Disposes of this toolbox.
* @return {void}
*/
Blockly.IToolbox.prototype.dispose;
+148
View File
@@ -0,0 +1,148 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview The interface for a toolbox item.
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
goog.provide('Blockly.ICollapsibleToolboxItem');
goog.provide('Blockly.ISelectableToolboxItem');
goog.provide('Blockly.IToolboxItem');
goog.requireType('Blockly.utils.toolbox');
/**
* Interface for an item in the toolbox.
* @interface
*/
Blockly.IToolboxItem = function() {};
/**
* Initializes the toolbox item.
* This includes creating the dom and updating the state of any items based
* on the info object.
* @return {void}
* @public
*/
Blockly.IToolboxItem.prototype.init;
/**
* Gets the div for the toolbox item.
* @return {?Element} The div for the toolbox item.
* @public
*/
Blockly.IToolboxItem.prototype.getDiv;
/**
* Gets a unique identifier for this toolbox item.
* @return {string} The id for the toolbox item.
* @public
*/
Blockly.IToolboxItem.prototype.getId;
/**
* Gets the parent if the toolbox item is nested.
* @return {?Blockly.IToolboxItem} The parent toolbox item, or null if
* this toolbox item is not nested.
* @public
*/
Blockly.IToolboxItem.prototype.getParent;
/**
* Gets the nested level of the category.
* @return {number} The nested level of the category.
* @package
*/
Blockly.IToolboxItem.prototype.getLevel;
/**
* Whether the toolbox item is selectable.
* @return {boolean} True if the toolbox item can be selected.
* @public
*/
Blockly.IToolboxItem.prototype.isSelectable;
/**
* Whether the toolbox item is collapsible.
* @return {boolean} True if the toolbox item is collapsible.
* @public
*/
Blockly.IToolboxItem.prototype.isCollapsible;
/**
* Dispose of this toolbox item. No-op by default.
* @public
*/
Blockly.IToolboxItem.prototype.dispose;
/**
* Interface for an item in the toolbox that can be selected.
* @extends {Blockly.IToolboxItem}
* @interface
*/
Blockly.ISelectableToolboxItem = function() {};
/**
* Gets the name of the toolbox item. Used for emitting events.
* @return {string} The name of the toolbox item.
* @public
*/
Blockly.ISelectableToolboxItem.prototype.getName;
/**
* Gets the contents of the toolbox item. These are items that are meant to be
* displayed in the flyout.
* @return {!Blockly.utils.toolbox.FlyoutItemInfoArray|string} The definition
* of items to be displayed in the flyout.
* @public
*/
Blockly.ISelectableToolboxItem.prototype.getContents;
/**
* Sets the current toolbox item as selected.
* @param {boolean} _isSelected True if this category is selected, false
* otherwise.
* @public
*/
Blockly.ISelectableToolboxItem.prototype.setSelected;
/**
* Handles when the toolbox item is clicked.
* @param {!Event} _e Click event to handle.
* @public
*/
Blockly.ISelectableToolboxItem.prototype.onClick;
/**
* Interface for an item in the toolbox that can be collapsed.
* @extends {Blockly.ISelectableToolboxItem}
* @interface
*/
Blockly.ICollapsibleToolboxItem = function() {};
/**
* Gets any children toolbox items. (ex. Gets the subcategories)
* @return {!Array<!Blockly.IToolboxItem>} The child toolbox items.
*/
Blockly.ICollapsibleToolboxItem.prototype.getChildToolboxItems;
/**
* Whether the toolbox item is expanded to show its child subcategories.
* @return {boolean} True if the toolbox item shows its children, false if it
* is collapsed.
* @public
*/
Blockly.ICollapsibleToolboxItem.prototype.isExpanded;
/**
* Toggles whether or not the toolbox item is expanded.
* @public
*/
Blockly.ICollapsibleToolboxItem.prototype.toggleExpanded;
+3 -1
View File
@@ -131,7 +131,9 @@ Blockly.navigation.focusToolbox_ = function() {
if (!Blockly.navigation.getMarker().getCurNode()) {
Blockly.navigation.markAtCursor_();
}
toolbox.selectFirstCategory();
if (!toolbox.getSelectedItem()) {
toolbox.selectItemByPosition(0);
}
}
};
+2 -1
View File
@@ -28,6 +28,7 @@ goog.require('Blockly.WorkspaceSvg');
goog.require('Blockly.Xml');
goog.requireType('Blockly.utils.Metrics');
goog.requireType('Blockly.utils.toolbox');
/**
@@ -173,7 +174,7 @@ Blockly.Mutator.prototype.createEditor_ = function() {
var hasFlyout = !!quarkXml;
if (hasFlyout) {
workspaceOptions.languageTree =
Blockly.utils.toolbox.convertToolboxToJSON(quarkXml);
Blockly.utils.toolbox.convertToolboxDefToJson(quarkXml);
workspaceOptions.getMetrics = this.getFlyoutMetrics_.bind(this);
}
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
+5 -40
View File
@@ -33,7 +33,7 @@ goog.require('Blockly.Xml');
Blockly.Options = function(options) {
var readOnly = !!options['readOnly'];
if (readOnly) {
var toolboxContents = null;
var toolboxJsonDef = null;
var hasCategories = false;
var hasTrashcan = false;
var hasCollapse = false;
@@ -41,12 +41,8 @@ Blockly.Options = function(options) {
var hasDisable = false;
var hasSounds = false;
} else {
var toolboxDef = options['toolbox'];
if (!Array.isArray(toolboxDef)) {
toolboxDef = Blockly.Options.parseToolboxTree(toolboxDef || null);
}
var toolboxContents = Blockly.utils.toolbox.convertToolboxToJSON(toolboxDef);
var hasCategories = Blockly.utils.toolbox.hasCategories(toolboxContents);
var toolboxJsonDef = Blockly.utils.toolbox.convertToolboxDefToJson(options['toolbox']);
var hasCategories = Blockly.utils.toolbox.hasCategories(toolboxJsonDef);
var hasTrashcan = options['trashcan'];
if (hasTrashcan === undefined) {
hasTrashcan = hasCategories;
@@ -148,8 +144,8 @@ Blockly.Options = function(options) {
this.hasCss = hasCss;
/** @type {boolean} */
this.horizontalLayout = horizontalLayout;
/** @type {Array.<Blockly.utils.toolbox.Toolbox>} */
this.languageTree = toolboxContents;
/** @type {?Blockly.utils.toolbox.ToolboxInfo} */
this.languageTree = toolboxJsonDef;
/** @type {!Blockly.Options.GridOptions} */
this.gridOptions = Blockly.Options.parseGridOptions_(options);
/** @type {!Blockly.Options.ZoomOptions} */
@@ -362,34 +358,3 @@ Blockly.Options.parseThemeOptions_ = function(options) {
return Blockly.Theme.defineTheme(theme.name ||
('builtin' + Blockly.utils.IdGenerator.getNextUniqueId()), theme);
};
/**
* Parse the provided toolbox tree into a consistent DOM format.
* @param {Node|NodeList|?string} tree DOM tree of blocks, or text representation
* of same.
* @return {Node} DOM tree of blocks, or null.
*/
Blockly.Options.parseToolboxTree = function(tree) {
if (tree) {
if (typeof tree != 'string') {
if (Blockly.utils.userAgent.IE && tree.outerHTML) {
// In this case the tree will not have been properly built by the
// browser. The HTML will be contained in the element, but it will
// not have the proper DOM structure since the browser doesn't support
// XSLTProcessor (XML -> HTML).
tree = tree.outerHTML;
} else if (!(tree instanceof Element)) {
tree = null;
}
}
if (typeof tree == 'string') {
tree = Blockly.Xml.textToDom(tree);
if (tree.nodeName.toLowerCase() != 'xml') {
throw TypeError('Toolbox should be an <xml> document.');
}
}
} else {
tree = null;
}
return tree;
};
+35 -11
View File
@@ -20,7 +20,6 @@ goog.requireType('Blockly.IConnectionChecker');
goog.requireType('Blockly.IFlyout');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.Theme');
goog.requireType('Blockly.utils.toolbox');
/**
@@ -80,6 +79,9 @@ Blockly.registry.Type.TOOLBOX = new Blockly.registry.Type('toolbox');
/** @type {!Blockly.registry.Type<Blockly.Theme>} */
Blockly.registry.Type.THEME = new Blockly.registry.Type('theme');
/** @type {!Blockly.registry.Type<Blockly.ToolboxItem>} */
Blockly.registry.Type.TOOLBOX_ITEM = new Blockly.registry.Type('toolboxItem');
/** @type {!Blockly.registry.Type<Blockly.IFlyout>} */
Blockly.registry.Type.FLYOUTS_VERTICAL_TOOLBOX =
new Blockly.registry.Type('flyoutsVerticalToolbox');
@@ -90,16 +92,18 @@ Blockly.registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX =
/**
* Registers a class based on a type and name.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @param {?function(new:T, ...?)|Object} registryItem The class or object to
* register.
* @param {boolean=} opt_quiet True to prevent an error when overriding an
* already registered item.
* @throws {Error} if the type or name is empty, a name with the given type has
* already been registered, or if the given class or object is not valid for it's type.
* @template T
*/
Blockly.registry.register = function(type, name, registryItem) {
Blockly.registry.register = function(type, name, registryItem, opt_quiet) {
if ((!(type instanceof Blockly.registry.Type) && typeof type != 'string') || String(type).trim() == '') {
throw Error('Invalid type "' + type + '". The type must be a' +
' non-empty string or a Blockly.registry.Type.');
@@ -123,8 +127,8 @@ Blockly.registry.register = function(type, name, registryItem) {
// Validate that the given class has all the required properties.
Blockly.registry.validate_(type, registryItem);
// If the name already exists throw an error.
if (typeRegistry[name]) {
// Don't throw an error if opt_quiet is true.
if (!opt_quiet && typeRegistry[name]) {
throw Error('Name "' + name + '" with type "' + type + '" already registered.');
}
typeRegistry[name] = registryItem;
@@ -150,7 +154,7 @@ Blockly.registry.validate_ = function(type, registryItem) {
/**
* Unregisters the registry item with the given type and name.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @template T
@@ -172,8 +176,8 @@ Blockly.registry.unregister = function(type, name) {
/**
* Gets the registry item for the given name and type. This can be either a
* class or an object.l
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* class or an object.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @return {?function(new:T, ...?)|Object} The class or object with the given
@@ -195,9 +199,29 @@ Blockly.registry.getItem_ = function(type, name) {
return typeRegistry[name];
};
/**
* Returns whether or not the registry contains an item with the given type and
* name.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @return {boolean} True if the registry has an item with the given type and
* name, false otherwise.
* @template T
*/
Blockly.registry.hasItem = function(type, name) {
type = String(type).toLowerCase();
name = name.toLowerCase();
var typeRegistry = Blockly.registry.typeMap_[type];
if (!typeRegistry) {
return false;
}
return !!(typeRegistry[name]);
};
/**
* Gets the class for the given name and type.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Field, Renderer)
* @param {string} name The plugin's name. (Ex. field_angle, geras)
* @return {?function(new:T, ...?)} The class with the given name and type or
@@ -210,7 +234,7 @@ Blockly.registry.getClass = function(type, name) {
/**
* Gets the object for the given name and type.
* @param {string|Blockly.registry.Type<T>} type The type of the plugin.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Category)
* @param {string} name The plugin's name. (Ex. logic_category)
* @returns {T} The object with the given name and type or null if none exists.
@@ -223,7 +247,7 @@ Blockly.registry.getObject = function(type, name) {
/**
* Gets the class from Blockly options for the given type.
* This is used for plugins that override a built in feature. (e.g. Toolbox)
* @param {Blockly.registry.Type<T>} type The type of the plugin.
* @param {!Blockly.registry.Type<T>} type The type of the plugin.
* @param {!Blockly.Options} options The option object to check for the given
* plugin.
* @return {?function(new:T, ...?)} The class for the plugin.
-943
View File
@@ -1,943 +0,0 @@
/**
* @license
* Copyright 2011 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Toolbox from whence to create blocks.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Toolbox');
goog.require('Blockly.Css');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.navigation');
goog.require('Blockly.registry');
goog.require('Blockly.Touch');
goog.require('Blockly.tree.TreeControl');
goog.require('Blockly.tree.TreeNode');
goog.require('Blockly.utils');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.colour');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.Rect');
goog.require('Blockly.utils.toolbox');
goog.requireType('Blockly.IBlocklyActionable');
goog.requireType('Blockly.IDeleteArea');
goog.requireType('Blockly.IFlyout');
goog.requireType('Blockly.IStyleable');
goog.requireType('Blockly.IToolbox');
/**
* Class for a Toolbox.
* Creates the toolbox's DOM.
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to create new
* blocks.
* @constructor
* @implements {Blockly.IBlocklyActionable}
* @implements {Blockly.IDeleteArea}
* @implements {Blockly.IStyleable}
* @implements {Blockly.IToolbox}
*/
Blockly.Toolbox = function(workspace) {
/**
* @type {!Blockly.WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = workspace.options.RTL;
/**
* Whether the toolbox should be laid out horizontally.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspace.options.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
*/
this.toolboxPosition = workspace.options.toolboxPosition;
/**
* Configuration constants for Closure's tree UI.
* @type {!Object.<string,*>}
* @private
*/
this.config_ = {
indentWidth: 19,
cssRoot: 'blocklyTreeRoot',
cssHideRoot: 'blocklyHidden',
cssTreeRow: 'blocklyTreeRow',
cssItemLabel: 'blocklyTreeLabel',
cssTreeIcon: 'blocklyTreeIcon',
cssExpandedFolderIcon: 'blocklyTreeIconOpen',
cssFileIcon: 'blocklyTreeIconNone',
cssSelectedRow: 'blocklyTreeSelected'
};
/**
* Configuration constants for tree separator.
* @type {!Object.<string,*>}
* @private
*/
this.treeSeparatorConfig_ = {
cssTreeRow: 'blocklyTreeSeparator'
};
if (this.horizontalLayout_) {
this.config_['cssTreeRow'] =
this.config_['cssTreeRow'] +
(workspace.RTL ?
' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
this.treeSeparatorConfig_['cssTreeRow'] =
'blocklyTreeSeparatorHorizontal ' +
(workspace.RTL ?
'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree');
this.config_['cssTreeIcon'] = '';
}
/**
* The toolbox flyout.
* @type {Blockly.IFlyout}
* @private
*/
this.flyout_ = null;
/**
* Width of the toolbox, which changes only in vertical layout.
* @type {number}
*/
this.width = 0;
/**
* Height of the toolbox, which changes only in horizontal layout.
* @type {number}
*/
this.height = 0;
/**
* The TreeNode most recently selected.
* @type {Blockly.tree.BaseNode}
* @private
*/
this.lastCategory_ = null;
};
/**
* Initializes the toolbox.
* @throws {Error} If missing a require for both `Blockly.HorizontalFlyout` and
* `Blockly.VerticalFlyout`.
*/
Blockly.Toolbox.prototype.init = function() {
var workspace = this.workspace_;
var svg = this.workspace_.getParentSvg();
/**
* HTML container for the Toolbox menu.
* @type {Element}
*/
this.HtmlDiv = document.createElement('div');
this.HtmlDiv.className = 'blocklyToolboxDiv blocklyNonSelectable';
this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR');
svg.parentNode.insertBefore(this.HtmlDiv, svg);
var themeManager = workspace.getThemeManager();
themeManager.subscribe(this.HtmlDiv, 'toolboxBackgroundColour',
'background-color');
themeManager.subscribe(this.HtmlDiv, 'toolboxForegroundColour', 'color');
// Clicking on toolbox closes popups.
Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
function(e) {
if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) {
// Close flyout.
Blockly.hideChaff(false);
} else {
// Just close popups.
Blockly.hideChaff(true);
}
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
}, /* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true);
var workspaceOptions = new Blockly.Options(
/** @type {!Blockly.BlocklyOptions} */
({
'parentWorkspace': workspace,
'rtl': workspace.RTL,
'oneBasedIndex': workspace.options.oneBasedIndex,
'horizontalLayout': workspace.horizontalLayout,
'renderer': workspace.options.renderer,
'rendererOverrides': workspace.options.rendererOverrides
}));
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;
var FlyoutClass = null;
if (workspace.horizontalLayout) {
FlyoutClass = Blockly.registry.getClassFromOptions(
Blockly.registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, workspace.options);
} else {
FlyoutClass = Blockly.registry.getClassFromOptions(
Blockly.registry.Type.FLYOUTS_VERTICAL_TOOLBOX, workspace.options);
}
if (!FlyoutClass) {
throw Error('Blockly.VerticalFlyout, Blockly.HorizontalFlyout or your own' +
' custom flyout must be required.');
}
this.flyout_ = new FlyoutClass(workspaceOptions);
// Insert the flyout after the workspace.
Blockly.utils.dom.insertAfter(
this.flyout_.createDom(Blockly.utils.dom.SvgElementType.SVG), svg);
this.flyout_.init(workspace);
this.config_['cssCollapsedFolderIcon'] =
'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr');
this.render(workspace.options.languageTree);
};
/**
* Fill the toolbox with categories and blocks.
* @param {Array.<Blockly.utils.toolbox.Toolbox>} toolboxDef Array holding objects
* containing information on the contents of the toolbox.
* @package
*/
Blockly.Toolbox.prototype.render = function(toolboxDef) {
if (this.tree_) {
this.tree_.dispose(); // Delete any existing content.
this.lastCategory_ = null;
}
var tree = new Blockly.tree.TreeControl(this,
/** @type {!Blockly.tree.BaseNode.Config} */ (this.config_));
this.tree_ = tree;
tree.setSelectedItem(null);
tree.onBeforeSelected(this.handleBeforeTreeSelected_);
tree.onAfterSelected(this.handleAfterTreeSelected_);
var openNode = null;
if (toolboxDef) {
this.tree_.contents = [];
this.hasColours_ = false;
openNode = this.createTree_(toolboxDef, this.tree_);
if (this.tree_.contents.length) {
throw Error('Toolbox cannot have both blocks and categories ' +
'in the root level.');
}
// Fire a resize event since the toolbox may have changed width and height.
this.workspace_.resizeContents();
}
tree.render(this.HtmlDiv);
if (openNode) {
tree.setSelectedItem(openNode);
}
this.addColour_();
this.position();
// Trees have an implicit orientation of vertical, so we only need to set this
// when the toolbox is in horizontal mode.
if (this.horizontalLayout_) {
Blockly.utils.aria.setState(
/** @type {!Element} */ (this.tree_.getElement()),
Blockly.utils.aria.State.ORIENTATION, 'horizontal');
}
};
/**
* Create the toolbox tree.
* @param {Array.<Blockly.utils.toolbox.Toolbox>} toolboxDef List of objects
* holding information on toolbox contents.
* @param {!Blockly.tree.BaseNode} treeOut The output tree for the toolbox. Due
* to the recursive nature of this function, treeOut can be either the root of
* the tree (Blockly.tree.TreeControl) or a child node of the tree
* (Blockly.tree.TreeNode). These nodes are built from the toolboxDef.
* @return {Blockly.tree.BaseNode} The TreeNode to expand when the toolbox is
* first loaded (or null).
* @private
*/
Blockly.Toolbox.prototype.createTree_ = function(toolboxDef, treeOut) {
var openNode = null;
var lastElement = null;
if (!toolboxDef) {
return null;
}
for (var i = 0, childIn; (childIn = toolboxDef[i]); i++) {
switch (childIn['kind'].toUpperCase()) {
case 'CATEGORY':
var categoryInfo = /** @type {Blockly.utils.toolbox.Category} */ (childIn);
openNode = this.addCategory_(categoryInfo, treeOut) || openNode;
lastElement = childIn;
break;
case 'SEP':
var separatorInfo = /** @type {Blockly.utils.toolbox.Separator} */ (childIn);
lastElement = this.addSeparator_(separatorInfo, treeOut, lastElement) || lastElement;
break;
case 'BLOCK':
case 'SHADOW':
case 'LABEL':
case 'BUTTON':
treeOut.contents.push(childIn);
lastElement = childIn;
break;
}
}
return openNode;
};
/**
* Add a category to the toolbox tree.
* @param {!Blockly.utils.toolbox.Category} categoryInfo The object holding
* information on the category.
* @param {!Blockly.tree.BaseNode} treeOut The TreeControl or TreeNode
* object built from the childNodes.
* @return {Blockly.tree.BaseNode} TreeNode to open at startup (or null).
* @private
*/
Blockly.Toolbox.prototype.addCategory_ = function(categoryInfo, treeOut) {
var openNode = null;
// Decode the category name for any potential message references
// (eg. `%{BKY_CATEGORY_NAME_LOGIC}`).
var categoryName = Blockly.utils.replaceMessageReferences(categoryInfo['name']);
// Create and add the tree node for the category.
var childOut = this.tree_.createNode(categoryName);
childOut.onSizeChanged(this.handleNodeSizeChanged_);
childOut.contents = [];
treeOut.add(childOut);
var custom = categoryInfo['custom'];
if (custom) {
// Variables and procedures are special dynamic categories.
childOut.contents = custom;
} else {
openNode = this.createTree_(categoryInfo['contents'], childOut) || openNode;
}
this.setColourOrStyle_(categoryInfo, childOut, categoryName);
openNode = this.setExpanded_(categoryInfo, childOut) || openNode;
return openNode;
};
/**
* Add either the colour or the style for a category.
* @param {!Blockly.utils.toolbox.Category} categoryInfo The object holding
* information on the category.
* @param {!Blockly.tree.TreeNode} childOut The TreeNode for a category.
* @param {string} categoryName The name of the category.
* @private
*/
Blockly.Toolbox.prototype.setColourOrStyle_ = function(
categoryInfo, childOut, categoryName) {
var styleName = categoryInfo['categorystyle'];
var colour = categoryInfo['colour'];
if (colour && styleName) {
childOut.hexColour = '';
console.warn('Toolbox category "' + categoryName +
'" must not have both a style and a colour');
} else if (styleName) {
this.setColourFromStyle_(styleName, childOut, categoryName);
} else {
this.setColour_(colour, childOut, categoryName);
}
};
/**
* Add a separator to the toolbox tree if it is between categories. Otherwise,
* add the separator to the list of contents.
* @param {!Blockly.utils.toolbox.Separator} separatorInfo The object holding
* information on the separator.
* @param {!Blockly.tree.BaseNode} treeOut The TreeControl or TreeNode
* object built from the childNodes.
* @param {Object} lastElement The last element to be added to the tree.
* @return {Object} The last element to be added to the tree, or
* null.
* @private
*/
Blockly.Toolbox.prototype.addSeparator_ = function(
separatorInfo, treeOut, lastElement) {
if (lastElement && lastElement['kind'].toUpperCase() == 'CATEGORY') {
// Separator between two categories.
// <sep></sep>
treeOut.add(new Blockly.Toolbox.TreeSeparator(
/** @type {!Blockly.tree.BaseNode.Config} */
(this.treeSeparatorConfig_)));
} else {
// Otherwise add to contents array.
treeOut.contents.push(separatorInfo);
return separatorInfo;
}
return null;
};
/**
* Checks whether a node should be expanded, and expands if necessary.
* @param {!Blockly.utils.toolbox.Category} categoryInfo The child to expand.
* @param {!Blockly.tree.TreeNode} childOut The TreeNode created from childIn.
* @return {Blockly.tree.BaseNode} TreeNode to open at startup (or null).
* @private
*/
Blockly.Toolbox.prototype.setExpanded_ = function(categoryInfo, childOut) {
var openNode = null;
if (categoryInfo['expanded'] == 'true') {
if (childOut.contents.length) {
// This is a category that directly contains blocks.
// After the tree is rendered, open this category and show flyout.
openNode = childOut;
}
childOut.setExpanded(true);
} else {
childOut.setExpanded(false);
}
return openNode;
};
/**
* Handle the before tree item selected action.
* @param {Blockly.tree.BaseNode} node The newly selected node.
* @return {boolean} Whether or not to cancel selecting the node.
* @private
*/
Blockly.Toolbox.prototype.handleBeforeTreeSelected_ = function(node) {
if (node == this.tree_) {
return false;
}
if (this.lastCategory_) {
this.lastCategory_.getRowElement().style.backgroundColor = '';
}
if (node) {
var hexColour = node.hexColour || '#57e';
node.getRowElement().style.backgroundColor = hexColour;
// Add colours to child nodes which may have been collapsed and thus
// not rendered.
this.addColour_(node);
}
return true;
};
/**
* Handle the after tree item selected action.
* @param {Blockly.tree.BaseNode} oldNode The previously selected node.
* @param {Blockly.tree.BaseNode} newNode The newly selected node.
* @private
*/
Blockly.Toolbox.prototype.handleAfterTreeSelected_ = function(
oldNode, newNode) {
if (newNode && newNode.contents && newNode.contents.length) {
this.flyout_.show(newNode.contents);
// Scroll the flyout to the top if the category has changed.
if (this.lastCategory_ != newNode &&
typeof this.flyout_.scrollToStart == 'function') {
this.flyout_.scrollToStart();
}
if (this.workspace_.keyboardAccessibilityMode) {
Blockly.navigation.setState(Blockly.navigation.STATE_TOOLBOX);
}
} else {
// Hide the flyout.
this.flyout_.hide();
if (this.workspace_.keyboardAccessibilityMode &&
!(newNode instanceof Blockly.Toolbox.TreeSeparator)) {
Blockly.navigation.setState(Blockly.navigation.STATE_WS);
}
}
if (oldNode != newNode && oldNode != this) {
var event = new Blockly.Events.Ui(null, 'category',
oldNode && oldNode.content, newNode && newNode.content);
event.workspaceId = this.workspace_.id;
Blockly.Events.fire(event);
}
if (newNode) {
this.lastCategory_ = newNode;
}
};
/**
* Handle a node sized changed event.
* @private
*/
Blockly.Toolbox.prototype.handleNodeSizeChanged_ = function() {
// Reposition the workspace so that (0,0) is in the correct position relative
// to the new absolute edge (ie toolbox edge).
var workspace = this.workspace_;
var rect = this.HtmlDiv.getBoundingClientRect();
var newX = this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ?
workspace.scrollX + rect.width : 0;
var newY = this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ?
workspace.scrollY + rect.height : 0;
workspace.translate(newX, newY);
// Even though the div hasn't changed size, the visible workspace
// surface of the workspace has, so we may need to reposition everything.
Blockly.svgResize(workspace);
};
/**
* Handles the given Blockly action on a toolbox.
* This is only triggered when keyboard accessibility mode is enabled.
* @param {!Blockly.Action} action The action to be handled.
* @return {boolean} True if the field handled the action, false otherwise.
* @package
*/
Blockly.Toolbox.prototype.onBlocklyAction = function(action) {
var selected = this.tree_.getSelectedItem();
if (!selected) {
return false;
}
switch (action.name) {
case Blockly.navigation.actionNames.PREVIOUS:
return selected.selectPrevious();
case Blockly.navigation.actionNames.OUT:
return selected.selectParent();
case Blockly.navigation.actionNames.NEXT:
return selected.selectNext();
case Blockly.navigation.actionNames.IN:
return selected.selectChild();
default:
return false;
}
};
/**
* Dispose of this toolbox.
*/
Blockly.Toolbox.prototype.dispose = function() {
this.flyout_.dispose();
this.tree_.dispose();
this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);
Blockly.utils.dom.removeNode(this.HtmlDiv);
this.lastCategory_ = null;
};
/**
* Toggles the visibility of the toolbox.
* @param {boolean} isVisible True if toolbox should be visible.
*/
Blockly.Toolbox.prototype.setVisible = function(isVisible) {
this.HtmlDiv.style.display = isVisible ? 'block' : 'none';
};
/**
* Get the width of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.Toolbox.prototype.getWidth = function() {
return this.width;
};
/**
* Get the height of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.Toolbox.prototype.getHeight = function() {
return this.height;
};
/**
* Get the toolbox flyout.
* @return {Blockly.IFlyout} The toolbox flyout.
*/
Blockly.Toolbox.prototype.getFlyout = function() {
return this.flyout_;
};
/**
* Move the toolbox to the edge.
*/
Blockly.Toolbox.prototype.position = function() {
var treeDiv = this.HtmlDiv;
if (!treeDiv) {
// Not initialized yet.
return;
}
var svgSize = Blockly.svgSize(this.workspace_.getParentSvg());
if (this.horizontalLayout_) {
treeDiv.style.left = '0';
treeDiv.style.height = 'auto';
treeDiv.style.width = svgSize.width + 'px';
this.height = treeDiv.offsetHeight;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
treeDiv.style.top = '0';
} else { // Bottom
treeDiv.style.bottom = '0';
}
} else {
if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
treeDiv.style.right = '0';
} else { // Left
treeDiv.style.left = '0';
}
treeDiv.style.height = svgSize.height + 'px';
this.width = treeDiv.offsetWidth;
}
this.flyout_.position();
};
/**
* Sets the colour on the category.
* @param {number|string} colourValue HSV hue value (0 to 360), #RRGGBB string,
* or a message reference string pointing to one of those two values.
* @param {Blockly.tree.TreeNode} childOut The child to set the hexColour on.
* @param {string} categoryName Name of the toolbox category.
* @private
*/
Blockly.Toolbox.prototype.setColour_ = function(colourValue, childOut,
categoryName) {
// Decode the colour for any potential message references
// (eg. `%{BKY_MATH_HUE}`).
var colour = Blockly.utils.replaceMessageReferences(colourValue);
if (colour == null || colour === '') {
// No attribute. No colour.
childOut.hexColour = '';
} else {
var hue = Number(colour);
if (!isNaN(hue)) {
childOut.hexColour = Blockly.hueToHex(hue);
this.hasColours_ = true;
} else {
var hex = Blockly.utils.colour.parse(colour);
if (hex) {
childOut.hexColour = hex;
this.hasColours_ = true;
} else {
childOut.hexColour = '';
console.warn('Toolbox category "' + categoryName +
'" has unrecognized colour attribute: ' + colour);
}
}
}
};
/**
* Retrieves and sets the colour for the category using the style name.
* The category colour is set from the colour style attribute.
* @param {string} styleName Name of the style.
* @param {!Blockly.tree.TreeNode} childOut The child to set the hexColour on.
* @param {string} categoryName Name of the toolbox category.
* @private
*/
Blockly.Toolbox.prototype.setColourFromStyle_ = function(
styleName, childOut, categoryName) {
childOut.styleName = styleName;
var theme = this.workspace_.getTheme();
if (styleName && theme) {
var style = theme.categoryStyles[styleName];
if (style && style.colour) {
this.setColour_(style.colour, childOut, categoryName);
} else {
console.warn('Style "' + styleName +
'" must exist and contain a colour value');
}
}
};
/**
* Recursively updates all the category colours using the category style name.
* @param {Blockly.tree.BaseNode=} opt_tree Starting point of tree.
* Defaults to the root node.
* @private
*/
Blockly.Toolbox.prototype.updateColourFromTheme_ = function(opt_tree) {
var tree = opt_tree || this.tree_;
if (tree) {
var children = tree.getChildren(false);
for (var i = 0, child; (child = children[i]); i++) {
if (child.styleName) {
this.setColourFromStyle_(child.styleName, child, '');
this.addColour_();
}
this.updateColourFromTheme_(child);
}
}
};
/**
* Updates the category colours and background colour of selected categories.
* @package
*/
Blockly.Toolbox.prototype.refreshTheme = function() {
var tree = this.tree_;
if (tree) {
this.updateColourFromTheme_(tree);
this.updateSelectedItemColour_(tree);
}
};
/**
* Updates the background colour of the selected category.
* @param {!Blockly.tree.BaseNode} tree Starting point of tree.
* Defaults to the root node.
* @private
*/
Blockly.Toolbox.prototype.updateSelectedItemColour_ = function(tree) {
var selectedItem = tree.getSelectedItem();
if (selectedItem) {
var hexColour = selectedItem.hexColour || '#57e';
selectedItem.getRowElement().style.backgroundColor = hexColour;
this.addColour_(selectedItem);
}
};
/**
* Recursively add colours to this toolbox.
* @param {Blockly.tree.BaseNode=} opt_tree Starting point of tree.
* Defaults to the root node.
* @private
*/
Blockly.Toolbox.prototype.addColour_ = function(opt_tree) {
var tree = opt_tree || this.tree_;
var children = tree.getChildren(false);
for (var i = 0, child; (child = children[i]); i++) {
var element = child.getRowElement();
if (element) {
if (this.hasColours_) {
var border = '8px solid ' + (child.hexColour || '#ddd');
} else {
var border = 'none';
}
if (this.workspace_.RTL) {
element.style.borderRight = border;
} else {
element.style.borderLeft = border;
}
}
this.addColour_(child);
}
};
/**
* Unhighlight any previously specified option.
*/
Blockly.Toolbox.prototype.clearSelection = function() {
this.tree_.setSelectedItem(null);
};
/**
* Adds a style on the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to add.
* @package
*/
Blockly.Toolbox.prototype.addStyle = function(style) {
Blockly.utils.dom.addClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Removes a style from the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to remove.
* @package
*/
Blockly.Toolbox.prototype.removeStyle = function(style) {
Blockly.utils.dom.removeClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Return the deletion rectangle for this toolbox.
* @return {Blockly.utils.Rect} Rectangle in which to delete.
*/
Blockly.Toolbox.prototype.getClientRect = function() {
if (!this.HtmlDiv) {
return null;
}
// BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
var top = toolboxRect.top;
var bottom = top + toolboxRect.height;
var left = toolboxRect.left;
var right = left + toolboxRect.width;
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
return new Blockly.utils.Rect(-BIG_NUM, bottom, -BIG_NUM, BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
return new Blockly.utils.Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, right);
} else { // Right
return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
};
/**
* Update the flyout's contents without closing it. Should be used in response
* to a change in one of the dynamic categories, such as variables or
* procedures.
*/
Blockly.Toolbox.prototype.refreshSelection = function() {
var selectedItem = this.tree_.getSelectedItem();
if (selectedItem && selectedItem.contents) {
this.flyout_.show(selectedItem.contents);
}
};
/**
* Select the first toolbox category if no category is selected.
* @package
*/
Blockly.Toolbox.prototype.selectFirstCategory = function() {
var selectedItem = this.tree_.getSelectedItem();
if (!selectedItem) {
this.tree_.selectChild();
}
};
/**
* A blank separator node in the tree.
* @param {!Blockly.tree.BaseNode.Config} config The configuration for the tree.
* @constructor
* @extends {Blockly.tree.TreeNode}
*/
Blockly.Toolbox.TreeSeparator = function(config) {
Blockly.tree.TreeNode.call(this, null, '', config);
};
Blockly.utils.object.inherits(Blockly.Toolbox.TreeSeparator,
Blockly.tree.TreeNode);
/**
* CSS for Toolbox. See css.js for use.
*/
Blockly.Css.register([
/* eslint-disable indent */
'.blocklyToolboxDelete {',
'cursor: url("<<<PATH>>>/handdelete.cur"), auto;',
'}',
'.blocklyToolboxGrab {',
'cursor: url("<<<PATH>>>/handclosed.cur"), auto;',
'cursor: grabbing;',
'cursor: -webkit-grabbing;',
'}',
/* Category tree in Toolbox. */
'.blocklyToolboxDiv {',
'background-color: #ddd;',
'overflow-x: visible;',
'overflow-y: auto;',
'position: absolute;',
'z-index: 70;', /* so blocks go under toolbox when dragging */
'-webkit-tap-highlight-color: transparent;', /* issue #1345 */
'}',
'.blocklyTreeRoot {',
'padding: 4px 0;',
'}',
'.blocklyTreeRoot:focus {',
'outline: none;',
'}',
'.blocklyTreeRow {',
'height: 22px;',
'line-height: 22px;',
'margin-bottom: 3px;',
'padding-right: 8px;',
'white-space: nowrap;',
'}',
'.blocklyHorizontalTree {',
'float: left;',
'margin: 1px 5px 8px 0;',
'}',
'.blocklyHorizontalTreeRtl {',
'float: right;',
'margin: 1px 0 8px 5px;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',
'margin-left: 8px;',
'}',
'.blocklyTreeRow:not(.blocklyTreeSelected):hover {',
'background-color: rgba(255, 255, 255, 0.2);',
'}',
'.blocklyTreeSeparator {',
'border-bottom: solid #e5e5e5 1px;',
'height: 0;',
'margin: 5px 0;',
'}',
'.blocklyTreeSeparatorHorizontal {',
'border-right: solid #e5e5e5 1px;',
'width: 0;',
'padding: 5px 0;',
'margin: 0 5px;',
'}',
'.blocklyTreeIcon {',
'background-image: url(<<<PATH>>>/sprites.png);',
'height: 16px;',
'vertical-align: middle;',
'width: 16px;',
'}',
'.blocklyTreeIconClosedLtr {',
'background-position: -32px -1px;',
'}',
'.blocklyTreeIconClosedRtl {',
'background-position: 0 -1px;',
'}',
'.blocklyTreeIconOpen {',
'background-position: -16px -1px;',
'}',
'.blocklyTreeSelected>.blocklyTreeIconClosedLtr {',
'background-position: -32px -17px;',
'}',
'.blocklyTreeSelected>.blocklyTreeIconClosedRtl {',
'background-position: 0 -17px;',
'}',
'.blocklyTreeSelected>.blocklyTreeIconOpen {',
'background-position: -16px -17px;',
'}',
'.blocklyTreeIconNone,',
'.blocklyTreeSelected>.blocklyTreeIconNone {',
'background-position: -48px -1px;',
'}',
'.blocklyTreeLabel {',
'cursor: default;',
'font: 16px sans-serif;',
'padding: 0 3px;',
'vertical-align: middle;',
'}',
'.blocklyToolboxDelete .blocklyTreeLabel {',
'cursor: url("<<<PATH>>>/handdelete.cur"), auto;',
'}',
'.blocklyTreeSelected .blocklyTreeLabel {',
'color: #fff;',
'}'
/* eslint-enable indent */
]);
Blockly.registry.register(Blockly.registry.Type.TOOLBOX,
Blockly.registry.DEFAULT, Blockly.Toolbox);
+690
View File
@@ -0,0 +1,690 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A toolbox category used to organize blocks in the toolbox.
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
goog.provide('Blockly.ToolboxCategory');
goog.require('Blockly.registry');
goog.require('Blockly.utils');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.toolbox');
goog.require('Blockly.ToolboxItem');
goog.requireType('Blockly.ICollapsibleToolboxItem');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.IToolboxItem');
/**
* Class for a category in a toolbox.
* @param {!Blockly.utils.toolbox.CategoryInfo} categoryDef The information needed
* to create a category in the toolbox.
* @param {!Blockly.IToolbox} toolbox The parent toolbox for the category.
* @param {Blockly.ICollapsibleToolboxItem=} opt_parent The parent category or null if
* the category does not have a parent.
* @constructor
* @extends {Blockly.ToolboxItem}
* @implements {Blockly.ISelectableToolboxItem}
*/
Blockly.ToolboxCategory = function(categoryDef, toolbox, opt_parent) {
Blockly.ToolboxCategory.superClass_.constructor.call(
this, categoryDef, toolbox, opt_parent);
/**
* The name that will be displayed on the category.
* @type {string}
* @protected
*/
this.name_ = Blockly.utils.replaceMessageReferences(categoryDef['name']);
/**
* The colour of the category.
* @type {string}
* @protected
*/
this.colour_ = this.getColour_(categoryDef);
/**
* The html container for the category.
* @type {?Element}
* @protected
*/
this.htmlDiv_ = null;
/**
* The html element for the category row.
* @type {?Element}
* @protected
*/
this.rowDiv_ = null;
/**
* The html element that holds children elements of the category row.
* @type {?Element}
* @protected
*/
this.rowContents_ = null;
/**
* The html element for the toolbox icon.
* @type {?Element}
* @protected
*/
this.iconDom_ = null;
/**
* All the css class names that are used to create a category.
* @type {!Blockly.ToolboxCategory.CssConfig}
* @protected
*/
this.cssConfig_ = this.makeDefaultCssConfig_();
var cssConfig = categoryDef['cssconfig'] || categoryDef['cssConfig'];
Blockly.utils.object.mixin(this.cssConfig_, cssConfig);
/**
* True if the category is meant to be hidden, false otherwise.
* @type {boolean}
* @protected
*/
this.isHidden_ = false;
/**
* True if this category is disabled, false otherwise.
* @type {boolean}
* @protected
*/
this.isDisabled_ = false;
/**
* The flyout items for this category.
* @type {string|!Blockly.utils.toolbox.FlyoutItemInfoArray}
* @protected
*/
this.flyoutItems_ = [];
this.parseContents_(categoryDef);
};
Blockly.utils.object.inherits(Blockly.ToolboxCategory, Blockly.ToolboxItem);
/**
* All the css class names that are used to create a category.
* @typedef {{
* container:?string,
* row:?string,
* icon:?string,
* label:?string,
* selected:?string,
* openIcon:?string,
* closedIcon:?string,
* }}
*/
Blockly.ToolboxCategory.CssConfig;
/**
* Name used for registering a toolbox category.
* @const {string}
*/
Blockly.ToolboxCategory.registrationName = 'category';
/**
* The number of pixels to move the category over at each nested level.
* @type {number}
*/
Blockly.ToolboxCategory.nestedPadding = 19;
/**
* The width in pixels of the strip of colour next to each category.
* @type {number}
*/
Blockly.ToolboxCategory.borderWidth = 8;
/**
* The default colour of the category. This is used as the background colour of
* the category when it is selected.
* @type {string}
*/
Blockly.ToolboxCategory.defaultBackgroundColour = '#57e';
/**
* Creates an object holding the default classes for a category.
* @return {!Blockly.ToolboxCategory.CssConfig} The configuration object holding
* all the CSS classes for a category.
* @protected
*/
Blockly.ToolboxCategory.prototype.makeDefaultCssConfig_ = function() {
return {
'container': 'blocklyToolboxCategory',
'row': 'blocklyTreeRow',
'rowContentContainer': 'blocklyTreeRowContentContainer',
'icon': 'blocklyTreeIcon',
'label': 'blocklyTreeLabel',
'contents': 'blocklyToolboxContents',
'selected': 'blocklyTreeSelected',
'openIcon': 'blocklyTreeIconOpen',
'closedIcon': 'blocklyTreeIconClosed',
};
};
/**
* Parses the contents array depending on if the category is a dynamic category,
* or if its contents are meant to be shown in the flyout.
* @param {!Blockly.utils.toolbox.CategoryInfo} categoryDef The information needed
* to create a category.
* @protected
*/
Blockly.ToolboxCategory.prototype.parseContents_ = function(categoryDef) {
var contents = categoryDef['contents'];
if (categoryDef['custom']) {
this.flyoutItems_ = categoryDef['custom'];
} else if (contents) {
for (var i = 0, itemDef; (itemDef = contents[i]); i++) {
var flyoutItem = /** @type {Blockly.utils.toolbox.FlyoutItemInfo} */ (itemDef);
this.flyoutItems_.push(flyoutItem);
}
}
};
/**
* @override
*/
Blockly.ToolboxCategory.prototype.init = function() {
this.createDom_();
if (this.toolboxItemDef_['hidden'] == 'true') {
this.hide();
}
};
/**
* Creates the dom for the category.
* @return {!Element} The parent element for the category.
* @protected
*/
Blockly.ToolboxCategory.prototype.createDom_ = function() {
this.htmlDiv_ = this.createContainer_();
Blockly.utils.aria.setRole(this.htmlDiv_, Blockly.utils.aria.Role.TREEITEM);
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.SELECTED,false);
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.LEVEL, this.level_);
this.rowDiv_ = this.createRowContainer_();
this.rowDiv_.setAttribute('id', this.id_);
this.rowDiv_.style.pointerEvents = 'auto';
this.htmlDiv_.appendChild(this.rowDiv_);
this.rowContents_ = this.createRowContentsContainer_();
this.rowContents_.style.pointerEvents = 'none';
this.rowDiv_.appendChild(this.rowContents_);
this.iconDom_ = this.createIconDom_();
Blockly.utils.aria.setRole(this.iconDom_, Blockly.utils.aria.Role.PRESENTATION);
this.rowContents_.appendChild(this.iconDom_);
var labelDom = this.createLabelDom_(this.name_);
this.rowContents_.appendChild(labelDom);
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.LABELLEDBY, labelDom.getAttribute('id'));
this.addColourBorder_(this.colour_);
return this.htmlDiv_;
};
/**
* Creates the container that holds the row and any subcategories.
* @return {!Element} The div that holds the icon and the label.
* @protected
*/
Blockly.ToolboxCategory.prototype.createContainer_ = function() {
var container = document.createElement('div');
Blockly.utils.dom.addClass(container, this.cssConfig_['container']);
return container;
};
/**
* Creates the parent of the contents container. All clicks will happen on this
* div.
* @return {!Element} The div that holds the contents container.
* @protected
*/
Blockly.ToolboxCategory.prototype.createRowContainer_ = function() {
var rowDiv = document.createElement('div');
Blockly.utils.dom.addClass(rowDiv, this.cssConfig_['row']);
var nestedPadding = Blockly.ToolboxCategory.nestedPadding * this.getLevel();
nestedPadding = nestedPadding.toString() + 'px';
this.workspace_.RTL ? rowDiv.style.paddingRight = nestedPadding :
rowDiv.style.paddingLeft = nestedPadding;
return rowDiv;
};
/**
* Creates the container for the label and icon.
* This is necessary so we can set all subcategory pointer events to none.
* @return {!Element} The div that holds the icon and the label.
* @protected
*/
Blockly.ToolboxCategory.prototype.createRowContentsContainer_ = function() {
var contentsContainer = document.createElement('div');
Blockly.utils.dom.addClass(contentsContainer, this.cssConfig_['rowContentContainer']);
return contentsContainer;
};
/**
* Creates the span that holds the category icon.
* @return {!Element} The span that holds the category icon.
* @protected
*/
Blockly.ToolboxCategory.prototype.createIconDom_ = function() {
var toolboxIcon = document.createElement('span');
if (!this.parentToolbox_.isHorizontal()) {
Blockly.utils.dom.addClass(toolboxIcon, this.cssConfig_['icon']);
}
toolboxIcon.style.display = 'inline-block';
return toolboxIcon;
};
/**
* Creates the span that holds the category label.
* This should have an id for accessibility purposes.
* @param {string} name The name of the category.
* @return {!Element} The span that holds the category label.
* @protected
*/
Blockly.ToolboxCategory.prototype.createLabelDom_ = function(name) {
var toolboxLabel = document.createElement('span');
toolboxLabel.setAttribute('id', this.getId() + '.label');
toolboxLabel.textContent = name;
Blockly.utils.dom.addClass(toolboxLabel, this.cssConfig_['label']);
return toolboxLabel;
};
/**
* Updates the colour for this category.
* @public
*/
Blockly.ToolboxCategory.prototype.refreshTheme = function() {
this.colour_ = this.getColour_(/** @type {Blockly.utils.toolbox.CategoryInfo} **/
(this.toolboxItemDef_));
this.addColourBorder_(this.colour_);
};
/**
* Add the strip of colour to the toolbox category.
* @param {string} colour The category colour.
* @protected
*/
Blockly.ToolboxCategory.prototype.addColourBorder_ = function(colour) {
if (colour) {
var border = Blockly.ToolboxCategory.borderWidth + 'px solid ' +
(colour || '#ddd');
if (this.workspace_.RTL) {
this.rowDiv_.style.borderRight = border;
} else {
this.rowDiv_.style.borderLeft = border;
}
}
};
/**
* Gets either the colour or the style for a category.
* @param {!Blockly.utils.toolbox.CategoryInfo} categoryDef The object holding
* information on the category.
* @return {string} The hex colour for the category.
* @protected
*/
Blockly.ToolboxCategory.prototype.getColour_ = function(categoryDef) {
var styleName = categoryDef['categorystyle'] || categoryDef['categoryStyle'];
var colour = categoryDef['colour'];
if (colour && styleName) {
console.warn('Toolbox category "' + this.name_ +
'" must not have both a style and a colour');
} else if (styleName) {
return this.getColourfromStyle_(styleName);
} else {
return this.parseColour_(colour);
}
return '';
};
/**
* Sets the colour for the category using the style name and returns the new
* colour as a hex string.
* @param {string} styleName Name of the style.
* @return {string} The hex colour for the category.
* @private
*/
Blockly.ToolboxCategory.prototype.getColourfromStyle_ = function(styleName) {
var theme = this.workspace_.getTheme();
if (styleName && theme) {
var style = theme.categoryStyles[styleName];
if (style && style.colour) {
return this.parseColour_(style.colour);
} else {
console.warn('Style "' + styleName +
'" must exist and contain a colour value');
}
}
return '';
};
/**
* Parses the colour on the category.
* @param {number|string} colourValue HSV hue value (0 to 360), #RRGGBB string,
* or a message reference string pointing to one of those two values.
* @return {string} The hex colour for the category.
* @private
*/
Blockly.ToolboxCategory.prototype.parseColour_ = function(colourValue) {
// Decode the colour for any potential message references
// (eg. `%{BKY_MATH_HUE}`).
var colour = Blockly.utils.replaceMessageReferences(colourValue);
if (colour == null || colour === '') {
// No attribute. No colour.
return '';
} else {
var hue = Number(colour);
if (!isNaN(hue)) {
return Blockly.hueToHex(hue);
} else {
var hex = Blockly.utils.colour.parse(colour);
if (hex) {
return hex;
} else {
console.warn('Toolbox category "' + this.name_ +
'" has unrecognized colour attribute: ' + colour);
return '';
}
}
}
};
/**
* Adds appropriate classes to display an open icon.
* @param {?Element} iconDiv The div that holds the icon.
* @protected
*/
Blockly.ToolboxCategory.prototype.openIcon_ = function(iconDiv) {
if (!iconDiv) {
return;
}
Blockly.utils.dom.removeClasses(iconDiv, this.cssConfig_['closedIcon']);
Blockly.utils.dom.addClass(iconDiv, this.cssConfig_['openIcon']);
};
/**
* Adds appropriate classes to display a closed icon.
* @param {?Element} iconDiv The div that holds the icon.
* @protected
*/
Blockly.ToolboxCategory.prototype.closeIcon_ = function(iconDiv) {
if (!iconDiv) {
return;
}
Blockly.utils.dom.removeClasses(iconDiv, this.cssConfig_['openIcon']);
Blockly.utils.dom.addClass(iconDiv, this.cssConfig_['closedIcon']);
};
/**
* Sets whether the category is visible or not.
* For a category to be visible its parent category must also be expanded.
* @param {boolean} isVisible True if category should be visible.
* @protected
*/
Blockly.ToolboxCategory.prototype.setVisible_ = function(isVisible) {
this.htmlDiv_.style.display = isVisible ? 'block' : 'none';
this.isHidden_ = !isVisible;
if (this.parentToolbox_.getSelectedItem() == this) {
this.parentToolbox_.clearSelection();
}
};
/**
* Hide the category.
*/
Blockly.ToolboxCategory.prototype.hide = function() {
this.setVisible_(false);
};
/**
* Show the category. Category will only appear if its parent category is also
* expanded.
*/
Blockly.ToolboxCategory.prototype.show = function() {
this.setVisible_(true);
};
/**
* Whether the category is visible.
* A category is only visible if all of its ancestors are expanded and isHidden_ is false.
* @return {boolean} True if the category is visible, false otherwise.
* @public
*/
Blockly.ToolboxCategory.prototype.isVisible = function() {
return !this.isHidden_ && this.allAncestorsExpanded_();
};
/**
* Whether all ancestors of a category (parent and parent's parent, etc.) are expanded.
* @return {boolean} True only if every ancestor is expanded
* @protected
*/
Blockly.ToolboxCategory.prototype.allAncestorsExpanded_ = function() {
var category = this;
while (category.getParent()) {
category = category.getParent();
if (!category.isExpanded()) {
return false;
}
}
return true;
};
/**
* @override
*/
Blockly.ToolboxCategory.prototype.isSelectable = function() {
return this.isVisible() && !this.isDisabled_;
};
/**
* Handles when the toolbox item is clicked.
* @param {!Event} _e Click event to handle.
* @public
*/
Blockly.ToolboxCategory.prototype.onClick = function(_e) {
// No-op
};
/**
* Sets the current category as selected.
* @param {boolean} isSelected True if this category is selected, false
* otherwise.
* @public
*/
Blockly.ToolboxCategory.prototype.setSelected = function(isSelected) {
if (isSelected) {
var defaultColour = this.parseColour_(
Blockly.ToolboxCategory.defaultBackgroundColour);
this.rowDiv_.style.backgroundColor = this.colour_ || defaultColour;
Blockly.utils.dom.addClass(this.rowDiv_, this.cssConfig_['selected']);
} else {
this.rowDiv_.style.backgroundColor = '';
Blockly.utils.dom.removeClass(this.rowDiv_, this.cssConfig_['selected']);
}
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.SELECTED, isSelected);
};
/**
* Sets whether the category is disabled.
* @param {boolean} isDisabled True to disable the category, false otherwise.
*/
Blockly.ToolboxCategory.prototype.setDisabled = function(isDisabled) {
this.isDisabled_ = isDisabled;
this.getDiv().setAttribute('disabled', isDisabled);
isDisabled ? this.getDiv().setAttribute('disabled', 'true') :
this.getDiv().removeAttribute('disabled');
};
/**
* Gets the name of the category. Used for emitting events.
* @return {string} The name of the toolbox item.
* @public
*/
Blockly.ToolboxCategory.prototype.getName = function() {
return this.name_;
};
/**
* @override
*/
Blockly.ToolboxCategory.prototype.getParent = function() {
return this.parent_;
};
/**
* @override
*/
Blockly.ToolboxCategory.prototype.getDiv = function() {
return this.htmlDiv_;
};
/**
* Gets the contents of the category. These are items that are meant to be
* displayed in the flyout.
* @return {!Blockly.utils.toolbox.FlyoutItemInfoArray|string} The definition
* of items to be displayed in the flyout.
* @public
*/
Blockly.ToolboxCategory.prototype.getContents = function() {
return this.flyoutItems_;
};
/**
* Updates the contents to be displayed in the flyout.
* If the flyout is open when the contents are updated, refreshSelection on the
* toolbox must also be called.
* @param {!Blockly.utils.toolbox.FlyoutDefinition|string} contents The contents
* to be displayed in the flyout. A string can be supplied to create a
* dynamic category.
* @public
*/
Blockly.ToolboxCategory.prototype.updateFlyoutContents = function(contents) {
this.flyoutItems_ = [];
if (typeof contents == 'string') {
this.toolboxItemDef_['custom'] = contents;
} else {
// Removes old custom field when contents is updated.
delete this.toolboxItemDef_['custom'];
this.toolboxItemDef_['contents'] =
Blockly.utils.toolbox.convertFlyoutDefToJsonArray(contents);
}
this.parseContents_(
/** @type {Blockly.utils.toolbox.CategoryInfo} */ (this.toolboxItemDef_));
};
/**
* @override
*/
Blockly.ToolboxCategory.prototype.dispose = function() {
Blockly.utils.dom.removeNode(this.htmlDiv_);
};
/**
* CSS for Toolbox. See css.js for use.
*/
Blockly.Css.register([
/* eslint-disable indent */
'.blocklyTreeRow:not(.blocklyTreeSelected):hover {',
'background-color: rgba(255, 255, 255, 0.2);',
'}',
'.blocklyToolboxDiv[layout="h"] .blocklyToolboxCategory {',
'margin: 1px 5px 1px 0;',
'}',
'.blocklyToolboxDiv[dir="RTL"][layout="h"] .blocklyToolboxCategory {',
'margin: 1px 0 1px 5px;',
'}',
'.blocklyTreeRow {',
'height: 22px;',
'line-height: 22px;',
'margin-bottom: 3px;',
'padding-right: 8px;',
'white-space: nowrap;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',
'margin-left: 8px;',
'padding-right: 0px',
'}',
'.blocklyTreeIcon {',
'background-image: url(<<<PATH>>>/sprites.png);',
'height: 16px;',
'vertical-align: middle;',
'visibility: hidden;',
'width: 16px;',
'}',
'.blocklyTreeIconClosed {',
'background-position: -32px -1px;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeIconClosed {',
'background-position: 0 -1px;',
'}',
'.blocklyTreeSelected>.blocklyTreeIconClosed {',
'background-position: -32px -17px;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeSelected>.blocklyTreeIconClosed {',
'background-position: 0 -17px;',
'}',
'.blocklyTreeIconOpen {',
'background-position: -16px -1px;',
'}',
'.blocklyTreeSelected>.blocklyTreeIconOpen {',
'background-position: -16px -17px;',
'}',
'.blocklyTreeLabel {',
'cursor: default;',
'font: 16px sans-serif;',
'padding: 0 3px;',
'vertical-align: middle;',
'}',
'.blocklyToolboxDelete .blocklyTreeLabel {',
'cursor: url("<<<PATH>>>/handdelete.cur"), auto;',
'}',
'.blocklyTreeSelected .blocklyTreeLabel {',
'color: #fff;',
'}'
/* eslint-enable indent */
]);
Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM,
Blockly.ToolboxCategory.registrationName, Blockly.ToolboxCategory);
+294
View File
@@ -0,0 +1,294 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A toolbox category used to organize blocks in the toolbox.
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
goog.provide('Blockly.CollapsibleToolboxCategory');
goog.require('Blockly.registry');
goog.require('Blockly.ToolboxCategory');
goog.require('Blockly.ToolboxSeparator');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.toolbox');
goog.require('Blockly.ToolboxItem');
goog.requireType('Blockly.ICollapsibleToolboxItem');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.IToolboxItem');
/**
* Class for a category in a toolbox that can be collapsed.
* @param {!Blockly.utils.toolbox.CategoryInfo} categoryDef The information needed
* to create a category in the toolbox.
* @param {!Blockly.IToolbox} toolbox The parent toolbox for the category.
* @param {Blockly.ICollapsibleToolboxItem=} opt_parent The parent category or null if
* the category does not have a parent.
* @constructor
* @extends {Blockly.ToolboxCategory}
* @implements {Blockly.ICollapsibleToolboxItem}
*/
Blockly.CollapsibleToolboxCategory = function(categoryDef, toolbox, opt_parent) {
/**
* Container for any child categories.
* @type {?Element}
* @protected
*/
this.subcategoriesDiv_ = null;
/**
* Whether or not the category should display its subcategories.
* @type {boolean}
* @protected
*/
this.expanded_ = false;
/**
* The child toolbox items for this category.
* @type {!Array<!Blockly.ToolboxItem>}
* @protected
*/
this.toolboxItems_ = [];
Blockly.CollapsibleToolboxCategory.superClass_.constructor.call(
this, categoryDef, toolbox, opt_parent);
};
Blockly.utils.object.inherits(Blockly.CollapsibleToolboxCategory, Blockly.ToolboxCategory);
/**
* All the css class names that are used to create a collapsible
* category. This is all the properties from the regular category plus contents.
* @typedef {{
* container:?string,
* row:?string,
* icon:?string,
* label:?string,
* selected:?string,
* openIcon:?string,
* closedIcon:?string,
* contents:?string,
* }}
*/
Blockly.CollapsibleToolboxCategory.CssConfig;
/**
* Name used for registering a collapsible toolbox category.
* @const {string}
*/
Blockly.CollapsibleToolboxCategory.registrationName = 'collapsibleCategory';
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.makeDefaultCssConfig_ = function() {
var cssConfig = Blockly.CollapsibleToolboxCategory.superClass_.makeDefaultCssConfig_.call(this);
cssConfig['contents'] = 'blocklyToolboxContents';
return cssConfig;
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.parseContents_ = function(categoryDef) {
var contents = categoryDef['contents'];
var prevIsFlyoutItem = true;
if (categoryDef['custom']) {
this.flyoutItems_ = categoryDef['custom'];
} else if (contents) {
for (var i = 0, itemDef; (itemDef = contents[i]); i++) {
// Separators can exist as either a flyout item or a toolbox item so
// decide where it goes based on the type of the previous item.
if (!Blockly.registry.hasItem(Blockly.registry.Type.TOOLBOX_ITEM, itemDef['kind']) ||
(itemDef['kind'].toLowerCase() == Blockly.ToolboxSeparator.registrationName &&
prevIsFlyoutItem)) {
var flyoutItem = /** @type {Blockly.utils.toolbox.FlyoutItemInfo} */ (itemDef);
this.flyoutItems_.push(flyoutItem);
prevIsFlyoutItem = true;
} else {
this.createToolboxItem_(itemDef);
prevIsFlyoutItem = false;
}
}
}
};
/**
* Creates a toolbox item and adds it to the list of toolbox items.
* @param {!Blockly.utils.toolbox.ToolboxItemInfo} itemDef The information needed
* to create a toolbox item.
* @private
*/
Blockly.CollapsibleToolboxCategory.prototype.createToolboxItem_ = function(itemDef) {
var registryName = itemDef['kind'];
var categoryDef = /** @type {!Blockly.utils.toolbox.CategoryInfo} */ (itemDef);
// Categories that are collapsible are created using a class registered under
// a diffferent name.
if (registryName.toUpperCase() == 'CATEGORY' &&
Blockly.utils.toolbox.isCategoryCollapsible(categoryDef)) {
registryName = Blockly.CollapsibleToolboxCategory.registrationName;
}
var ToolboxItemClass = Blockly.registry.getClass(
Blockly.registry.Type.TOOLBOX_ITEM, registryName);
var toolboxItem = new ToolboxItemClass(itemDef, this.parentToolbox_, this);
this.toolboxItems_.push(toolboxItem);
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.init = function() {
Blockly.CollapsibleToolboxCategory.superClass_.init.call(this);
this.setExpanded(this.toolboxItemDef_['expanded'] == 'true' ||
this.toolboxItemDef_['expanded']);
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.createDom_ = function() {
Blockly.CollapsibleToolboxCategory.superClass_.createDom_.call(this);
var subCategories = this.getChildToolboxItems();
this.subcategoriesDiv_ = this.createSubCategoriesDom_(subCategories);
Blockly.utils.aria.setRole(this.subcategoriesDiv_,
Blockly.utils.aria.Role.GROUP);
this.htmlDiv_.appendChild(this.subcategoriesDiv_);
return this.htmlDiv_;
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.createIconDom_ = function() {
var toolboxIcon = document.createElement('span');
if (!this.parentToolbox_.isHorizontal()) {
Blockly.utils.dom.addClass(toolboxIcon, this.cssConfig_['icon']);
toolboxIcon.style.visibility = 'visible';
}
toolboxIcon.style.display = 'inline-block';
return toolboxIcon;
};
/**
* Create the dom for all subcategories.
* @param {!Array<!Blockly.ToolboxItem>} subcategories The subcategories.
* @return {!Element} The div holding all the subcategories.
* @protected
*/
Blockly.CollapsibleToolboxCategory.prototype.createSubCategoriesDom_ = function(subcategories) {
var contentsContainer = document.createElement('div');
Blockly.utils.dom.addClass(contentsContainer, this.cssConfig_['contents']);
for (var i = 0; i < subcategories.length; i++) {
var newCategory = subcategories[i];
newCategory.init();
var newCategoryDiv = newCategory.getDiv();
contentsContainer.appendChild(newCategoryDiv);
}
return contentsContainer;
};
/**
* Opens or closes the current category.
* @param {boolean} isExpanded True to expand the category, false to close.
* @public
*/
Blockly.CollapsibleToolboxCategory.prototype.setExpanded = function(isExpanded) {
if (this.expanded_ == isExpanded) {
return;
}
this.expanded_ = isExpanded;
if (isExpanded) {
this.subcategoriesDiv_.style.display = 'block';
this.openIcon_(this.iconDom_);
} else {
this.subcategoriesDiv_.style.display = 'none';
this.closeIcon_(this.iconDom_);
}
Blockly.utils.aria.setState(/** @type {!Element} */ (this.htmlDiv_),
Blockly.utils.aria.State.EXPANDED, isExpanded);
this.parentToolbox_.handleToolboxItemResize();
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.setVisible_ = function(isVisible) {
this.htmlDiv_.style.display = isVisible ? 'block' : 'none';
for (var i = 0, child; (child = this.getChildToolboxItems()[i]); i++) {
child.setVisible_(isVisible);
}
this.isHidden_ = !isVisible;
if (this.parentToolbox_.getSelectedItem() == this) {
this.parentToolbox_.clearSelection();
}
};
/**
* Whether the category is expanded to show its child subcategories.
* @return {boolean} True if the toolbox item shows its children, false if it
* is collapsed.
* @public
*/
Blockly.CollapsibleToolboxCategory.prototype.isExpanded = function() {
return this.expanded_;
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.isCollapsible = function() {
return true;
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.onClick = function(_e) {
this.toggleExpanded();
};
/**
* Toggles whether or not the category is expanded.
* @public
*/
Blockly.CollapsibleToolboxCategory.prototype.toggleExpanded = function() {
this.setExpanded(!this.expanded_);
};
/**
* @override
*/
Blockly.CollapsibleToolboxCategory.prototype.getDiv = function() {
return this.htmlDiv_;
};
/**
* Gets any children toolbox items. (ex. Gets the subcategories)
* @return {!Array<!Blockly.IToolboxItem>} The child toolbox items.
*/
Blockly.CollapsibleToolboxCategory.prototype.getChildToolboxItems = function() {
return this.toolboxItems_;
};
Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM,
Blockly.CollapsibleToolboxCategory.registrationName, Blockly.CollapsibleToolboxCategory);
+123
View File
@@ -0,0 +1,123 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A separator used for separating toolbox categories.
* @author aschmiedt@google.com (Abby Schmiedt)
* @author maribethb@google.com (Maribeth Bottorff)
*/
'use strict';
goog.provide('Blockly.ToolboxSeparator');
goog.require('Blockly.registry');
goog.require('Blockly.ToolboxItem');
goog.require('Blockly.utils.dom');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.IToolboxItem');
goog.requireType('Blockly.utils.toolbox');
/**
* Class for a toolbox separator. This is the thin visual line that appears on
* the toolbox. This item is not interactable.
* @param {!Blockly.utils.toolbox.SeparatorInfo} separatorDef The information
* needed to create a separator.
* @param {!Blockly.IToolbox} toolbox The parent toolbox for the separator.
* @constructor
* @extends {Blockly.ToolboxItem}
* @implements {Blockly.IToolboxItem}
*/
Blockly.ToolboxSeparator = function(separatorDef, toolbox) {
Blockly.ToolboxSeparator.superClass_.constructor.call(
this, separatorDef, toolbox);
/**
* All the css class names that are used to create a separator.
* @type {!Blockly.ToolboxSeparator.CssConfig}
* @protected
*/
this.cssConfig_ = {
'container': 'blocklyTreeSeparator'
};
var cssConfig = separatorDef['cssconfig'] || separatorDef['cssConfig'];
Blockly.utils.object.mixin(this.cssConfig_, cssConfig);
};
Blockly.utils.object.inherits(Blockly.ToolboxSeparator, Blockly.ToolboxItem);
/**
* All the css class names that are used to create a separator.
* @typedef {{
* container:?string,
* }}
*/
Blockly.ToolboxSeparator.CssConfig;
/**
* Name used for registering a toolbox separator.
* @const {string}
*/
Blockly.ToolboxSeparator.registrationName = 'sep';
/**
* @override
*/
Blockly.ToolboxSeparator.prototype.init = function() {
this.createDom_();
};
/**
* Creates the dom for a separator.
* @return {!Element} The parent element for the separator.
* @protected
*/
Blockly.ToolboxSeparator.prototype.createDom_ = function() {
var container = document.createElement('div');
Blockly.utils.dom.addClass(container, this.cssConfig_['container']);
this.htmlDiv_ = container;
return container;
};
/**
* @override
*/
Blockly.ToolboxSeparator.prototype.getDiv = function() {
return this.htmlDiv_;
};
/**
* @override
*/
Blockly.ToolboxSeparator.prototype.dispose = function() {
Blockly.utils.dom.removeNode(this.htmlDiv_);
};
/**
* CSS for Toolbox. See css.js for use.
*/
Blockly.Css.register([
/* eslint-disable indent */
'.blocklyTreeSeparator {',
'border-bottom: solid #e5e5e5 1px;',
'height: 0;',
'margin: 5px 0;',
'}',
'.blocklyToolboxDiv[layout="h"] .blocklyTreeSeparator {',
'border-right: solid #e5e5e5 1px;',
'border-bottom: none;',
'height: auto;',
'margin: 0 5px 0 5px;',
'padding: 5px 0;',
'width: 0;',
'}',
/* eslint-enable indent */
]);
Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM,
Blockly.ToolboxSeparator.registrationName, Blockly.ToolboxSeparator);
+985
View File
@@ -0,0 +1,985 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Toolbox from whence to create blocks.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Toolbox');
goog.require('Blockly.CollapsibleToolboxCategory');
goog.require('Blockly.Css');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Ui');
goog.require('Blockly.navigation');
goog.require('Blockly.registry');
goog.require('Blockly.Touch');
goog.require('Blockly.utils');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.Rect');
goog.requireType('Blockly.Action');
goog.requireType('Blockly.IBlocklyActionable');
goog.requireType('Blockly.ICollapsibleToolboxItem');
goog.requireType('Blockly.IDeleteArea');
goog.requireType('Blockly.IFlyout');
goog.requireType('Blockly.ISelectableToolboxItem');
goog.requireType('Blockly.IStyleable');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.IToolboxItem');
goog.requireType('Blockly.utils.toolbox');
goog.requireType('Blockly.WorkspaceSvg');
/**
* Class for a Toolbox.
* Creates the toolbox's DOM.
* @param {!Blockly.WorkspaceSvg} workspace The workspace in which to create new
* blocks.
* @constructor
* @implements {Blockly.IBlocklyActionable}
* @implements {Blockly.IDeleteArea}
* @implements {Blockly.IStyleable}
* @implements {Blockly.IToolbox}
*/
Blockly.Toolbox = function(workspace) {
/**
* The workspace this toolbox is on.
* @type {!Blockly.WorkspaceSvg}
* @protected
*/
this.workspace_ = workspace;
/**
* The JSON describing the contents of this toolbox.
* @type {!Blockly.utils.toolbox.ToolboxInfo}
* @protected
*/
this.toolboxDef_ = workspace.options.languageTree || {'contents': []};
/**
* Whether the toolbox should be laid out horizontally.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspace.options.horizontalLayout;
/**
* The html container for the toolbox.
* @type {?Element}
*/
this.HtmlDiv = null;
/**
* The html container for the contents of a toolbox.
* @type {?Element}
* @protected
*/
this.contentsDiv_ = null;
/**
* The list of items in the toolbox.
* @type {!Array<!Blockly.IToolboxItem>}
* @protected
*/
this.contents_ = [];
/**
* The width of the toolbox.
* @type {number}
* @protected
*/
this.width_ = 0;
/**
* The height of the toolbox.
* @type {number}
* @protected
*/
this.height_ = 0;
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = workspace.options.RTL;
/**
* The flyout for the toolbox.
* @type {?Blockly.IFlyout}
* @private
*/
this.flyout_ = null;
/**
* A map from toolbox item IDs to toolbox items.
* @type {!Object<string, Blockly.IToolboxItem>}
* @protected
*/
this.contentMap_ = {};
/**
* Position of the toolbox and flyout relative to the workspace.
* TODO (#4246): Add an enum for toolbox types.
* @type {number}
*/
this.toolboxPosition = workspace.options.toolboxPosition;
/**
* The currently selected item.
* @type {?Blockly.ISelectableToolboxItem}
* @protected
*/
this.selectedItem_ = null;
/**
* The previously selected item.
* @type {?Blockly.ISelectableToolboxItem}
* @protected
*/
this.previouslySelectedItem_ = null;
/**
* Array holding info needed to unbind event handlers.
* Used for disposing.
* Ex: [[node, name, func], [node, name, func]].
* @type {!Array<!Blockly.EventData>}
* @protected
*/
this.boundEvents_ = [];
};
/**
* Initializes the toolbox
* @public
*/
Blockly.Toolbox.prototype.init = function() {
var workspace = this.workspace_;
var svg = workspace.getParentSvg();
this.flyout_ = this.createFlyout_();
this.HtmlDiv = this.createDom_(this.workspace_);
Blockly.utils.dom.insertAfter(this.flyout_.createDom('svg'), svg);
this.flyout_.init(workspace);
this.render(this.toolboxDef_);
var themeManager = workspace.getThemeManager();
themeManager.subscribe(this.HtmlDiv, 'toolboxBackgroundColour',
'background-color');
themeManager.subscribe(this.HtmlDiv, 'toolboxForegroundColour', 'color');
};
/**
* Creates the dom for the toolbox.
* @param {!Blockly.WorkspaceSvg} workspace The workspace this toolbox is on.
* @return {!Element} The html container for the toolbox.
* @protected
*/
Blockly.Toolbox.prototype.createDom_ = function(workspace) {
var svg = workspace.getParentSvg();
var container = this.createContainer_();
this.contentsDiv_ = this.createContentsContainer_();
this.contentsDiv_.tabIndex = 0;
Blockly.utils.aria.setRole(this.contentsDiv_, Blockly.utils.aria.Role.TREE);
container.appendChild(this.contentsDiv_);
svg.parentNode.insertBefore(container, svg);
this.attachEvents_(container, this.contentsDiv_);
return container;
};
/**
* Creates the container div for the toolbox.
* @return {!Element} The html container for the toolbox.
* @protected
*/
Blockly.Toolbox.prototype.createContainer_ = function() {
var toolboxContainer = document.createElement('div');
toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v');
Blockly.utils.dom.addClass(toolboxContainer, 'blocklyToolboxDiv');
Blockly.utils.dom.addClass(toolboxContainer, 'blocklyNonSelectable');
toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR');
return toolboxContainer;
};
/**
* Creates the container for all the contents in the toolbox.
* @return {!Element} The html container for the toolbox contents.
* @protected
*/
Blockly.Toolbox.prototype.createContentsContainer_ = function() {
var contentsContainer = document.createElement('div');
Blockly.utils.dom.addClass(contentsContainer, 'blocklyToolboxContents');
if (this.isHorizontal()) {
contentsContainer.style.flexDirection = 'row';
}
return contentsContainer;
};
/**
* Adds event listeners to the toolbox container div.
* @param {!Element} container The html container for the toolbox.
* @param {!Element} contentsContainer The html container for the contents
* of the toolbox.
* @protected
*/
Blockly.Toolbox.prototype.attachEvents_ = function(container,
contentsContainer) {
// Clicking on toolbox closes popups.
var clickEvent = Blockly.bindEventWithChecks_(container, 'mousedown', this,
this.onClick_, /* opt_noCaptureIdentifier */ false,
/* opt_noPreventDefault */ true);
this.boundEvents_.push(clickEvent);
var keyDownEvent = Blockly.bindEventWithChecks_(contentsContainer, 'keydown',
this, this.onKeyDown_, /* opt_noCaptureIdentifier */ false,
/* opt_noPreventDefault */ true);
this.boundEvents_.push(keyDownEvent);
};
/**
* Handles on click events for when the toolbox or toolbox items are clicked.
* @param {!Event} e Click event to handle.
* @protected
*/
Blockly.Toolbox.prototype.onClick_ = function(e) {
if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) {
// Close flyout.
Blockly.hideChaff(false);
} else {
var srcElement = e.srcElement;
var itemId = srcElement.getAttribute('id');
if (itemId) {
var item = this.getToolboxItemById(itemId);
if (item.isSelectable()) {
this.setSelectedItem(item);
item.onClick(e);
}
}
// Just close popups.
Blockly.hideChaff(true);
}
Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
};
/**
* Handles key down events for the toolbox.
* @param {!KeyboardEvent} e The key down event.
* @protected
*/
Blockly.Toolbox.prototype.onKeyDown_ = function(e) {
var handled = false;
switch (e.keyCode) {
case Blockly.utils.KeyCodes.DOWN:
handled = this.selectNext_();
break;
case Blockly.utils.KeyCodes.UP:
handled = this.selectPrevious_();
break;
case Blockly.utils.KeyCodes.LEFT:
handled = this.selectParent_();
break;
case Blockly.utils.KeyCodes.RIGHT:
handled = this.selectChild_();
break;
case Blockly.utils.KeyCodes.ENTER:
case Blockly.utils.KeyCodes.SPACE:
if (this.selectedItem_ && this.selectedItem_.isCollapsible()) {
var collapsibleItem = /** @type {!Blockly.ICollapsibleToolboxItem} */ (this.selectedItem_);
collapsibleItem.toggleExpanded();
handled = true;
}
break;
default:
handled = false;
break;
}
if (handled) {
e.preventDefault();
}
};
/**
* Creates the flyout based on the toolbox layout.
* @return {!Blockly.IFlyout} The flyout for the toolbox.
* @throws {Error} If missing a require for `Blockly.HorizontalFlyout`,
* `Blockly.VerticalFlyout`, and no flyout plugin is specified.
* @protected
*/
Blockly.Toolbox.prototype.createFlyout_ = function() {
var workspace = this.workspace_;
// TODO (#4247): Look into adding a makeFlyout method to Blockly Options.
var workspaceOptions = new Blockly.Options(
/** @type {!Blockly.BlocklyOptions} */
({
'parentWorkspace': workspace,
'rtl': workspace.RTL,
'oneBasedIndex': workspace.options.oneBasedIndex,
'horizontalLayout': workspace.horizontalLayout,
'renderer': workspace.options.renderer,
'rendererOverrides': workspace.options.rendererOverrides
}));
// Options takes in either 'end' or 'start'. This has already been parsed to
// be either 0 or 1, so set it after.
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;
var FlyoutClass = null;
if (workspace.horizontalLayout) {
FlyoutClass = Blockly.registry.getClassFromOptions(
Blockly.registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, workspace.options);
} else {
FlyoutClass = Blockly.registry.getClassFromOptions(
Blockly.registry.Type.FLYOUTS_VERTICAL_TOOLBOX, workspace.options);
}
if (!FlyoutClass) {
throw new Error('Blockly.VerticalFlyout, Blockly.HorizontalFlyout or your own' +
' custom flyout must be required.');
}
return new FlyoutClass(workspaceOptions);
};
/**
* Fills the toolbox with new toolbox items and removes any old contents.
* @param {!Blockly.utils.toolbox.ToolboxInfo} toolboxDef Object holding information
* for creating a toolbox.
* @package
*/
Blockly.Toolbox.prototype.render = function(toolboxDef) {
this.toolboxDef_ = toolboxDef;
for (var i = 0; i < this.contents_.length; i++) {
var toolboxItem = this.contents_[i];
if (toolboxItem) {
toolboxItem.dispose();
}
}
this.contents_ = [];
this.contentMap_ = {};
this.renderContents_(toolboxDef['contents']);
this.position();
};
/**
* Adds all the toolbox items to the toolbox.
* @param {!Array<!Blockly.utils.toolbox.ToolboxItemInfo>} toolboxDef Array
* holding objects containing information on the contents of the toolbox.
* @protected
*/
Blockly.Toolbox.prototype.renderContents_ = function(toolboxDef) {
// This is for performance reasons. By using document fragment we only have to
// add to the dom once.
var fragment = document.createDocumentFragment();
for (var i = 0, toolboxItemDef; (toolboxItemDef = toolboxDef[i]); i++) {
this.createToolboxItem_(toolboxItemDef, fragment);
}
this.contentsDiv_.appendChild(fragment);
};
/**
* Creates and renders the toolbox item.
* @param {Blockly.utils.toolbox.ToolboxItemInfo} toolboxItemDef Any information
* that can be used to create an item in the toolbox.
* @param {!DocumentFragment} fragment The document fragment to add the child
* toolbox elements to.
* @private
*/
Blockly.Toolbox.prototype.createToolboxItem_ = function(toolboxItemDef, fragment) {
var registryName = toolboxItemDef['kind'];
// Categories that are collapsible are created using a class registered under
// a diffferent name.
if (registryName.toUpperCase() == 'CATEGORY' &&
Blockly.utils.toolbox.isCategoryCollapsible(
/** @type {!Blockly.utils.toolbox.CategoryInfo} */(toolboxItemDef))) {
registryName = Blockly.CollapsibleToolboxCategory.registrationName;
}
var ToolboxItemClass = Blockly.registry.getClass(
Blockly.registry.Type.TOOLBOX_ITEM, registryName.toLowerCase());
if (ToolboxItemClass) {
var toolboxItem = new ToolboxItemClass(toolboxItemDef, this);
this.addToolboxItem_(toolboxItem);
toolboxItem.init();
var toolboxItemDom = toolboxItem.getDiv();
if (toolboxItemDom) {
fragment.appendChild(toolboxItemDom);
}
}
};
/**
* Adds an item to the toolbox.
* @param {!Blockly.IToolboxItem} toolboxItem The item in the toolbox.
* @protected
*/
Blockly.Toolbox.prototype.addToolboxItem_ = function(toolboxItem) {
this.contents_.push(toolboxItem);
this.contentMap_[toolboxItem.getId()] = toolboxItem;
if (toolboxItem.isCollapsible()) {
var collapsibleItem = /** @type {Blockly.ICollapsibleToolboxItem} */
(toolboxItem);
for (var i = 0, child; (child = collapsibleItem.getChildToolboxItems()[i]); i++) {
this.addToolboxItem_(child);
}
}
};
/**
* Gets the items in the toolbox.
* @return {!Array<!Blockly.IToolboxItem>} The list of items in the toolbox.
* @public
*/
Blockly.Toolbox.prototype.getToolboxItems = function() {
return this.contents_;
};
/**
* Adds a style on the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to add.
* @package
*/
Blockly.Toolbox.prototype.addStyle = function(style) {
Blockly.utils.dom.addClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Removes a style from the toolbox. Usually used to change the cursor.
* @param {string} style The name of the class to remove.
* @package
*/
Blockly.Toolbox.prototype.removeStyle = function(style) {
Blockly.utils.dom.removeClass(/** @type {!Element} */ (this.HtmlDiv), style);
};
/**
* Return the deletion rectangle for this toolbox.
* @return {?Blockly.utils.Rect} Rectangle in which to delete.
* @public
*/
Blockly.Toolbox.prototype.getClientRect = function() {
if (!this.HtmlDiv) {
return null;
}
// BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
var top = toolboxRect.top;
var bottom = top + toolboxRect.height;
var left = toolboxRect.left;
var right = left + toolboxRect.width;
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
return new Blockly.utils.Rect(-BIG_NUM, bottom, -BIG_NUM, BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
return new Blockly.utils.Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, right);
} else { // Right
return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM);
}
};
/**
* Gets the toolbox item with the given id.
* @param {string} id The id of the toolbox item.
* @return {?Blockly.IToolboxItem} The toolbox item with the given id, or null if
* no item exists.
* @public
*/
Blockly.Toolbox.prototype.getToolboxItemById = function(id) {
return this.contentMap_[id];
};
/**
* Gets the width of the toolbox.
* @return {number} The width of the toolbox.
* @public
*/
Blockly.Toolbox.prototype.getWidth = function() {
return this.width_;
};
/**
* Gets the height of the toolbox.
* @return {number} The width of the toolbox.
* @public
*/
Blockly.Toolbox.prototype.getHeight = function() {
return this.height_;
};
/**
* Gets the toolbox flyout.
* @return {?Blockly.IFlyout} The toolbox flyout.
* @public
*/
Blockly.Toolbox.prototype.getFlyout = function() {
return this.flyout_;
};
/**
* Gets the workspace for the toolbox.
* @return {!Blockly.WorkspaceSvg} The parent workspace for the toolbox.
* @public
*/
Blockly.Toolbox.prototype.getWorkspace = function() {
return this.workspace_;
};
/**
* Gets the selected item.
* @return {?Blockly.ISelectableToolboxItem} The selected item, or null if no item is
* currently selected.
* @public
*/
Blockly.Toolbox.prototype.getSelectedItem = function() {
return this.selectedItem_;
};
/**
* Gets the previously selected item.
* @return {?Blockly.ISelectableToolboxItem} The previously selected item, or null if no
* item was previously selected.
* @public
*/
Blockly.Toolbox.prototype.getPreviouslySelectedItem = function() {
return this.previouslySelectedItem_;
};
/**
* Gets whether or not the toolbox is horizontal.
* @return {boolean} True if the toolbox is horizontal, false if the toolbox is
* vertical.
* @public
*/
Blockly.Toolbox.prototype.isHorizontal = function() {
return this.horizontalLayout_;
};
/**
* Positions the toolbox based on whether it is a horizontal toolbox and whether
* the workspace is in rtl.
* @public
*/
Blockly.Toolbox.prototype.position = function() {
var toolboxDiv = this.HtmlDiv;
if (!toolboxDiv) {
// Not initialized yet.
return;
}
if (this.horizontalLayout_) {
toolboxDiv.style.left = '0';
toolboxDiv.style.height = 'auto';
toolboxDiv.style.width = '100%';
this.height_ = toolboxDiv.offsetHeight;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
toolboxDiv.style.top = '0';
} else { // Bottom
toolboxDiv.style.bottom = '0';
}
} else {
if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
toolboxDiv.style.right = '0';
} else { // Left
toolboxDiv.style.left = '0';
}
toolboxDiv.style.height = '100%';
this.width_ = toolboxDiv.offsetWidth;
}
this.flyout_.position();
};
/**
* Handles resizing the toolbox when a toolbox item resizes.
* @package
*/
Blockly.Toolbox.prototype.handleToolboxItemResize = function() {
// Reposition the workspace so that (0,0) is in the correct position relative
// to the new absolute edge (ie toolbox edge).
var workspace = this.workspace_;
var rect = this.HtmlDiv.getBoundingClientRect();
var newX = this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ?
workspace.scrollX + rect.width : 0;
var newY = this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ?
workspace.scrollY + rect.height : 0;
workspace.translate(newX, newY);
// Even though the div hasn't changed size, the visible workspace
// surface of the workspace has, so we may need to reposition everything.
Blockly.svgResize(workspace);
};
/**
* Unhighlights any previously selected item.
* @public
*/
Blockly.Toolbox.prototype.clearSelection = function() {
this.setSelectedItem(null);
};
/**
* Updates the category colours and background colour of selected categories.
* @package
*/
Blockly.Toolbox.prototype.refreshTheme = function() {
for (var i = 0; i < this.contents_.length; i++) {
var child = this.contents_[i];
if (child.refreshTheme) {
child.refreshTheme();
}
}
};
/**
* Updates the flyout's content without closing it. Should be used in response
* to a change in one of the dynamic categories, such as variables or
* procedures.
* @public
*/
Blockly.Toolbox.prototype.refreshSelection = function() {
if (this.selectedItem_ && this.selectedItem_.isSelectable() &&
this.selectedItem_.getContents().length) {
this.flyout_.show(this.selectedItem_.getContents());
}
};
/**
* Shows or hides the toolbox.
* @param {boolean} isVisible True if toolbox should be visible.
* @public
*/
Blockly.Toolbox.prototype.setVisible = function(isVisible) {
this.HtmlDiv.style.display = isVisible ? 'block' : 'none';
};
/**
* Sets the given item as selected.
* No-op if the item is not selectable.
* @param {?Blockly.IToolboxItem} newItem The toolbox item to select.
* @public
*/
Blockly.Toolbox.prototype.setSelectedItem = function(newItem) {
var oldItem = this.selectedItem_;
if ((!newItem && !oldItem) || (newItem && !newItem.isSelectable())) {
return;
}
newItem = /** @type {Blockly.ISelectableToolboxItem} */ (newItem);
if (this.shouldDeselectItem_(oldItem, newItem) && oldItem != null) {
this.deselectItem_(oldItem);
}
if (this.shouldSelectItem_(oldItem, newItem) && newItem != null) {
this.selectItem_(oldItem, newItem);
}
this.updateFlyout_(oldItem, newItem);
this.fireSelectEvent_(oldItem, newItem);
};
/**
* Decides whether the old item should be deselected.
* @param {?Blockly.ISelectableToolboxItem} oldItem The previously selected
* toolbox item.
* @param {?Blockly.ISelectableToolboxItem} newItem The newly selected toolbox
* item.
* @return {boolean} True if the old item should be deselected, false otherwise.
* @protected
*/
Blockly.Toolbox.prototype.shouldDeselectItem_ = function(oldItem, newItem) {
// Deselect the old item unless the old item is collapsible and has been
// previously clicked on.
return oldItem != null && (!oldItem.isCollapsible() || oldItem != newItem);
};
/**
* Decides whether the new item should be selected.
* @param {?Blockly.ISelectableToolboxItem} oldItem The previously selected
* toolbox item.
* @param {?Blockly.ISelectableToolboxItem} newItem The newly selected toolbox
* item.
* @return {boolean} True if the new item should be selected, false otherwise.
* @protected
*/
Blockly.Toolbox.prototype.shouldSelectItem_ = function(oldItem, newItem) {
// Select the new item unless the old item equals the new item.
return newItem != null && newItem != oldItem;
};
/**
* Deselects the given item, marks it as unselected, and updates aria state.
* @param {!Blockly.ISelectableToolboxItem} item The previously selected
* toolbox item which should be deselected.
* @protected
*/
Blockly.Toolbox.prototype.deselectItem_ = function(item) {
this.selectedItem_ = null;
this.previouslySelectedItem_ = item;
item.setSelected(false);
Blockly.utils.aria.setState(/** @type {!Element} */ (this.contentsDiv_),
Blockly.utils.aria.State.ACTIVEDESCENDANT, '');
};
/**
* Selects the given item, marks it selected, and updates aria state.
* @param {?Blockly.ISelectableToolboxItem} oldItem The previously selected
* toolbox item.
* @param {!Blockly.ISelectableToolboxItem} newItem The newly selected toolbox
* item.
* @protected
*/
Blockly.Toolbox.prototype.selectItem_ = function(oldItem, newItem) {
this.selectedItem_ = newItem;
this.previouslySelectedItem_ = oldItem;
newItem.setSelected(true);
Blockly.utils.aria.setState(/** @type {!Element} */ (this.contentsDiv_),
Blockly.utils.aria.State.ACTIVEDESCENDANT, newItem.getId());
};
/**
* Selects the toolbox item by its position in the list of toolbox items.
* @param {number} position The position of the item to select.
* @public
*/
Blockly.Toolbox.prototype.selectItemByPosition = function(position) {
if (position > -1 && position < this.contents_.length) {
var item = this.contents_[position];
if (item.isSelectable()) {
this.setSelectedItem(item);
}
}
};
/**
* Decides whether to hide or show the flyout depending on the selected item.
* @param {?Blockly.ISelectableToolboxItem} oldItem The previously selected toolbox item.
* @param {?Blockly.ISelectableToolboxItem} newItem The newly selected toolbox item.
* @protected
*/
Blockly.Toolbox.prototype.updateFlyout_ = function(oldItem, newItem) {
if ((oldItem == newItem && !newItem.isCollapsible()) || !newItem ||
!newItem.getContents().length) {
this.flyout_.hide();
} else {
this.flyout_.show(newItem.getContents());
this.flyout_.scrollToStart();
}
};
/**
* Emits an event when a new toolbox item is selected.
* @param {?Blockly.ISelectableToolboxItem} oldItem The previously selected
* toolbox item.
* @param {?Blockly.ISelectableToolboxItem} newItem The newly selected toolbox
* item.
* @private
*/
Blockly.Toolbox.prototype.fireSelectEvent_ = function(oldItem, newItem) {
var oldElement = oldItem && oldItem.getName();
var newElement = newItem && newItem.getName();
// In this case the toolbox closes, so the newElement should be null.
if (oldItem == newItem) {
newElement = null;
}
// TODO (#4187): Update Toolbox Events.
var event = new Blockly.Events.Ui(null, 'category',
oldElement, newElement);
event.workspaceId = this.workspace_.id;
Blockly.Events.fire(event);
};
/**
* Handles the given Blockly action on a toolbox.
* This is only triggered when keyboard accessibility mode is enabled.
* @param {!Blockly.Action} action The action to be handled.
* @return {boolean} True if the field handled the action, false otherwise.
* @package
*/
Blockly.Toolbox.prototype.onBlocklyAction = function(action) {
var selected = this.selectedItem_;
if (!selected) {
return false;
}
switch (action.name) {
case Blockly.navigation.actionNames.PREVIOUS:
return this.selectPrevious_();
case Blockly.navigation.actionNames.OUT:
return this.selectParent_();
case Blockly.navigation.actionNames.NEXT:
return this.selectNext_();
case Blockly.navigation.actionNames.IN:
return this.selectChild_();
default:
return false;
}
};
/**
* Closes the current item if it is expanded, or selects the parent.
* @return {boolean} True if a parent category was selected, false otherwise.
* @private
*/
Blockly.Toolbox.prototype.selectParent_ = function() {
if (!this.selectedItem_) {
return false;
}
if (this.selectedItem_.isCollapsible() && this.selectedItem_.isExpanded()) {
var collapsibleItem = /** @type {!Blockly.ICollapsibleToolboxItem} */ (this.selectedItem_);
collapsibleItem.setExpanded(false);
return true;
} else if (this.selectedItem_.getParent() &&
this.selectedItem_.getParent().isSelectable()) {
this.setSelectedItem(this.selectedItem_.getParent());
return true;
}
return false;
};
/**
* Selects the first child of the currently selected item, or nothing if the
* toolbox item has no children.
* @return {boolean} True if a child category was selected, false otherwise.
* @private
*/
Blockly.Toolbox.prototype.selectChild_ = function() {
if (!this.selectedItem_ || !this.selectedItem_.isCollapsible()) {
return false;
}
var collapsibleItem = /** @type {Blockly.ICollapsibleToolboxItem} */
(this.selectedItem_);
if (!collapsibleItem.isExpanded()) {
collapsibleItem.setExpanded(true);
return true;
} else {
this.selectNext_();
return true;
}
};
/**
* Selects the next visible toolbox item.
* @return {boolean} True if a next category was selected, false otherwise.
* @private
*/
Blockly.Toolbox.prototype.selectNext_ = function() {
if (!this.selectedItem_) {
return false;
}
var nextItemIdx = this.contents_.indexOf(this.selectedItem_) + 1;
if (nextItemIdx > -1 && nextItemIdx < this.contents_.length) {
var nextItem = this.contents_[nextItemIdx];
while (nextItem && !nextItem.isSelectable()) {
nextItem = this.contents_[++nextItemIdx];
}
if (nextItem && nextItem.isSelectable()) {
this.setSelectedItem(nextItem);
return true;
}
}
return false;
};
/**
* Selects the previous visible toolbox item.
* @return {boolean} True if a previous category was selected, false otherwise.
* @private
*/
Blockly.Toolbox.prototype.selectPrevious_ = function() {
if (!this.selectedItem_) {
return false;
}
var prevItemIdx = this.contents_.indexOf(this.selectedItem_) - 1;
if (prevItemIdx > -1 && prevItemIdx < this.contents_.length) {
var prevItem = this.contents_[prevItemIdx];
while (prevItem && !prevItem.isSelectable()) {
prevItem = this.contents_[--prevItemIdx];
}
if (prevItem && prevItem.isSelectable()) {
this.setSelectedItem(prevItem);
return true;
}
}
return false;
};
/**
* Disposes of this toolbox.
* @public
*/
Blockly.Toolbox.prototype.dispose = function() {
this.flyout_.dispose();
for (var i = 0; i < this.contents_.length; i++) {
var toolboxItem = this.contents_[i];
toolboxItem.dispose();
}
for (var j = 0; j < this.boundEvents_.length; j++) {
Blockly.unbindEvent_(this.boundEvents_[j]);
}
this.boundEvents_ = [];
this.contents_ = [];
this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);
Blockly.utils.dom.removeNode(this.HtmlDiv);
};
/**
* CSS for Toolbox. See css.js for use.
*/
Blockly.Css.register([
/* eslint-disable indent */
'.blocklyToolboxDelete {',
'cursor: url("<<<PATH>>>/handdelete.cur"), auto;',
'}',
'.blocklyToolboxGrab {',
'cursor: url("<<<PATH>>>/handclosed.cur"), auto;',
'cursor: grabbing;',
'cursor: -webkit-grabbing;',
'}',
/* Category tree in Toolbox. */
'.blocklyToolboxDiv {',
'background-color: #ddd;',
'overflow-x: visible;',
'overflow-y: auto;',
'padding: 4px 0 4px 0;',
'position: absolute;',
'z-index: 70;', /* so blocks go under toolbox when dragging */
'-webkit-tap-highlight-color: transparent;', /* issue #1345 */
'}',
'.blocklyToolboxContents {',
'display: flex;',
'flex-wrap: wrap;',
'flex-direction: column;',
'}',
'.blocklyToolboxContents:focus {',
'outline: none;',
'}',
/* eslint-enable indent */
]);
Blockly.registry.register(Blockly.registry.Type.TOOLBOX,
Blockly.registry.DEFAULT, Blockly.Toolbox);
+145
View File
@@ -0,0 +1,145 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview An item in the toolbox.
* @author aschmiedt@google.com (Abby Schmiedt)
*/
'use strict';
goog.provide('Blockly.ToolboxItem');
goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.IToolboxItem');
goog.requireType('Blockly.utils.toolbox');
goog.requireType('Blockly.WorkspaceSvg');
/**
* Class for an item in the toolbox.
* @param {!Blockly.utils.toolbox.ToolboxItemInfo} toolboxItemDef The JSON defining the
* toolbox item.
* @param {!Blockly.IToolbox} toolbox The toolbox that holds the toolbox item.
* @param {Blockly.ICollapsibleToolboxItem=} opt_parent The parent toolbox item
* or null if the category does not have a parent.
* @constructor
* @implements {Blockly.IToolboxItem}
*/
Blockly.ToolboxItem = function(toolboxItemDef, toolbox, opt_parent) {
/**
* The id for the category.
* @type {string}
* @protected
*/
this.id_ = toolboxItemDef['id'] || Blockly.utils.IdGenerator.getNextUniqueId();
/**
* The parent of the category.
* @type {?Blockly.ICollapsibleToolboxItem}
* @protected
*/
this.parent_ = opt_parent || null;
/**
* The level that the category is nested at.
* @type {number}
* @protected
*/
this.level_ = this.parent_ ? this.parent_.getLevel() + 1 : 0;
/**
* The JSON definition of the toolbox item.
* @type {!Blockly.utils.toolbox.ToolboxItemInfo}
* @protected
*/
this.toolboxItemDef_ = toolboxItemDef;
/**
* The toolbox this category belongs to.
* @type {!Blockly.IToolbox}
* @protected
*/
this.parentToolbox_ = toolbox;
/**
* The workspace of the parent toolbox.
* @type {!Blockly.WorkspaceSvg}
* @protected
*/
this.workspace_ = this.parentToolbox_.getWorkspace();
};
/**
* Initializes the toolbox item.
* This includes creating the dom and updating the state of any items based
* on the info object.
* @public
*/
Blockly.ToolboxItem.prototype.init = function() {
// No-op by default.
};
/**
* Gets the div for the toolbox item.
* @return {?Element} The div for the toolbox item.
* @public
*/
Blockly.ToolboxItem.prototype.getDiv = function() {
return null;
};
/**
* Gets a unique identifier for this toolbox item.
* @return {string} The id for the toolbox item.
* @public
*/
Blockly.ToolboxItem.prototype.getId = function() {
return this.id_;
};
/**
* Gets the parent if the toolbox item is nested.
* @return {?Blockly.IToolboxItem} The parent toolbox item, or null if
* this toolbox item is not nested.
* @public
*/
Blockly.ToolboxItem.prototype.getParent = function() {
return null;
};
/**
* Gets the nested level of the category.
* @return {number} The nested level of the category.
* @package
*/
Blockly.ToolboxItem.prototype.getLevel = function() {
return this.level_;
};
/**
* Whether the toolbox item is selectable.
* @return {boolean} True if the toolbox item can be selected.
* @public
*/
Blockly.ToolboxItem.prototype.isSelectable = function() {
return false;
};
/**
* Whether the toolbox item is collapsible.
* @return {boolean} True if the toolbox item is collapsible.
* @public
*/
Blockly.ToolboxItem.prototype.isCollapsible = function() {
return false;
};
/**
* Dispose of this toolbox item. No-op by default.
* @public
*/
Blockly.ToolboxItem.prototype.dispose = function() {
};
+14
View File
@@ -235,6 +235,20 @@ Blockly.utils.dom.addClass = function(element, className) {
return true;
};
/**
* Removes multiple calsses from an element.
* @param {!Element} element DOM element to remove classes from.
* @param {string} classNames A string of one or multiple class names for an
* element.
*/
Blockly.utils.dom.removeClasses = function(element, classNames) {
var classList = classNames.split(' ');
for (var i = 0; i < classList.length; i++) {
var cssName = classList[i];
Blockly.utils.dom.removeClass(element, cssName);
}
};
/**
* Remove a CSS class from a element.
* Similar to Closure's goog.dom.classes.remove, except it handles SVG elements.
+274 -53
View File
@@ -13,6 +13,9 @@
goog.provide('Blockly.utils.toolbox');
goog.requireType('Blockly.ToolboxCategory');
goog.requireType('Blockly.ToolboxSeparator');
/**
* The information needed to create a block in the toolbox.
* @typedef {{
@@ -23,16 +26,18 @@ goog.provide('Blockly.utils.toolbox');
* disabled: (?string|?boolean)
* }}
*/
Blockly.utils.toolbox.Block;
Blockly.utils.toolbox.BlockInfo;
/**
* The information needed to create a separator in the toolbox.
* @typedef {{
* kind:string,
* gap:?number
* id:?string,
* gap:?number,
* cssconfig:?Blockly.ToolboxSeparator.CssConfig
* }}
*/
Blockly.utils.toolbox.Separator;
Blockly.utils.toolbox.SeparatorInfo;
/**
* The information needed to create a button in the toolbox.
@@ -42,83 +47,269 @@ Blockly.utils.toolbox.Separator;
* callbackkey:string
* }}
*/
Blockly.utils.toolbox.Button;
Blockly.utils.toolbox.ButtonInfo;
/**
* The information needed to create a label in the toolbox.
* @typedef {{
* kind:string,
* id:?string,
* text:string
* }}
*/
Blockly.utils.toolbox.Label;
Blockly.utils.toolbox.LabelInfo;
/**
* The information needed to create either a button or a label in the flyout.
* @typedef {Blockly.utils.toolbox.ButtonInfo|
* Blockly.utils.toolbox.LabelInfo}
*/
Blockly.utils.toolbox.ButtonOrLabelInfo;
/**
* The information needed to create a category in the toolbox.
* @typedef {{
* kind:string,
* name:string,
* id:?string,
* categorystyle:?string,
* colour:?string,
* contents:Array.<Blockly.utils.toolbox.Toolbox>
* cssconfig:?Blockly.ToolboxCategory.CssConfig,
* contents:!Array<Blockly.utils.toolbox.ToolboxItemInfo>,
* hidden:?string
* }}
*/
Blockly.utils.toolbox.Category;
Blockly.utils.toolbox.StaticCategoryInfo;
/**
* The information needed to create a custom category.
* @typedef {{
* kind:string,
* custom:string,
* id:?string,
* categorystyle:?string,
* colour:?string,
* cssconfig:?Blockly.ToolboxCategory.CssConfig,
* hidden:?string
* }}
*/
Blockly.utils.toolbox.DynamicCategoryInfo;
/**
* The information needed to create either a dynamic or static category.
* @typedef {Blockly.utils.toolbox.StaticCategoryInfo|
* Blockly.utils.toolbox.DynamicCategoryInfo}
*/
Blockly.utils.toolbox.CategoryInfo;
/**
* Any information that can be used to create an item in the toolbox.
* @typedef {Blockly.utils.toolbox.Block|
* Blockly.utils.toolbox.Separator|
* Blockly.utils.toolbox.Button|
* Blockly.utils.toolbox.Label|
* Blockly.utils.toolbox.Category}
* @typedef {Blockly.utils.toolbox.FlyoutItemInfo|
* Blockly.utils.toolbox.StaticCategoryInfo}
*/
Blockly.utils.toolbox.Toolbox;
Blockly.utils.toolbox.ToolboxItemInfo;
/**
* All the different types that can be displayed in a flyout.
* @typedef {Blockly.utils.toolbox.BlockInfo|
* Blockly.utils.toolbox.SeparatorInfo|
* Blockly.utils.toolbox.ButtonInfo|
* Blockly.utils.toolbox.LabelInfo|
* Blockly.utils.toolbox.DynamicCategoryInfo}
*/
Blockly.utils.toolbox.FlyoutItemInfo;
/**
* The JSON definition of a toolbox.
* @typedef {{
* contents:!Array<Blockly.utils.toolbox.ToolboxItemInfo>
* }}
*/
Blockly.utils.toolbox.ToolboxInfo;
/**
* An array holding flyout items.
* @typedef {
* Array<!Blockly.utils.toolbox.FlyoutItemInfo>
* }
*/
Blockly.utils.toolbox.FlyoutItemInfoArray;
/**
* All of the different types that can create a toolbox.
* @typedef {Node|
* NodeList|
* Array.<Blockly.utils.toolbox.Toolbox>|
* Array.<Node>}
* Blockly.utils.toolbox.ToolboxInfo|
* string}
*/
Blockly.utils.toolbox.ToolboxDefinition;
/**
* All of the different types that can be used to show items in a flyout.
* @typedef {Blockly.utils.toolbox.FlyoutItemInfoArray|
* NodeList|
* Blockly.utils.toolbox.ToolboxInfo|
* Array<!Node>}
*/
Blockly.utils.toolbox.FlyoutDefinition;
/**
* Parse the provided toolbox definition into a consistent format.
* @param {Blockly.utils.toolbox.ToolboxDefinition} toolboxDef The definition of the
* toolbox in one of its many forms.
* @return {Array.<Blockly.utils.toolbox.Toolbox>} Array of JSON holding
* information on toolbox contents.
* The name used to identify a toolbox that has category like items.
* This only needs to be used if a toolbox wants to be treated like a category
* toolbox but does not actually contain any toolbox items with the kind
* 'category'.
* @const {string}
*/
Blockly.utils.toolbox.CATEGORY_TOOLBOX_KIND = 'categoryToolbox';
/**
* The name used to identify a toolbox that has no categories and is displayed
* as a simple flyout displaying blocks, buttons, or labels.
* @const {string}
*/
Blockly.utils.toolbox.FLYOUT_TOOLBOX_KIND = 'flyoutToolbox';
/**
* Converts the toolbox definition into toolbox JSON.
* @param {?Blockly.utils.toolbox.ToolboxDefinition} toolboxDef The definition
* of the toolbox in one of its many forms.
* @return {?Blockly.utils.toolbox.ToolboxInfo} Object holding information
* for creating a toolbox.
* @package
*/
Blockly.utils.toolbox.convertToolboxToJSON = function(toolboxDef) {
Blockly.utils.toolbox.convertToolboxDefToJson = function(toolboxDef) {
if (!toolboxDef) {
return null;
}
// If it is an array of JSON, then it is already in the correct format.
if (Array.isArray(toolboxDef) && toolboxDef.length && !(toolboxDef[0].nodeType)) {
if (Blockly.utils.toolbox.hasCategories(toolboxDef)) {
// TODO: Remove after #3985 has been looked into.
console.warn('Due to some performance issues, defining a toolbox using' +
' JSON is not ready yet. Please define your toolbox using xml.');
}
return /** @type {!Array.<Blockly.utils.toolbox.Toolbox>} */ (toolboxDef);
if (toolboxDef instanceof Element || typeof toolboxDef == 'string') {
toolboxDef = Blockly.utils.toolbox.parseToolboxTree(toolboxDef);
toolboxDef = Blockly.utils.toolbox.convertToToolboxJson_(toolboxDef);
}
return Blockly.utils.toolbox.toolboxXmlToJson_(toolboxDef);
var toolboxJson = /** @type {Blockly.utils.toolbox.ToolboxInfo} */ (toolboxDef);
Blockly.utils.toolbox.validateToolbox_(toolboxJson);
return toolboxJson;
};
/**
* Convert the xml for a toolbox to JSON.
* @param {!NodeList|!Node|!Array.<Node>} toolboxDef The
* definition of the toolbox in one of its many forms.
* @return {!Array.<Blockly.utils.toolbox.Toolbox>} A list of objects in the
* toolbox.
* Validates the toolbox JSON fields have been set correctly.
* @param {Blockly.utils.toolbox.ToolboxInfo} toolboxJson Object holding
* information for creating a toolbox.
* @throws {Error} if the toolbox is not the correct format.
* @private
*/
Blockly.utils.toolbox.toolboxXmlToJson_ = function(toolboxDef) {
Blockly.utils.toolbox.validateToolbox_ = function(toolboxJson) {
var toolboxKind = toolboxJson['kind'];
var toolboxContents = toolboxJson['contents'];
if (toolboxKind) {
if (toolboxKind != Blockly.utils.toolbox.FLYOUT_TOOLBOX_KIND &&
toolboxKind != Blockly.utils.toolbox.CATEGORY_TOOLBOX_KIND) {
throw Error('Invalid toolbox kind ' + toolboxKind + '.' +
' Please supply either ' +
Blockly.utils.toolbox.FLYOUT_TOOLBOX_KIND + ' or ' +
Blockly.utils.toolbox.CATEGORY_TOOLBOX_KIND);
}
}
if (!toolboxContents) {
throw Error('Toolbox must have a contents attribute.');
}
};
/**
* Converts the flyout definition into a list of flyout items.
* @param {?Blockly.utils.toolbox.FlyoutDefinition} flyoutDef The definition of
* the flyout in one of its many forms.
* @return {!Blockly.utils.toolbox.FlyoutItemInfoArray} A list of flyout items.
* @package
*/
Blockly.utils.toolbox.convertFlyoutDefToJsonArray = function(flyoutDef) {
if (!flyoutDef) {
return [];
}
if (flyoutDef['contents']) {
return flyoutDef['contents'];
}
// If it is already in the correct format return the flyoutDef.
if (Array.isArray(flyoutDef) && flyoutDef.length > 0 &&
!flyoutDef[0].nodeType) {
return flyoutDef;
}
return Blockly.utils.toolbox.xmlToJsonArray_(
/** @type {!Array<Node>|!NodeList} */ (flyoutDef));
};
/**
* Whether or not the toolbox definition has categories.
* @param {?Blockly.utils.toolbox.ToolboxInfo} toolboxJson Object holding
* information for creating a toolbox.
* @return {boolean} True if the toolbox has categories.
* @package
*/
Blockly.utils.toolbox.hasCategories = function(toolboxJson) {
if (!toolboxJson) {
return false;
}
var toolboxKind = toolboxJson['kind'];
if (toolboxKind) {
return toolboxKind == Blockly.utils.toolbox.CATEGORY_TOOLBOX_KIND;
}
var categories = toolboxJson['contents'].filter(function(item) {
return item['kind'].toUpperCase() == 'CATEGORY';
});
return !!categories.length;
};
/**
* Whether or not the category is collapsible.
* @param {!Blockly.utils.toolbox.CategoryInfo} categoryInfo Object holing
* information for creating a category.
* @return {boolean} True if the category has subcategories.
* @package
*/
Blockly.utils.toolbox.isCategoryCollapsible = function(categoryInfo) {
if (!categoryInfo || !categoryInfo['contents']) {
return false;
}
var categories = categoryInfo['contents'].filter(function(item) {
return item['kind'].toUpperCase() == 'CATEGORY';
});
return !!categories.length;
};
/**
* Parses the provided toolbox definition into a consistent format.
* @param {Node} toolboxDef The definition of the toolbox in one of its many forms.
* @return {!Blockly.utils.toolbox.ToolboxInfo} Object holding information
* for creating a toolbox.
* @private
*/
Blockly.utils.toolbox.convertToToolboxJson_ = function(toolboxDef) {
var contents = Blockly.utils.toolbox.xmlToJsonArray_(
/** @type {!Node|!Array<Node>} */ (toolboxDef));
var toolboxJson = {'contents': contents};
if (toolboxDef instanceof Node) {
Blockly.utils.toolbox.addAttributes_(toolboxDef, toolboxJson);
}
return toolboxJson;
};
/**
* Converts the xml for a toolbox to JSON.
* @param {!Node|!Array<Node>|!NodeList} toolboxDef The
* definition of the toolbox in one of its many forms.
* @return {!Blockly.utils.toolbox.FlyoutItemInfoArray|
* !Array<Blockly.utils.toolbox.ToolboxItemInfo>} A list of objects in
* the toolbox.
* @private
*/
Blockly.utils.toolbox.xmlToJsonArray_ = function(toolboxDef) {
var arr = [];
// If it is a node it will have children.
var childNodes = toolboxDef.childNodes;
@@ -139,31 +330,61 @@ Blockly.utils.toolbox.toolboxXmlToJson_ = function(toolboxDef) {
obj['blockxml'] = child;
} else if (tagName == 'CATEGORY') {
// Get the contents of a category
obj['contents'] = Blockly.utils.toolbox.toolboxXmlToJson_(child);
obj['contents'] = Blockly.utils.toolbox.xmlToJsonArray_(child);
}
// Add xml attributes to object
for (var j = 0; j < child.attributes.length; j++) {
var attr = child.attributes[j];
obj[attr.nodeName] = attr.value;
}
Blockly.utils.toolbox.addAttributes_(child, obj);
arr.push(obj);
}
return arr;
};
/**
* Whether or not the toolbox definition has categories or not.
* @param {Node|Array.<Blockly.utils.toolbox.Toolbox>} toolboxDef The definition
* of the toolbox. Either in xml or JSON.
* @return {boolean} True if the toolbox has categories.
* @package
* Adds the attributes on the node to the given object.
* @param {!Node} node The node to copy the attributes from.
* @param {!Object} obj The object to copy the attributes to.
* @private
*/
Blockly.utils.toolbox.hasCategories = function(toolboxDef) {
if (Array.isArray(toolboxDef)) {
// Search for categories
return !!(toolboxDef.length && toolboxDef[0]['kind'].toUpperCase() == 'CATEGORY');
} else {
return !!(toolboxDef && toolboxDef.getElementsByTagName('category').length);
Blockly.utils.toolbox.addAttributes_ = function(node, obj) {
for (var j = 0; j < node.attributes.length; j++) {
var attr = node.attributes[j];
if (attr.nodeName.indexOf('css-') > -1) {
obj['cssconfig'] = obj['cssconfig'] || {};
obj['cssconfig'][attr.nodeName.replace('css-', '')] = attr.value;
} else {
obj[attr.nodeName] = attr.value;
}
}
};
/**
* Parse the provided toolbox tree into a consistent DOM format.
* @param {?Node|?string} toolboxDef DOM tree of blocks, or text representation
* of same.
* @return {?Node} DOM tree of blocks, or null.
*/
Blockly.utils.toolbox.parseToolboxTree = function(toolboxDef) {
if (toolboxDef) {
if (typeof toolboxDef != 'string') {
if (Blockly.utils.userAgent.IE && toolboxDef.outerHTML) {
// In this case the tree will not have been properly built by the
// browser. The HTML will be contained in the element, but it will
// not have the proper DOM structure since the browser doesn't support
// XSLTProcessor (XML -> HTML).
toolboxDef = toolboxDef.outerHTML;
} else if (!(toolboxDef instanceof Element)) {
toolboxDef = null;
}
}
if (typeof toolboxDef == 'string') {
toolboxDef = Blockly.Xml.textToDom(toolboxDef);
if (toolboxDef.nodeName.toLowerCase() != 'xml') {
throw TypeError('Toolbox should be an <xml> document.');
}
}
} else {
toolboxDef = null;
}
return toolboxDef;
};
+12 -13
View File
@@ -1755,16 +1755,14 @@ Blockly.WorkspaceSvg.prototype.showContextMenu = function(e) {
/**
* Modify the block tree on the existing toolbox.
* @param {Blockly.utils.toolbox.ToolboxDefinition|string} toolboxDef
* DOM tree of toolbox contents, string of toolbox contents, or array of JSON
* representing toolbox contents.
* @param {?Blockly.utils.toolbox.ToolboxDefinition} toolboxDef
* DOM tree of toolbox contents, string of toolbox contents, or JSON
* representing toolbox definition.
*/
Blockly.WorkspaceSvg.prototype.updateToolbox = function(toolboxDef) {
if (!Array.isArray(toolboxDef)) {
toolboxDef = Blockly.Options.parseToolboxTree(toolboxDef);
}
toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(toolboxDef);
if (!toolboxDef) {
var parsedToolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(toolboxDef);
if (!parsedToolboxDef) {
if (this.options.languageTree) {
throw Error('Can\'t nullify an existing toolbox.');
}
@@ -1773,18 +1771,19 @@ Blockly.WorkspaceSvg.prototype.updateToolbox = function(toolboxDef) {
if (!this.options.languageTree) {
throw Error('Existing toolbox is null. Can\'t create new toolbox.');
}
if (Blockly.utils.toolbox.hasCategories(toolboxDef)) {
if (Blockly.utils.toolbox.hasCategories(parsedToolboxDef)) {
if (!this.toolbox_) {
throw Error('Existing toolbox has no categories. Can\'t change mode.');
}
this.options.languageTree = toolboxDef;
this.toolbox_.render(toolboxDef);
this.options.languageTree = parsedToolboxDef;
this.toolbox_.render(parsedToolboxDef);
} else {
if (!this.flyout_) {
throw Error('Existing toolbox has categories. Can\'t change mode.');
}
this.options.languageTree = toolboxDef;
this.flyout_.show(toolboxDef);
this.options.languageTree = parsedToolboxDef;
this.flyout_.show(parsedToolboxDef);
}
};
+1 -1
View File
@@ -27,11 +27,11 @@ function typings() {
const blocklySrcs = [
"core/",
"core/components",
"core/components/tree",
"core/keyboard_nav",
"core/renderers/common",
"core/renderers/measurables",
"core/theme",
"core/toolbox",
"core/interfaces",
"core/utils",
"msg/"
+7
View File
@@ -28,7 +28,14 @@
"dispatchPointerEvent": true,
"createFireChangeListenerSpy": true,
"createGenUidStubWithReturns": true,
"getBasicToolbox": true,
"getCategoryJSON": true,
"getChildItem": true,
"getCollapsibleItem": true,
"getDeeplyNestedJSON": true,
"getInjectedToolbox": true,
"getNonCollapsibleItem": true,
"getSeparator": true,
"getSimpleJSON": true,
"getXmlArray": true,
"sharedTestSetup": true,
+9 -11
View File
@@ -120,7 +120,7 @@
</xml>
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-categories" style="display: none">
<category name="First">
<category name="First" css-container="something">
<block type="basic_block">
<field name="TEXT">FirstCategory-FirstBlock</field>
</block>
@@ -139,20 +139,18 @@
<category name="First" expanded="true" categorystyle="logic_category">
<sep gap="-1"></sep>
<button text="insert" callbackkey="insertConnectionRows"></button>
<block type="basic_block">
<field name="TEXT">FirstCategory-FirstBlock</field>
</block>
<block type="basic_block">
<field name="TEXT">FirstCategory-SecondBlock</field>
</block>
<block type="stack_block"></block>
<block type="stack_block"></block>
</category>
<category name="Second">
<block type="basic_block">
<field name="TEXT">SecondCategory-FirstBlock</field>
</block>
<block type="stack_block"></block>
</category>
<sep gap="-1"></sep>
<sep id="separator" gap="-1"></sep>
<category name="Variables" custom="VARIABLE"></category>
<category name="NestedCategory" >
<category id="nestedCategory" name="NestedItemOne"></category>
</category>
<category name="lastItem"></category>
</xml>
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox-connections" style="display: none">
<block type="stack_block"></block>
+18 -55
View File
@@ -51,9 +51,6 @@ suite('Navigation', function() {
return false;
}
};
this.firstCategory_ = this.workspace.getToolbox().tree_.getChildAt(0);
this.secondCategory_ = this.firstCategory_.getNextShownNode();
});
teardown(function() {
@@ -61,60 +58,26 @@ suite('Navigation', function() {
delete Blockly.Blocks['basic_block'];
});
test('Next', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.S;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(),
this.secondCategory_);
});
function testToolboxSelectMethodCalled(ws, mockEvent, keyCode, selectMethodName) {
mockEvent.keyCode = keyCode;
var toolbox = ws.getToolbox();
toolbox.selectedItem_ = toolbox.contents_[0];
var selectNextStub = sinon.stub(toolbox, selectMethodName);
Blockly.navigation.onKeyPress(mockEvent);
sinon.assert.called(selectNextStub);
}
// Should be a no-op.
test('Next at end', function() {
this.workspace.getToolbox().tree_.getSelectedItem().selectNext();
this.mockEvent.keyCode = Blockly.utils.KeyCodes.S;
// Go forward one so that we can go back one.
Blockly.navigation.onKeyPress(this.mockEvent);
var startCategory = this.workspace.getToolbox().tree_.getSelectedItem();
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(),
startCategory);
test('Calls toolbox selectNext_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.S, 'selectNext_');
});
test('Previous', function() {
// Go forward one so that we can go back one:
this.workspace.getToolbox().tree_.getSelectedItem().selectNext();
this.mockEvent.keyCode = Blockly.utils.KeyCodes.W;
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(),
this.secondCategory_);
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(),
this.firstCategory_);
test('Calls toolbox selectPrevious_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.W, 'selectPrevious_');
});
// Should be a no-op.
test('Previous at start', function() {
var startCategory = this.workspace.getToolbox().tree_.getSelectedItem();
this.mockEvent.keyCode = Blockly.utils.KeyCodes.W;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(),
startCategory);
test('Calls toolbox selectParent_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.D, 'selectChild_');
});
test('Out', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.A;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
// TODO (fenichel/aschmiedt): Decide whether out should go to the
// workspace.
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
test('Calls toolbox selectChild_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.A, 'selectParent_');
});
test('Go to flyout', function() {
@@ -255,7 +218,7 @@ suite('Navigation', function() {
}]);
this.workspace = createNavigationWorkspace(true);
this.basicBlock = this.workspace.newBlock('basic_block');
this.firstCategory_ = this.workspace.getToolbox().tree_.getChildAt(0);
this.firstCategory_ = this.workspace.getToolbox().contents_[0];
this.mockEvent = {
getModifierState: function() {
return false;
@@ -337,7 +300,7 @@ suite('Navigation', function() {
test('Toolbox', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.T;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(this.workspace.getToolbox().tree_.getSelectedItem(), this.firstCategory_);
chai.assert.equal(this.workspace.getToolbox().getSelectedItem(), this.firstCategory_);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
});
+111 -7
View File
@@ -6,13 +6,16 @@
/**
* Get JSON for a toolbox that contains categories.
* @return {Array.<Blockly.utils.toolbox.Toolbox>} The array holding information
* @return {Blockly.utils.toolbox.ToolboxJson} The array holding information
* for a toolbox.
*/
function getCategoryJSON() {
return [
return {"contents": [
{
"kind": "CATEGORY",
"cssconfig": {
"container": "something"
},
"contents": [
{
"kind": "BLOCK",
@@ -34,16 +37,16 @@ function getCategoryJSON() {
}
],
"name": "Second"
}];
}]};
}
/**
* Get JSON for a simple toolbox.
* @return {Array.<Blockly.utils.toolbox.Toolbox>} The array holding information
* @return {Blockly.utils.toolbox.ToolboxJson} The array holding information
* for a simple toolbox.
*/
function getSimpleJSON() {
return [
return {"contents":[
{
"kind":"BLOCK",
"blockxml": "<block type=\"logic_operation\"></block>",
@@ -62,12 +65,56 @@ function getSimpleJSON() {
"kind":"LABEL",
"text":"tooltips"
}
];
]};
}
/**
* Get JSON for a toolbox that contains categories that contain categories.
* @return {Blockly.utils.toolbox.ToolboxJson} The array holding information
* for a toolbox.
*/
function getDeeplyNestedJSON() {
return {"contents": [
{
"kind": "CATEGORY",
"cssconfig": {
"container": "something"
},
"contents": [{
"kind": "CATEGORY",
"contents": [{
"kind": "CATEGORY",
"contents": [
{
"kind": "BLOCK",
"blockxml": '<block type="basic_block"><field name="TEXT">NestedCategory-FirstBlock</field></block>'
},
{
"kind": "BLOCK",
"blockxml": '<block type="basic_block"><field name="TEXT">NestedCategory-SecondBlock</field></block>'
}
],
"name": "NestedCategoryInner"
}],
"name": "NestedCategoryMiddle",
}],
"name": "NestedCategoryOuter"
},
{
"kind": "CATEGORY",
"contents": [
{
"kind": "BLOCK",
"blockxml": '<block type="basic_block"><field name="TEXT">SecondCategory-FirstBlock</field></block>'
}
],
"name": "Second"
}]};
}
/**
* Get an array filled with xml elements.
* @return {Array.<Nodde>} Array holding xml elements for a toolbox.
* @return {Array<Node>} Array holding xml elements for a toolbox.
*/
function getXmlArray() {
// Need to use HTMLElement instead of Element so parser output is
@@ -79,3 +126,60 @@ function getXmlArray() {
var label = Blockly.Xml.textToDom('<label text="tooltips"></label>');
return [block, separator, button, label];
}
function getInjectedToolbox() {
/**
* Category: First
* sep
* basic_block
* basic_block
* Category: second
* basic_block
* Category: Variables
* custom: VARIABLE
* Category: NestedCategory
* Category: NestedItemOne
*/
var toolboxXml = document.getElementById('toolbox-test');
var workspace = Blockly.inject('blocklyDiv',
{
toolbox: toolboxXml
});
return workspace.getToolbox();
}
function getBasicToolbox() {
var workspace = new Blockly.WorkspaceSvg(new Blockly.Options({}));
var toolbox = new Blockly.Toolbox(workspace);
toolbox.HtmlDiv = document.createElement('div');
toolbox.flyout_ = sinon.createStubInstance(Blockly.VerticalFlyout);
return toolbox;
}
function getCollapsibleItem(toolbox) {
var contents = toolbox.contents_;
for (var i = 0; i < contents.length; i++) {
var item = contents[i];
if (item.isCollapsible()) {
return item;
}
}
}
function getNonCollapsibleItem(toolbox) {
var contents = toolbox.contents_;
for (var i = 0; i < contents.length; i++) {
var item = contents[i];
if (!item.isCollapsible()) {
return item;
}
}
}
function getChildItem(toolbox) {
return toolbox.getToolboxItemById('nestedCategory');
}
function getSeparator(toolbox) {
return toolbox.getToolboxItemById('separator');
}
+479 -212
View File
@@ -5,112 +5,92 @@
*/
suite('Toolbox', function() {
setup(function() {
sharedTestSetup.call(this);
Blockly.defineBlocksWithJsonArray([{
"type": "basic_block",
"message0": "%1",
"args0": [
{
"type": "field_input",
"name": "TEXT",
"text": "default"
}
]
}]);
this.toolboxXml = document.getElementById('toolbox-test');
this.workspace = Blockly.inject('blocklyDiv',
{
toolbox: this.toolboxXml
});
this.toolbox = this.workspace.getToolbox();
defineStackBlock();
});
teardown(function() {
sharedTestTeardown.call(this);
delete Blockly.Blocks['basic_block'];
delete Blockly.Blocks['row_block'];
});
suite('init', function() {
setup(function() {
this.toolbox.init();
this.toolbox = getInjectedToolbox();
});
test('HtmlDiv is created', function() {
teardown(function() {
this.toolbox.dispose();
});
test('Init called -> HtmlDiv is created', function() {
chai.assert.isDefined(this.toolbox.HtmlDiv);
});
test('HtmlDiv is inserted before parent node', function() {
test('Init called -> HtmlDiv is inserted before parent node', function() {
var toolboxDiv = Blockly.getMainWorkspace().getInjectionDiv().childNodes[0];
chai.assert.equal(toolboxDiv.className, 'blocklyToolboxDiv blocklyNonSelectable');
chai.assert.equal(toolboxDiv.className,
'blocklyToolboxDiv blocklyNonSelectable');
});
test('hideChaff is called when the toolbox is clicked', function() {
var hideChaffStub = sinon.stub(Blockly, "hideChaff");
var evt = new MouseEvent('pointerdown', {});
this.toolbox.HtmlDiv.dispatchEvent(evt);
sinon.assert.calledOnce(hideChaffStub);
test('Init called -> Toolbox is subscribed to background and foreground colour', function() {
var themeManager = this.toolbox.workspace_.getThemeManager();
var themeManagerSpy = sinon.spy(themeManager, 'subscribe');
this.toolbox.init();
sinon.assert.calledWith(themeManagerSpy, this.toolbox.HtmlDiv,
'toolboxBackgroundColour', 'background-color');
sinon.assert.calledWith(themeManagerSpy, this.toolbox.HtmlDiv,
'toolboxForegroundColour', 'color');
});
test('Flyout is initialized', function() {
test('Init called -> Render is called', function() {
var renderSpy = sinon.spy(this.toolbox, 'render');
this.toolbox.init();
sinon.assert.calledOnce(renderSpy);
});
test('Init called -> Flyout is initialized', function() {
this.toolbox.init();
chai.assert.isDefined(this.toolbox.flyout_);
});
});
suite('render', function() {
setup(function() {
this.toolboxXml = Blockly.utils.toolbox.convertToolboxToJSON(this.toolboxXml);
this.toolbox.selectFirstCategory();
this.firstChild = this.toolbox.tree_.getChildAt(0);
this.secondChild = this.toolbox.tree_.getChildAt(1);
this.toolbox.handleBeforeTreeSelected_(this.secondChild);
this.toolbox = getInjectedToolbox();
});
test('Tree is created and set', function() {
this.toolbox.render(this.toolboxXml);
chai.assert.isDefined(this.toolbox.tree_);
teardown(function() {
this.toolbox.dispose();
});
test('Throws error if a toolbox has both blocks and categories at root level', function() {
test('Render called with valid toolboxDef -> Contents are created', function() {
var positionStub = sinon.stub(this.toolbox, 'position');
this.toolbox.render({'contents': [
{'kind': 'category', 'contents': []},
{'kind': 'category', 'contents': []}
]});
chai.assert.lengthOf(this.toolbox.contents_, 2);
sinon.assert.called(positionStub);
});
// TODO: Uncomment once implemented.
test.skip('Toolbox definition with both blocks and categories -> Should throw an error', function() {
var toolbox = this.toolbox;
var badToolboxDef = [
{
"kind": "block",
"blockxml": "<block type='controls_if'></block>"
"kind": "block"
},
{
"kind": "category",
"name": "loops",
"categorystyle": "math_category",
"contents": [
{
"kind": "block",
"blockxml": "<block type='controls_if'></block>"
},
{
"kind": "button",
"text": "insert",
"callbackkey":"insertConnectionRows"
},
{
"kind": "label",
"text": "Something"
}
]
}
];
chai.assert.throws(function() {
toolbox.render(badToolboxDef);
toolbox.render({'contents' : badToolboxDef});
}, 'Toolbox cannot have both blocks and categories in the root level.');
});
test('Select any open nodes', function() {
// TODO: Uncomment once implemented.
test.skip('Expanded set to true for a non collapsible toolbox item -> Should open flyout', function() {
this.toolbox.render(this.toolboxXml);
var selectedNode = this.toolbox.tree_.children_[0];
chai.assert.isTrue(selectedNode.selected_);
});
test('Set the state for horizontal layout ', function() {
this.toolbox.horizontalLayout_ = true;
this.toolbox.render(this.toolboxXml);
var orientationAttribute = this.toolbox.tree_.getElement()
.getAttribute('aria-orientation');
chai.assert.equal(orientationAttribute, 'horizontal');
});
test('Create a toolbox from JSON', function() {
var jsonDef = [
test('JSON toolbox definition -> Should create toolbox with contents', function() {
var jsonDef = {'contents' : [
{
"kind": "category",
"contents": [
@@ -135,88 +115,335 @@ suite('Toolbox', function() {
}
]
}
];
]};
this.toolbox.render(jsonDef);
chai.assert.lengthOf(this.toolbox.tree_.children_, 1);
chai.assert.lengthOf(this.toolbox.contents_, 1);
});
});
suite('handleBeforeTreeSelected_', function() {
suite('onClick_', function() {
setup(function() {
this.toolbox.selectFirstCategory();
this.firstChild = this.toolbox.tree_.getChildAt(0);
this.secondChild = this.toolbox.tree_.getChildAt(1);
this.toolbox.handleBeforeTreeSelected_(this.secondChild);
this.toolbox = getInjectedToolbox();
});
test('Clear the previously selected category', function() {
chai.assert.equal(this.firstChild.getRowElement().style.backgroundColor, '');
teardown(function() {
this.toolbox.dispose();
});
test('Set color for new selected category', function() {
chai.assert.equal(this.secondChild.getRowElement().style.backgroundColor,
'rgb(85, 119, 238)');
test('Toolbox clicked -> Should close flyout', function() {
var hideChaffStub = sinon.stub(Blockly, "hideChaff");
var evt = new MouseEvent('pointerdown', {});
this.toolbox.HtmlDiv.dispatchEvent(evt);
sinon.assert.calledOnce(hideChaffStub);
});
test('Category clicked -> Should select category', function() {
var categoryXml = document.getElementsByClassName('blocklyTreeRow')[0];
var evt = {
'srcElement': categoryXml
};
var item = this.toolbox.contentMap_[categoryXml.getAttribute('id')];
var setSelectedSpy = sinon.spy(this.toolbox, 'setSelectedItem');
var onClickSpy = sinon.spy(item, 'onClick');
this.toolbox.onClick_(evt);
sinon.assert.calledOnce(setSelectedSpy);
sinon.assert.calledOnce(onClickSpy);
});
});
suite('handleAfterTreeSelected_', function() {
suite('onKeyDown_', function() {
setup(function() {
this.toolbox.selectFirstCategory();
this.firstChild = this.toolbox.tree_.getChildAt(0);
this.secondChild = this.toolbox.tree_.getChildAt(1);
this.showStub = sinon.stub(this.toolbox.flyout_, "show");
this.toolbox = getInjectedToolbox();
});
test('Show the new set of blocks in the flyout', function() {
this.toolbox.handleAfterTreeSelected_(this.firstChild, this.secondChild);
sinon.assert.calledWith(this.showStub, this.secondChild.contents);
teardown(function() {
this.toolbox.dispose();
});
test('Opening the previous selected category does not scroll', function() {
var scrollStub = sinon.stub(this.toolbox.flyout_, "scrollToStart");
this.toolbox.handleAfterTreeSelected_(null, this.firstChild);
sinon.assert.notCalled(scrollStub);
function createKeyDownMock(keyCode) {
return {
'keyCode': keyCode,
'preventDefault': function() {}
};
}
function testCorrectFunctionCalled(toolbox, keyCode, funcName) {
var event = createKeyDownMock(keyCode);
var preventDefaultEvent = sinon.stub(event, 'preventDefault');
var selectMethodStub = sinon.stub(toolbox, funcName);
selectMethodStub.returns(true);
toolbox.onKeyDown_(event);
sinon.assert.called(selectMethodStub);
sinon.assert.called(preventDefaultEvent);
}
test('Down button is pushed -> Should call selectNext_', function() {
testCorrectFunctionCalled(this.toolbox, Blockly.utils.KeyCodes.DOWN, 'selectNext_', true);
});
test('Opening new category scrolls to top', function() {
var scrollStub = sinon.stub(this.toolbox.flyout_, "scrollToStart");
this.toolbox.handleAfterTreeSelected_(null, this.secondChild);
sinon.assert.calledOnce(scrollStub);
test('Up button is pushed -> Should call selectPrevious_', function() {
testCorrectFunctionCalled(this.toolbox, Blockly.utils.KeyCodes.UP, 'selectPrevious_', true);
});
test('Clicking selected category closes flyout', function() {
var flyoutHideStub = sinon.stub(this.toolbox.flyout_, "hide");
this.toolbox.handleAfterTreeSelected_(this.firstChild);
sinon.assert.calledOnce(flyoutHideStub);
test('Left button is pushed -> Should call selectParent_', function() {
testCorrectFunctionCalled(this.toolbox, Blockly.utils.KeyCodes.LEFT, 'selectParent_', true);
});
test('UI Event is fired when new category is selected', function() {
this.eventsFireStub.resetHistory();
this.toolbox.handleAfterTreeSelected_(this.firstChild);
sinon.assert.calledOnce(this.eventsFireStub);
test('Right button is pushed -> Should call selectChild_', function() {
testCorrectFunctionCalled(this.toolbox, Blockly.utils.KeyCodes.RIGHT, 'selectChild_', true);
});
test('Last category is updated when there is a new node', function() {
this.toolbox.handleAfterTreeSelected_(this.firstChild, this.secondChild);
chai.assert.equal(this.toolbox.lastCategory_, this.secondChild);
test('Enter button is pushed -> Should toggle expandedd', function() {
this.toolbox.selectedItem_ = getCollapsibleItem(this.toolbox);
var toggleExpandedStub = sinon.stub(this.toolbox.selectedItem_, 'toggleExpanded');
var event = createKeyDownMock(Blockly.utils.KeyCodes.ENTER);
var preventDefaultEvent = sinon.stub(event, 'preventDefault');
this.toolbox.onKeyDown_(event);
sinon.assert.called(toggleExpandedStub);
sinon.assert.called(preventDefaultEvent);
});
test('Enter button is pushed when no item is selected -> Should not call prevent default', function() {
this.toolbox.selectedItem_ = null;
var event = createKeyDownMock(Blockly.utils.KeyCodes.ENTER);
var preventDefaultEvent = sinon.stub(event, 'preventDefault');
this.toolbox.onKeyDown_(event);
sinon.assert.notCalled(preventDefaultEvent);
});
});
suite('Select Methods', function() {
setup(function() {
this.toolbox = getInjectedToolbox();
});
teardown(function() {
this.toolbox.dispose();
});
suite('selectChild_', function() {
test('No item is selected -> Should not handle event', function() {
this.toolbox.selectedItem_ = null;
var handled = this.toolbox.selectChild_();
chai.assert.isFalse(handled);
});
test('Selected item is not collapsible -> Should not handle event', function() {
this.toolbox.selectedItem_ = getNonCollapsibleItem(this.toolbox);
var handled = this.toolbox.selectChild_();
chai.assert.isFalse(handled);
});
test('Selected item is collapsible -> Should expand', function() {
var collapsibleItem = getCollapsibleItem(this.toolbox);
this.toolbox.selectedItem_ = collapsibleItem;
var handled = this.toolbox.selectChild_();
chai.assert.isTrue(handled);
chai.assert.isTrue(collapsibleItem.isExpanded());
chai.assert.equal(this.toolbox.selectedItem_, collapsibleItem);
});
test('Selected item is expanded -> Should select child', function() {
var collapsibleItem = getCollapsibleItem(this.toolbox);
collapsibleItem.expanded_ = true;
var selectNextStub = sinon.stub(this.toolbox, 'selectNext_');
this.toolbox.selectedItem_ = collapsibleItem;
var handled = this.toolbox.selectChild_();
chai.assert.isTrue(handled);
sinon.assert.called(selectNextStub);
});
});
suite('selectParent_', function() {
test('No item selected -> Should not handle event', function() {
this.toolbox.selectedItem_ = null;
var handled = this.toolbox.selectParent_();
chai.assert.isFalse(handled);
});
test('Selected item is expanded -> Should collapse', function() {
var collapsibleItem = getCollapsibleItem(this.toolbox);
collapsibleItem.expanded_ = true;
this.toolbox.selectedItem_ = collapsibleItem;
var handled = this.toolbox.selectParent_();
chai.assert.isTrue(handled);
chai.assert.isFalse(collapsibleItem.isExpanded());
chai.assert.equal(this.toolbox.selectedItem_, collapsibleItem);
});
test('Selected item is not expanded -> Should get parent', function() {
var childItem = getChildItem(this.toolbox);
this.toolbox.selectedItem_ = childItem;
var handled = this.toolbox.selectParent_();
chai.assert.isTrue(handled);
chai.assert.equal(this.toolbox.selectedItem_, childItem.getParent());
});
});
suite('selectNext_', function() {
test('No item is selected -> Should not handle event', function() {
this.toolbox.selectedItem_ = null;
var handled = this.toolbox.selectNext_();
chai.assert.isFalse(handled);
});
test('Next item is selectable -> Should select next item', function() {
var item = this.toolbox.contents_[0];
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectNext_();
chai.assert.isTrue(handled);
chai.assert.equal(this.toolbox.selectedItem_, this.toolbox.contents_[1]);
});
test('Selected item is last item -> Should not handle event', function() {
var item = this.toolbox.contents_[this.toolbox.contents_.length - 1];
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectNext_();
chai.assert.isFalse(handled);
chai.assert.equal(this.toolbox.selectedItem_, item);
});
test('Selected item is collapsed -> Should skip over its children', function() {
var item = getCollapsibleItem(this.toolbox);
var childItem = item.flyoutItems_[0];
item.expanded_ = false;
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectNext_();
chai.assert.isTrue(handled);
chai.assert.notEqual(this.toolbox.selectedItem_, childItem);
});
});
suite('selectPrevious', function() {
test('No item is selected -> Should not handle event', function() {
this.toolbox.selectedItem_ = null;
var handled = this.toolbox.selectPrevious_();
chai.assert.isFalse(handled);
});
test('Selected item is first item -> Should not handle event', function() {
var item = this.toolbox.contents_[0];
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectPrevious_();
chai.assert.isFalse(handled);
chai.assert.equal(this.toolbox.selectedItem_, item);
});
test('Previous item is selectable -> Should select previous item', function() {
var item = this.toolbox.contents_[1];
var prevItem = this.toolbox.contents_[0];
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectPrevious_();
chai.assert.isTrue(handled);
chai.assert.equal(this.toolbox.selectedItem_, prevItem);
});
test('Previous item is collapsed -> Should skip over children of the previous item', function() {
var childItem = getChildItem(this.toolbox);
var parentItem = childItem.getParent();
var parentIdx = this.toolbox.contents_.indexOf(parentItem);
// Gets the item after the parent.
var item = this.toolbox.contents_[parentIdx + 1];
this.toolbox.selectedItem_ = item;
var handled = this.toolbox.selectPrevious_();
chai.assert.isTrue(handled);
chai.assert.notEqual(this.toolbox.selectedItem_, childItem);
});
});
});
suite('setSelectedItem', function() {
setup(function() {
this.toolbox = getInjectedToolbox();
});
teardown(function() {
this.toolbox.dispose();
});
function setupSetSelected(toolbox, oldItem, newItem) {
toolbox.selectedItem_ = oldItem;
var newItemStub = sinon.stub(newItem, 'setSelected');
toolbox.setSelectedItem(newItem);
return newItemStub;
}
test('Selected item and new item are null -> Should not update the flyout', function() {
this.selectedItem_ = null;
this.toolbox.setSelectedItem(null);
var updateFlyoutStub = sinon.stub(this.toolbox, 'updateFlyout_');
sinon.assert.notCalled(updateFlyoutStub);
});
test('New item is not selectable -> Should not update the flyout', function() {
var separator = getSeparator(this.toolbox);
this.toolbox.setSelectedItem(separator);
var updateFlyoutStub = sinon.stub(this.toolbox, 'updateFlyout_');
sinon.assert.notCalled(updateFlyoutStub);
});
test('Select an item with no children -> Should select item', function() {
var oldItem = getCollapsibleItem(this.toolbox);
var oldItemStub = sinon.stub(oldItem, 'setSelected');
var newItem = getNonCollapsibleItem(this.toolbox);
var newItemStub = setupSetSelected(this.toolbox, oldItem, newItem);
sinon.assert.calledWith(oldItemStub, false);
sinon.assert.calledWith(newItemStub, true);
});
test('Select previously selected item with no children -> Should deselect', function() {
var newItem = getNonCollapsibleItem(this.toolbox);
var newItemStub = setupSetSelected(this.toolbox, newItem, newItem);
sinon.assert.calledWith(newItemStub, false);
});
test('Select collapsible item -> Should select item', function() {
var newItem = getCollapsibleItem(this.toolbox);
var newItemStub = setupSetSelected(this.toolbox, null, newItem);
sinon.assert.calledWith(newItemStub, true);
});
test('Select previously selected collapsible item -> Should not deselect', function() {
var newItem = getCollapsibleItem(this.toolbox);
var newItemStub = setupSetSelected(this.toolbox, newItem, newItem);
sinon.assert.notCalled(newItemStub);
});
});
suite('updateFlyout_', function() {
setup(function() {
this.toolbox = getInjectedToolbox();
});
teardown(function() {
this.toolbox.dispose();
});
function testHideFlyout(toolbox, oldItem, newItem) {
var updateFlyoutStub = sinon.stub(toolbox.flyout_, 'hide');
var newItem = getNonCollapsibleItem(toolbox);
toolbox.updateFlyout_(oldItem, newItem);
sinon.assert.called(updateFlyoutStub);
}
test('Select previously selected item -> Should close flyout', function() {
var newItem = getNonCollapsibleItem(this.toolbox);
testHideFlyout(this.toolbox, newItem, newItem);
});
test('No new item -> Should close flyout', function() {
testHideFlyout(this.toolbox, null, null);
});
test('Select collapsible item -> Should close flyout', function() {
var newItem = getCollapsibleItem(this.toolbox);
testHideFlyout(this.toolbox,null, newItem);
});
test('Select selectable item -> Should open flyout', function() {
var showFlyoutstub = sinon.stub(this.toolbox.flyout_, 'show');
var scrollToStartFlyout = sinon.stub(this.toolbox.flyout_, 'scrollToStart');
var newItem = getNonCollapsibleItem(this.toolbox);
this.toolbox.updateFlyout_(null, newItem);
sinon.assert.called(showFlyoutstub);
sinon.assert.called(scrollToStartFlyout);
});
});
suite('position', function() {
setup(function() {
this.toolbox.init();
this.toolbox = getBasicToolbox();
});
function checkHorizontalToolbox(toolbox) {
chai.assert.equal(toolbox.HtmlDiv.style.left, '0px', 'Check left position');
chai.assert.equal(toolbox.HtmlDiv.style.height, 'auto', 'Check height');
var svgSize = Blockly.svgSize(toolbox.workspace_.getParentSvg());
chai.assert.equal(toolbox.HtmlDiv.style.width, svgSize.width + 'px', 'Check width');
chai.assert.equal(toolbox.height, toolbox.HtmlDiv.offsetHeight, 'Check height');
chai.assert.equal(toolbox.HtmlDiv.style.width, '100%', 'Check width');
chai.assert.equal(toolbox.height_, toolbox.HtmlDiv.offsetHeight, 'Check height');
}
function checkVerticalToolbox(toolbox) {
var svgSize = Blockly.svgSize(toolbox.workspace_.getParentSvg());
chai.assert.equal(toolbox.HtmlDiv.style.height, svgSize.height + 'px', 'Check height');
chai.assert.equal(toolbox.width, toolbox.HtmlDiv.offsetWidth, 'Check width');
chai.assert.equal(toolbox.HtmlDiv.style.height, '100%', 'Check height');
chai.assert.equal(toolbox.width_, toolbox.HtmlDiv.offsetWidth, 'Check width');
}
test('Return if tree is not yet initialized', function() {
this.toolbox.HtmlDiv = null;
this.toolbox.horizontalLayout_ = true;
this.toolbox.position();
chai.assert.equal(this.toolbox.height, '');
test('HtmlDiv is not created -> Should not resize', function() {
var toolbox = this.toolbox;
toolbox.HtmlDiv = null;
toolbox.horizontalLayout_ = true;
toolbox.position();
chai.assert.equal(toolbox.height_, 0);
});
test('Position horizontal at top', function() {
test('Horizontal toolbox at top -> Should anchor horizontal toolbox to top', function() {
var toolbox = this.toolbox;
toolbox.toolboxPosition = Blockly.TOOLBOX_AT_TOP;
toolbox.horizontalLayout_ = true;
@@ -224,7 +451,7 @@ suite('Toolbox', function() {
checkHorizontalToolbox(toolbox);
chai.assert.equal(toolbox.HtmlDiv.style.top, '0px', 'Check top');
});
test('Position horizontal at bottom', function() {
test('Horizontal toolbox at bottom -> Should anchor horizontal toolbox to bottom', function() {
var toolbox = this.toolbox;
toolbox.toolboxPosition = Blockly.TOOLBOX_AT_BOTTOM;
toolbox.horizontalLayout_ = true;
@@ -232,7 +459,7 @@ suite('Toolbox', function() {
checkHorizontalToolbox(toolbox);
chai.assert.equal(toolbox.HtmlDiv.style.bottom, '0px', 'Check bottom');
});
test('Position Vertical at right', function() {
test('Vertical toolbox at right -> Should anchor to right', function() {
var toolbox = this.toolbox;
toolbox.toolboxPosition = Blockly.TOOLBOX_AT_RIGHT;
toolbox.horizontalLayout_ = false;
@@ -240,9 +467,9 @@ suite('Toolbox', function() {
chai.assert.equal(toolbox.HtmlDiv.style.right, '0px', 'Check right');
checkVerticalToolbox(toolbox);
});
test('Position Vertical at left ', function() {
test('Vertical toolbox at left -> Should anchor to left', function() {
var toolbox = this.toolbox;
toolbox.toolboxPosition = Blockly.TOOLBOX_AT_RIGHT;
toolbox.toolboxPosition = Blockly.TOOLBOX_AT_LEFT;
toolbox.horizontalLayout_ = false;
toolbox.position();
chai.assert.equal(toolbox.HtmlDiv.style.left, '0px', 'Check left');
@@ -250,53 +477,7 @@ suite('Toolbox', function() {
});
});
suite('createTree_', function() {
setup(function() {
this.tree = new Blockly.tree.TreeControl(this.toolbox, this.toolbox.config_);
this.tree.contents = [];
this.toolboxXml = document.getElementById('toolbox-test');
this.separatorIdx = 0;
this.buttonIdx = 1;
this.dynamicCategoryIdx = 3;
this.categorySeparatorIdx = 2;
this.toolboxXml = Blockly.utils.toolbox.convertToolboxToJSON(this.toolboxXml);
});
test('Having a dynamic category', function() {
this.toolbox.createTree_(this.toolboxXml, this.tree);
chai.assert.equal(this.tree.children_[this.dynamicCategoryIdx].contents, 'VARIABLE');
});
test('Node is expanded', function() {
var openNode = this.toolbox.createTree_(this.toolboxXml, this.tree);
chai.assert.exists(openNode);
});
test('Having a tree separator', function() {
this.toolbox.createTree_(this.toolboxXml, this.tree);
var sepObj = this.tree.children_[0].contents[this.separatorIdx];
chai.assert.isNotNull(sepObj);
chai.assert.equal(sepObj['gap'], -1);
});
test('Separator between two categories', function() {
this.toolbox.createTree_(this.toolboxXml, this.tree);
chai.assert.instanceOf(this.tree.children_[this.categorySeparatorIdx],
Blockly.Toolbox.TreeSeparator);
});
test('Having a button', function() {
this.toolbox.createTree_(this.toolboxXml, this.tree);
var btnObj = this.tree.children_[0].contents[this.buttonIdx];
chai.assert.isNotNull(btnObj);
chai.assert.equal(btnObj['text'], 'insert');
chai.assert.equal(btnObj['callbackkey'], 'insertConnectionRows');
});
test('Colours are set using correct method', function() {
var setColourFromStyleStub = sinon.stub(this.toolbox, "setColourFromStyle_");
var setColourStub = sinon.stub(this.toolbox, "setColour_");
this.toolbox.createTree_(this.toolboxXml, this.tree);
sinon.assert.calledOnce(setColourFromStyleStub);
sinon.assert.called(setColourStub);
});
});
suite('parseToolbox', function() {
suite('parseMethods', function() {
setup(function() {
this.categoryToolboxJSON = getCategoryJSON();
this.simpleToolboxJSON = getSimpleJSON();
@@ -305,72 +486,158 @@ suite('Toolbox', function() {
function checkValue(actual, expected, value) {
var actualVal = actual[value];
var expectedVal = expected[value];
chai.assert.equal(actualVal.toUpperCase(), expectedVal.toUpperCase(), 'Checknig value for: ' + value);
chai.assert.equal(actualVal.toUpperCase(), expectedVal.toUpperCase(), 'Checking value for: ' + value);
}
function checkContents(actualContents, expectedContents) {
chai.assert.equal(actualContents.length, expectedContents.length);
for (var i = 0; i < actualContents.length; i++) {
// TODO: Check the values as well as all the keys.
chai.assert.containsAllKeys(actualContents[i], Object.keys(expectedContents[i]));
}
}
function checkCategory(actual, expected) {
checkValue(actual, expected, 'kind');
checkValue(actual, expected, 'name');
chai.assert.deepEqual(actual['cssconfig'], expected['cssconfig']);
checkContents(actual.contents, expected.contents);
}
function checkCategoryToolbox(actual, expected) {
chai.assert.equal(actual.length, expected.length);
var actualContents = actual['contents'];
var expectedContents = expected['contents'];
chai.assert.equal(actualContents.length, expectedContents.length);
for (var i = 0; i < expected.length; i++) {
checkCategory(actual[i], expected[i]);
checkCategory(actualContents[i], expected[i]);
}
}
function checkSimpleToolbox(actual, expected) {
checkContents(actual, expected);
checkContents(actual['contents'], expected['contents']);
}
test('Simple Toolbox: Array with xml', function() {
var xmlList = getXmlArray();
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(xmlList);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
suite('parseToolbox', function() {
test('Category Toolbox: JSON', function() {
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(this.categoryToolboxJSON);
chai.assert.isNotNull(toolboxDef);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
});
test('Simple Toolbox: JSON', function() {
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(this.simpleToolboxJSON);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
});
test('Category Toolbox: xml', function() {
var toolboxXml = document.getElementById('toolbox-categories');
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(toolboxXml);
chai.assert.isNotNull(toolboxDef);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
});
test('Simple Toolbox: xml', function() {
var toolboxXml = document.getElementById('toolbox-simple');
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(toolboxXml);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
});
test('Simple Toolbox: string', function() {
var toolbox = '<xml>';
toolbox += ' <block type="controls_if"></block>';
toolbox += ' <block type="controls_whileUntil"></block>';
toolbox += '</xml>';
var toolboxJson = {
'contents': [
{
'kind': 'block',
'type': 'controls_if'
},
{
'kind': 'block',
'type': 'controls_if'
}
]
};
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(toolbox);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, toolboxJson);
});
test('Category Toolbox: string', function() {
var toolbox = '<xml>';
toolbox += ' <category name="a"></category>';
toolbox += ' <category name="b"></category>';
toolbox += '</xml>';
var toolboxJson = {
'contents': [
{
'kind': 'category',
'name': 'a'
},
{
'kind': 'category',
'name': 'b'
}
]
};
var toolboxDef = Blockly.utils.toolbox.convertToolboxDefToJson(toolbox);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, toolboxJson);
});
});
test('Category Toolbox: Array with xml', function() {
var categoryOne = Blockly.Xml.textToDom('<category name="First"><block type="basic_block"><field name="TEXT">FirstCategory-FirstBlock</field></block><block type="basic_block"><field name="TEXT">FirstCategory-SecondBlock</field></block></category>');
var categoryTwo = Blockly.Xml.textToDom('<category name="Second"><block type="basic_block"><field name="TEXT">SecondCategory-FirstBlock</field></block></category>');
var xmlList = [categoryOne, categoryTwo];
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(xmlList);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
suite('parseFlyout', function() {
test('Array of Nodes', function() {
var xmlList = getXmlArray();
var flyoutDef = Blockly.utils.toolbox.convertFlyoutDefToJsonArray(xmlList);
checkContents(flyoutDef, this.simpleToolboxJSON['contents']);
});
test('NodeList', function() {
var nodeList = document.getElementById('toolbox-simple').childNodes;
var flyoutDef = Blockly.utils.toolbox.convertFlyoutDefToJsonArray(nodeList);
checkContents(flyoutDef, this.simpleToolboxJSON['contents']);
});
test('List of json', function() {
var jsonList = this.simpleToolboxJSON['contents'];
var flyoutDef = Blockly.utils.toolbox.convertFlyoutDefToJsonArray(jsonList);
checkContents(flyoutDef, this.simpleToolboxJSON['contents']);
});
test('Json', function() {
var flyoutDef = Blockly.utils.toolbox.convertFlyoutDefToJsonArray(this.simpleToolboxJSON);
checkContents(flyoutDef, this.simpleToolboxJSON['contents']);
});
});
test('Category Toolbox: Array with JSON', function() {
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(this.categoryToolboxJSON);
chai.assert.isNotNull(toolboxDef);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
});
suite('Nested Categories', function() {
setup(function() {
this.toolbox = getInjectedToolbox();
});
test('Simple Toolbox: Array with JSON', function() {
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(this.simpleToolboxJSON);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
teardown(function() {
this.toolbox.dispose();
});
test('Category Toolbox: NodeList', function() {
var nodeList = document.getElementById('toolbox-categories').childNodes;
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(nodeList);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
test('Child categories visible if all ancestors expanded', function() {
this.toolbox.render(getDeeplyNestedJSON());
var outerCategory = this.toolbox.contents_[0];
var middleCategory = this.toolbox.contents_[1];
var innerCategory = this.toolbox.contents_[2];
outerCategory.toggleExpanded();
middleCategory.toggleExpanded();
innerCategory.show();
chai.assert.isTrue(innerCategory.isVisible(),
'All ancestors are expanded, so category should be visible');
});
test('Simple Toolbox: NodeList', function() {
var nodeList = document.getElementById('toolbox-simple').childNodes;
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(nodeList);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
});
test('Category Toolbox: xml', function() {
var toolboxXml = document.getElementById('toolbox-categories');
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(toolboxXml);
chai.assert.isNotNull(toolboxDef);
checkCategoryToolbox(toolboxDef, this.categoryToolboxJSON);
});
test('Simple Toolbox: xml', function() {
var toolboxXml = document.getElementById('toolbox-simple');
var toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(toolboxXml);
chai.assert.isNotNull(toolboxDef);
checkSimpleToolbox(toolboxDef, this.simpleToolboxJSON);
test('Child categories not visible if any ancestor not expanded', function() {
this.toolbox.render(getDeeplyNestedJSON());
var middleCategory = this.toolbox.contents_[1];
var innerCategory = this.toolbox.contents_[2];
// Don't expand the outermost category
// Even though the direct parent of inner is expanded, it shouldn't be visible
// because all ancestor categories need to be visible, not just parent
middleCategory.toggleExpanded();
innerCategory.show();
chai.assert.isFalse(innerCategory.isVisible(),
'Not all ancestors are expanded, so category should not be visible');
});
});
});
+4 -4
View File
@@ -97,25 +97,25 @@ suite('WorkspaceSvg', function() {
test('Passes in toolbox def when current toolbox is null', function() {
this.workspace.options.languageTree = null;
chai.assert.throws(function() {
this.workspace.updateToolbox([]);
this.workspace.updateToolbox({'contents': []});
}.bind(this), 'Existing toolbox is null. Can\'t create new toolbox.');
});
test('Existing toolbox has no categories', function() {
sinon.stub(Blockly.utils.toolbox, 'hasCategories').returns(true);
this.workspace.toolbox_ = null;
chai.assert.throws(function() {
this.workspace.updateToolbox([]);
this.workspace.updateToolbox({'contents': []});
}.bind(this), 'Existing toolbox has no categories. Can\'t change mode.');
});
test('Existing toolbox has categories', function() {
sinon.stub(Blockly.utils.toolbox, 'hasCategories').returns(false);
this.workspace.flyout_ = null;
chai.assert.throws(function() {
this.workspace.updateToolbox([]);
this.workspace.updateToolbox({'contents': []});
}.bind(this), 'Existing toolbox has categories. Can\'t change mode.');
});
test('Passing in string as toolboxdef', function() {
var parseToolboxFake = sinon.spy(Blockly.Options, 'parseToolboxTree');
var parseToolboxFake = sinon.spy(Blockly.utils.toolbox, 'parseToolboxTree');
this.workspace.updateToolbox('<xml><category name="something"></category></xml>');
sinon.assert.calledOnce(parseToolboxFake);
});
+5
View File
@@ -16,6 +16,11 @@
var alignCategory = {
"kind": "CATEGORY",
"name": "Align",
"classConfig": {
"container": "containerSomething",
"row": "rowSomething",
"icon": "iconSomething"
},
"contents": [
{
"kind": "BLOCK",