diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 5c109ce3c..06e0b1621 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -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'], {}); diff --git a/core/components/tree/basenode.js b/core/components/tree/basenode.js deleted file mode 100644 index bd8485912..000000000 --- a/core/components/tree/basenode.js +++ /dev/null @@ -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.} 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); }); - } -}; diff --git a/core/components/tree/treecontrol.js b/core/components/tree/treecontrol.js deleted file mode 100644 index 97ea3e39d..000000000 --- a/core/components/tree/treecontrol.js +++ /dev/null @@ -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_); -}; diff --git a/core/components/tree/treenode.js b/core/components/tree/treenode.js deleted file mode 100644 index c6aa3f2d2..000000000 --- a/core/components/tree/treenode.js +++ /dev/null @@ -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_; diff --git a/core/flyout_base.js b/core/flyout_base.js index 0d0720683..bf59b3e7c 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -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., gaps:!Array.}} */ ( 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.} 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., gaps:Array.}} 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.} 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.} gaps The list gaps between items in the flyout. * @param {number} defaultGap The default gap between the button and next element. diff --git a/core/flyout_button.js b/core/flyout_button.js index d57d13aaf..7e0e84f1d 100644 --- a/core/flyout_button.js +++ b/core/flyout_button.js @@ -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 diff --git a/core/interfaces/i_flyout.js b/core/interfaces/i_flyout.js index abd2c591e..a63b8f485 100644 --- a/core/interfaces/i_flyout.js +++ b/core/interfaces/i_flyout.js @@ -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; diff --git a/core/interfaces/i_toolbox.js b/core/interfaces/i_toolbox.js index 62ac90ce0..208be48cc 100644 --- a/core/interfaces/i_toolbox.js +++ b/core/interfaces/i_toolbox.js @@ -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.} 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; diff --git a/core/interfaces/i_toolbox_item.js b/core/interfaces/i_toolbox_item.js new file mode 100644 index 000000000..87bc0d8dd --- /dev/null +++ b/core/interfaces/i_toolbox_item.js @@ -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} 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; diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index c162e282e..2040894fc 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -131,7 +131,9 @@ Blockly.navigation.focusToolbox_ = function() { if (!Blockly.navigation.getMarker().getCurNode()) { Blockly.navigation.markAtCursor_(); } - toolbox.selectFirstCategory(); + if (!toolbox.getSelectedItem()) { + toolbox.selectItemByPosition(0); + } } }; diff --git a/core/mutator.js b/core/mutator.js index 1a8d334d4..797c66152 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -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); diff --git a/core/options.js b/core/options.js index 6ffb071ee..c6ba7bad2 100644 --- a/core/options.js +++ b/core/options.js @@ -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.} */ - 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 document.'); - } - } - } else { - tree = null; - } - return tree; -}; diff --git a/core/registry.js b/core/registry.js index 95037b6ba..19b619efb 100644 --- a/core/registry.js +++ b/core/registry.js @@ -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.registry.Type.THEME = new Blockly.registry.Type('theme'); +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.TOOLBOX_ITEM = new Blockly.registry.Type('toolboxItem'); + /** @type {!Blockly.registry.Type} */ 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} type The type of the plugin. + * @param {string|!Blockly.registry.Type} 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} type The type of the plugin. + * @param {string|!Blockly.registry.Type} 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} type The type of the plugin. + * class or an object. + * @param {string|!Blockly.registry.Type} 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} 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} type The type of the plugin. + * @param {string|!Blockly.registry.Type} 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} type The type of the plugin. + * @param {string|!Blockly.registry.Type} 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} type The type of the plugin. + * @param {!Blockly.registry.Type} 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. diff --git a/core/toolbox.js b/core/toolbox.js deleted file mode 100644 index 0dd7e7ca5..000000000 --- a/core/toolbox.js +++ /dev/null @@ -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.} - * @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.} - * @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.} 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.} 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. - // - 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("<<>>/handdelete.cur"), auto;', - '}', - - '.blocklyToolboxGrab {', - 'cursor: url("<<>>/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(<<>>/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("<<>>/handdelete.cur"), auto;', - '}', - - '.blocklyTreeSelected .blocklyTreeLabel {', - 'color: #fff;', - '}' - /* eslint-enable indent */ -]); - -Blockly.registry.register(Blockly.registry.Type.TOOLBOX, - Blockly.registry.DEFAULT, Blockly.Toolbox); diff --git a/core/toolbox/category.js b/core/toolbox/category.js new file mode 100644 index 000000000..76e3c4841 --- /dev/null +++ b/core/toolbox/category.js @@ -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(<<>>/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("<<>>/handdelete.cur"), auto;', + '}', + + '.blocklyTreeSelected .blocklyTreeLabel {', + 'color: #fff;', + '}' + /* eslint-enable indent */ +]); + +Blockly.registry.register(Blockly.registry.Type.TOOLBOX_ITEM, + Blockly.ToolboxCategory.registrationName, Blockly.ToolboxCategory); diff --git a/core/toolbox/collapsible_category.js b/core/toolbox/collapsible_category.js new file mode 100644 index 000000000..07b2c958a --- /dev/null +++ b/core/toolbox/collapsible_category.js @@ -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} + * @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} 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} 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); diff --git a/core/toolbox/separator.js b/core/toolbox/separator.js new file mode 100644 index 000000000..b5a72669e --- /dev/null +++ b/core/toolbox/separator.js @@ -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); diff --git a/core/toolbox/toolbox.js b/core/toolbox/toolbox.js new file mode 100644 index 000000000..50117758a --- /dev/null +++ b/core/toolbox/toolbox.js @@ -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} + * @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} + * @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} + * @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} 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} 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("<<>>/handdelete.cur"), auto;', + '}', + + '.blocklyToolboxGrab {', + 'cursor: url("<<>>/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); diff --git a/core/toolbox/toolbox_item.js b/core/toolbox/toolbox_item.js new file mode 100644 index 000000000..550b47aea --- /dev/null +++ b/core/toolbox/toolbox_item.js @@ -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() { +}; diff --git a/core/utils/dom.js b/core/utils/dom.js index ebe44964d..ac0cffdcc 100644 --- a/core/utils/dom.js +++ b/core/utils/dom.js @@ -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. diff --git a/core/utils/toolbox.js b/core/utils/toolbox.js index caf8f9ccc..d34339414 100644 --- a/core/utils/toolbox.js +++ b/core/utils/toolbox.js @@ -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. + * cssconfig:?Blockly.ToolboxCategory.CssConfig, + * contents:!Array, + * 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.ToolboxInfo; + +/** + * An array holding flyout items. + * @typedef { + * Array + * } + */ +Blockly.utils.toolbox.FlyoutItemInfoArray; /** * All of the different types that can create a toolbox. * @typedef {Node| - * NodeList| - * Array.| - * Array.} + * 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} + */ +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.} 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.} */ (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.} toolboxDef The - * definition of the toolbox in one of its many forms. - * @return {!Array.} 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|!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} */ (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|!NodeList} toolboxDef The + * definition of the toolbox in one of its many forms. + * @return {!Blockly.utils.toolbox.FlyoutItemInfoArray| + * !Array} 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.} 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 document.'); + } + } + } else { + toolboxDef = null; + } + return toolboxDef; +}; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 8baada898..84d697b00 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -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); } }; diff --git a/scripts/gulpfiles/typings.js b/scripts/gulpfiles/typings.js index cdb16ad66..e15d57474 100644 --- a/scripts/gulpfiles/typings.js +++ b/scripts/gulpfiles/typings.js @@ -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/" diff --git a/tests/mocha/.eslintrc.json b/tests/mocha/.eslintrc.json index cfcc4d939..0b9db3e2c 100644 --- a/tests/mocha/.eslintrc.json +++ b/tests/mocha/.eslintrc.json @@ -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, diff --git a/tests/mocha/index.html b/tests/mocha/index.html index bfcb59091..7af275f12 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -120,7 +120,7 @@