/** * @license * Visual Blocks Editor * * Copyright 2011 Google Inc. * https://developers.google.com/blockly/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Toolbox from whence to create blocks. * @author fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.Toolbox'); goog.require('Blockly.HorizontalFlyout'); goog.require('Blockly.Touch'); goog.require('Blockly.VerticalFlyout'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events'); goog.require('goog.events.BrowserFeature'); goog.require('goog.html.SafeHtml'); goog.require('goog.html.SafeStyle'); goog.require('goog.math.Rect'); goog.require('goog.style'); goog.require('goog.ui.tree.TreeControl'); goog.require('goog.ui.tree.TreeNode'); /** * Class for a Toolbox. * Creates the toolbox's DOM. * @param {!Blockly.Workspace} workspace The workspace in which to create new * blocks. * @constructor */ Blockly.Toolbox = function(workspace) { /** * @type {!Blockly.Workspace} * @private */ this.workspace_ = workspace; /** * Whether toolbox categories should be represented by icons instead of text. * @type {boolean} * @private */ this.iconic_ = false; /** * 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; }; /** * Width of the toolbox, which changes only in vertical layout. * @type {number} */ Blockly.Toolbox.prototype.width = 250; /** * Height of the toolbox, which changes only in horizontal layout. * @type {number} */ Blockly.Toolbox.prototype.height = 0; Blockly.Toolbox.prototype.selectedItem_ = null; /** * Initializes the toolbox. */ Blockly.Toolbox.prototype.init = function() { var workspace = this.workspace_; var svg = this.workspace_.getParentSvg(); /** * HTML container for the Toolbox menu. * @type {Element} */ this.HtmlDiv = goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyToolboxDiv'); this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR'); svg.parentNode.insertBefore(this.HtmlDiv, svg); // Clicking on toolbox closes popups. Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this, function(e) { Blockly.DropDownDiv.hide(); 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. }); this.createFlyout_(); this.categoryMenu_ = new Blockly.Toolbox.CategoryMenu(this, this.HtmlDiv); this.populate_(workspace.options.languageTree); this.position(); }; /** * Dispose of this toolbox. */ Blockly.Toolbox.prototype.dispose = function() { this.flyout_.dispose(); this.categoryMenu_.dispose(); this.categoryMenu_ = null; goog.dom.removeNode(this.HtmlDiv); this.workspace_ = null; this.lastCategory_ = null; }; /** * Create and configure a flyout based on the main workspace's options. * @private */ Blockly.Toolbox.prototype.createFlyout_ = function() { var workspace = this.workspace_; var options = { disabledPatternId: workspace.options.disabledPatternId, parentWorkspace: workspace, RTL: workspace.RTL, oneBasedIndex: workspace.options.oneBasedIndex, horizontalLayout: workspace.horizontalLayout, toolboxPosition: workspace.options.toolboxPosition }; if (workspace.horizontalLayout) { this.flyout_ = new Blockly.HorizontalFlyout(options); } else { this.flyout_ = new Blockly.VerticalFlyout(options); } this.flyout_.setParentToolbox(this); goog.dom.insertSiblingAfter(this.flyout_.createDom('svg'), this.workspace_.getParentSvg()); this.flyout_.init(workspace); }; /** * Fill the toolbox with categories and blocks. * @param {!Node} newTree DOM tree of blocks. * @private */ Blockly.Toolbox.prototype.populate_ = function(newTree) { this.categoryMenu_.populate(newTree); this.setSelectedItem(this.categoryMenu_.categories_[0]); }; /** * 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, not including the block menu. * @return {number} The height of the toolbox. */ Blockly.Toolbox.prototype.getHeight = function() { return this.categoryMenu_ ? this.categoryMenu_.getHeight() : 0; }; /** * Move the toolbox to the edge. */ Blockly.Toolbox.prototype.position = function() { var treeDiv = this.HtmlDiv; if (!treeDiv) { // Not initialized yet. return; } var svg = this.workspace_.getParentSvg(); var svgSize = Blockly.svgSize(svg); 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 = this.getHeight() + 'px'; treeDiv.style.width = this.width + 'px'; } this.flyout_.position(); }; /** * Unhighlight any previously specified option. */ Blockly.Toolbox.prototype.clearSelection = function() { this.setSelectedItem(null); }; /** * Adds styles on the toolbox indicating blocks will be deleted. * @package */ Blockly.Toolbox.prototype.addDeleteStyle = function() { Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv), 'blocklyToolboxDelete'); }; /** * Remove styles from the toolbox that indicate blocks will be deleted. * @package */ Blockly.Toolbox.prototype.removeDeleteStyle = function() { Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv), 'blocklyToolboxDelete'); }; /** * Adds styles on the toolbox indicating blocks will be deleted. * @package */ Blockly.Toolbox.prototype.addDeleteStyle = function() { Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv), 'blocklyToolboxDelete'); }; /** * Remove styles from the toolbox that indicate blocks will be deleted. * @package */ Blockly.Toolbox.prototype.removeDeleteStyle = function() { Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv), 'blocklyToolboxDelete'); }; /** * Return the deletion rectangle for this toolbox. * @return {goog.math.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 x = toolboxRect.left; var y = toolboxRect.top; var width = toolboxRect.width; var height = toolboxRect.height; // 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_LEFT) { return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width, 2 * BIG_NUM); } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM); } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM, BIG_NUM + y + height); } else { // Bottom return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width); } }; /** * 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.getSelectedItem(); if (selectedItem && selectedItem.getContents()) { this.flyout_.show(selectedItem.getContents()); } }; /** * @return {Blockly.Toolbox.Category} the currently selected category. */ Blockly.Toolbox.prototype.getSelectedItem = function() { return this.selectedItem_; }; /** * Set the currently selected category. * @param {Blockly.Toolbox.Category} item The category to select. */ Blockly.Toolbox.prototype.setSelectedItem = function(item) { if (this.selectedItem_) { // Don't do anything if they selected the already-open category. if (this.selectedItem_ == item) { return; } // They selected a different category but one was already open. Close it. this.selectedItem_.setSelected(false); } this.selectedItem_ = item; if (this.selectedItem_ != null) { this.selectedItem_.setSelected(true); this.flyout_.show(item.getContents()); this.flyout_.scrollToStart(); } }; /** * Wrapper function for calling setSelectedItem from a touch handler. * @param {Blockly.Toolbox.Category} item The category to select. * @return {function} A function that can be passed to bindEvent. */ Blockly.Toolbox.prototype.setSelectedItemFactory = function(item) { var selectedItem = item; return function() { this.setSelectedItem(selectedItem); Blockly.Touch.clearTouchIdentifier(); }; }; // Category menu /** * Class for a table of category titles that will control which category is * displayed. * @param {Blockly.Toolbox} parent The toolbox that owns the category menu. * @param {Element} parentHtml The containing html div. * @constructor */ Blockly.Toolbox.CategoryMenu = function(parent, parentHtml) { this.parent_ = parent; this.height_ = 0; this.parentHtml_ = parentHtml; this.createDom(); this.categories_ = []; }; /** * @return {number} the height of the category menu. */ Blockly.Toolbox.CategoryMenu.prototype.getHeight = function() { return this.height_; }; /** * Create the DOM for the category menu. */ Blockly.Toolbox.CategoryMenu.prototype.createDom = function() { /*