diff options
author | David Barksdale <amatus@amatus.name> | 2016-12-03 14:23:02 -0600 |
---|---|---|
committer | David Barksdale <amatus@amatus.name> | 2016-12-03 14:23:02 -0600 |
commit | 99e916beaf6afe5b2300bd95e73267550767bf7a (patch) | |
tree | 2b22c71fbdbf507735635c834de2276450b6372a /src/blockly/core/toolbox.js | |
parent | 53284a2f5d22d15cd7851fd0f06a53ea1df0c280 (diff) |
Add Semantic-UI and get blockly working
Diffstat (limited to 'src/blockly/core/toolbox.js')
-rw-r--r-- | src/blockly/core/toolbox.js | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/src/blockly/core/toolbox.js b/src/blockly/core/toolbox.js new file mode 100644 index 0000000..5a5096c --- /dev/null +++ b/src/blockly/core/toolbox.js @@ -0,0 +1,650 @@ +/** + * @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.Flyout'); +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; + + /** + * Is RTL vs LTR. + * @type {boolean} + */ + this.RTL = workspace.options.RTL; + + /** + * Whether the toolbox should be laid out horizontally. + * @type {boolean} + * @private + */ + this.horizontalLayout_ = workspace.options.horizontalLayout; + + /** + * Position of the toolbox and flyout relative to the workspace. + * @type {number} + */ + this.toolboxPosition = workspace.options.toolboxPosition; + + /** + * Configuration constants for Closure's tree UI. + * @type {Object.<string,*>} + * @private + */ + this.config_ = { + indentWidth: 19, + cssRoot: 'blocklyTreeRoot', + cssHideRoot: 'blocklyHidden', + cssItem: '', + cssTreeRow: 'blocklyTreeRow', + cssItemLabel: 'blocklyTreeLabel', + cssTreeIcon: 'blocklyTreeIcon', + cssExpandedFolderIcon: 'blocklyTreeIconOpen', + cssFileIcon: 'blocklyTreeIconNone', + cssSelectedRow: 'blocklyTreeSelected' + }; + + + /** + * Configuration constants for tree separator. + * @type {Object.<string,*>} + * @private + */ + this.treeSeparatorConfig_ = { + cssTreeRow: 'blocklyTreeSeparator' + }; + + if (this.horizontalLayout_) { + this.config_['cssTreeRow'] = + this.config_['cssTreeRow'] + + (workspace.RTL ? + ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree'); + + this.treeSeparatorConfig_['cssTreeRow'] = + 'blocklyTreeSeparatorHorizontal ' + + (workspace.RTL ? + 'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree'); + this.config_['cssTreeIcon'] = ''; + } +}; + +/** + * Width of the toolbox, which changes only in vertical layout. + * @type {number} + */ +Blockly.Toolbox.prototype.width = 0; + +/** + * Height of the toolbox, which changes only in horizontal layout. + * @type {number} + */ +Blockly.Toolbox.prototype.height = 0; + +/** + * The SVG group currently selected. + * @type {SVGGElement} + * @private + */ +Blockly.Toolbox.prototype.selectedOption_ = null; + +/** + * The tree node most recently selected. + * @type {goog.ui.tree.BaseNode} + * @private + */ +Blockly.Toolbox.prototype.lastCategory_ = null; + +/** + * Initializes the toolbox. + */ +Blockly.Toolbox.prototype.init = function() { + var workspace = this.workspace_; + var svg = this.workspace_.getParentSvg(); + + // Create an HTML container for the Toolbox menu. + 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.bindEvent_(this.HtmlDiv, 'mousedown', this, + function(e) { + if (Blockly.isRightButton(e) || e.target == this.HtmlDiv) { + // Close flyout. + Blockly.hideChaff(false); + } else { + // Just close popups. + Blockly.hideChaff(true); + } + }); + var workspaceOptions = { + disabledPatternId: workspace.options.disabledPatternId, + parentWorkspace: workspace, + RTL: workspace.RTL, + horizontalLayout: workspace.horizontalLayout, + toolboxPosition: workspace.options.toolboxPosition + }; + /** + * @type {!Blockly.Flyout} + * @private + */ + this.flyout_ = new Blockly.Flyout(workspaceOptions); + goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_); + this.flyout_.init(workspace); + + this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif'; + this.config_['cssCollapsedFolderIcon'] = + 'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr'); + var tree = new Blockly.Toolbox.TreeControl(this, this.config_); + this.tree_ = tree; + tree.setShowRootNode(false); + tree.setShowLines(false); + tree.setShowExpandIcons(false); + tree.setSelectedItem(null); + var openNode = this.populate_(workspace.options.languageTree); + tree.render(this.HtmlDiv); + if (openNode) { + tree.setSelectedItem(openNode); + } + this.addColour_(); + this.position(); +}; + +/** + * Dispose of this toolbox. + */ +Blockly.Toolbox.prototype.dispose = function() { + this.flyout_.dispose(); + this.tree_.dispose(); + goog.dom.removeNode(this.HtmlDiv); + this.workspace_ = null; + this.lastCategory_ = null; +}; + +/** + * 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; +}; + +/** + * 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 svgPosition = goog.style.getPageOffset(svg); + 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 = svgSize.height + 'px'; + this.width = treeDiv.offsetWidth; + } + this.flyout_.position(); +}; + +/** + * Fill the toolbox with categories and blocks. + * @param {!Node} newTree DOM tree of blocks. + * @return {Node} Tree node to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.populate_ = function(newTree) { + this.tree_.removeChildren(); // Delete any existing content. + this.tree_.blocks = []; + this.hasColours_ = false; + var openNode = + this.syncTrees_(newTree, this.tree_, this.workspace_.options.pathToMedia); + + if (this.tree_.blocks.length) { + throw '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(); + return openNode; +}; + +/** + * Sync trees of the toolbox. + * @param {!Node} treeIn DOM tree of blocks. + * @param {!Blockly.Toolbox.TreeControl} treeOut + * @param {string} pathToMedia + * @return {Node} Tree node to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { + var openNode = null; + var lastElement = null; + for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) { + if (!childIn.tagName) { + // Skip over text. + continue; + } + switch (childIn.tagName.toUpperCase()) { + case 'CATEGORY': + var childOut = this.tree_.createNode(childIn.getAttribute('name')); + childOut.blocks = []; + treeOut.add(childOut); + var custom = childIn.getAttribute('custom'); + if (custom) { + // Variables and procedures are special dynamic categories. + childOut.blocks = custom; + } else { + var newOpenNode = this.syncTrees_(childIn, childOut, pathToMedia); + if (newOpenNode) { + openNode = newOpenNode; + } + } + var colour = childIn.getAttribute('colour'); + if (goog.isString(colour)) { + if (colour.match(/^#[0-9a-fA-F]{6}$/)) { + childOut.hexColour = colour; + } else { + childOut.hexColour = Blockly.hueToRgb(colour); + } + this.hasColours_ = true; + } else { + childOut.hexColour = ''; + } + if (childIn.getAttribute('expanded') == 'true') { + if (childOut.blocks.length) { + // This is a category that directly contians blocks. + // After the tree is rendered, open this category and show flyout. + openNode = childOut; + } + childOut.setExpanded(true); + } else { + childOut.setExpanded(false); + } + lastElement = childIn; + break; + case 'SEP': + if (lastElement) { + if (lastElement.tagName.toUpperCase() == 'CATEGORY') { + // Separator between two categories. + // <sep></sep> + treeOut.add(new Blockly.Toolbox.TreeSeparator( + this.treeSeparatorConfig_)); + } else { + // Change the gap between two blocks. + // <sep gap="36"></sep> + // The default gap is 24, can be set larger or smaller. + // Note that a deprecated method is to add a gap to a block. + // <block type="math_arithmetic" gap="8"></block> + var newGap = parseFloat(childIn.getAttribute('gap')); + if (!isNaN(newGap) && lastElement) { + lastElement.setAttribute('gap', newGap); + } + } + } + break; + case 'BLOCK': + case 'SHADOW': + treeOut.blocks.push(childIn); + lastElement = childIn; + break; + } + } + return openNode; +}; + +/** + * Recursively add colours to this toolbox. + * @param {Blockly.Toolbox.TreeNode} 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(); + 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); +}; + +/** + * 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.tree_.getSelectedItem(); + if (selectedItem && selectedItem.blocks) { + this.flyout_.show(selectedItem.blocks); + } +}; + +// Extending Closure's Tree UI. + +/** + * Extention of a TreeControl object that uses a custom tree node. + * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree. + * @param {Object} config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. + * @constructor + * @extends {goog.ui.tree.TreeControl} + */ +Blockly.Toolbox.TreeControl = function(toolbox, config) { + this.toolbox_ = toolbox; + goog.ui.tree.TreeControl.call(this, goog.html.SafeHtml.EMPTY, config); +}; +goog.inherits(Blockly.Toolbox.TreeControl, goog.ui.tree.TreeControl); + +/** + * Adds touch handling to TreeControl. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.enterDocument = function() { + Blockly.Toolbox.TreeControl.superClass_.enterDocument.call(this); + + // Add touch handler. + if (goog.events.BrowserFeature.TOUCH_ENABLED) { + var el = this.getElement(); + Blockly.bindEvent_(el, goog.events.EventType.TOUCHSTART, this, + this.handleTouchEvent_); + } +}; + +/** + * Handles touch events. + * @param {!goog.events.BrowserEvent} e The browser event. + * @private + */ +Blockly.Toolbox.TreeControl.prototype.handleTouchEvent_ = function(e) { + e.preventDefault(); + var node = this.getNodeFromEvent_(e); + if (node && e.type === goog.events.EventType.TOUCHSTART) { + // Fire asynchronously since onMouseDown takes long enough that the browser + // would fire the default mouse event before this method returns. + setTimeout(function() { + node.onMouseDown(e); // Same behaviour for click and touch. + }, 1); + } +}; + +/** + * Creates a new tree node using a custom tree node. + * @param {string=} opt_html The HTML content of the node label. + * @return {!goog.ui.tree.TreeNode} The new item. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html) { + return new Blockly.Toolbox.TreeNode(this.toolbox_, opt_html ? + goog.html.SafeHtml.htmlEscape(opt_html) : goog.html.SafeHtml.EMPTY, + this.getConfig(), this.getDomHelper()); +}; + +/** + * Display/hide the flyout when an item is selected. + * @param {goog.ui.tree.BaseNode} node The item to select. + * @override + */ +Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) { + var toolbox = this.toolbox_; + if (node == this.selectedItem_ || node == toolbox.tree_) { + return; + } + if (toolbox.lastCategory_) { + toolbox.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. + toolbox.addColour_(node); + } + var oldNode = this.getSelectedItem(); + goog.ui.tree.TreeControl.prototype.setSelectedItem.call(this, node); + if (node && node.blocks && node.blocks.length) { + toolbox.flyout_.show(node.blocks); + // Scroll the flyout to the top if the category has changed. + if (toolbox.lastCategory_ != node) { + toolbox.flyout_.scrollToStart(); + } + } else { + // Hide the flyout. + toolbox.flyout_.hide(); + } + if (oldNode != node && oldNode != this) { + var event = new Blockly.Events.Ui(null, 'category', + oldNode && oldNode.getHtml(), node && node.getHtml()); + event.workspaceId = toolbox.workspace_.id; + Blockly.Events.fire(event); + } + if (node) { + toolbox.lastCategory_ = node; + } +}; + +/** + * A single node in the tree, customized for Blockly's UI. + * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree. + * @param {!goog.html.SafeHtml} html The HTML content of the node label. + * @param {Object=} opt_config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config + * will be used. + * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper. + * @constructor + * @extends {goog.ui.tree.TreeNode} + */ +Blockly.Toolbox.TreeNode = function(toolbox, html, opt_config, opt_domHelper) { + goog.ui.tree.TreeNode.call(this, html, opt_config, opt_domHelper); + if (toolbox) { + this.horizontalLayout_ = toolbox.horizontalLayout_; + var resize = function() { + // 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(toolbox.workspace_); + }; + // Fire a resize event since the toolbox may have changed width. + goog.events.listen(toolbox.tree_, + goog.ui.tree.BaseNode.EventType.EXPAND, resize); + goog.events.listen(toolbox.tree_, + goog.ui.tree.BaseNode.EventType.COLLAPSE, resize); + } +}; +goog.inherits(Blockly.Toolbox.TreeNode, goog.ui.tree.TreeNode); + +/** + * Supress population of the +/- icon. + * @return {!goog.html.SafeHtml} The source for the icon. + * @override + */ +Blockly.Toolbox.TreeNode.prototype.getExpandIconSafeHtml = function() { + return goog.html.SafeHtml.create('span'); +}; + +/** + * Expand or collapse the node on mouse click. + * @param {!goog.events.BrowserEvent} e The browser event. + * @override + */ +Blockly.Toolbox.TreeNode.prototype.onMouseDown = function(e) { + // Expand icon. + if (this.hasChildren() && this.isUserCollapsible_) { + this.toggle(); + this.select(); + } else if (this.isSelected()) { + this.getTree().setSelectedItem(null); + } else { + this.select(); + } + this.updateRow(); +}; + +/** + * Supress the inherited double-click behaviour. + * @param {!goog.events.BrowserEvent} e The browser event. + * @override + * @private + */ +Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) { + // NOP. +}; + +/** + * Remap event.keyCode in horizontalLayout so that arrow + * keys work properly and call original onKeyDown handler. + * @param {!goog.events.BrowserEvent} e The browser event. + * @return {boolean} The handled value. + * @override + * @private + */ +Blockly.Toolbox.TreeNode.prototype.onKeyDown = function(e) { + if (this.horizontalLayout_) { + var map = {}; + map[goog.events.KeyCodes.RIGHT] = goog.events.KeyCodes.DOWN; + map[goog.events.KeyCodes.LEFT] = goog.events.KeyCodes.UP; + map[goog.events.KeyCodes.UP] = goog.events.KeyCodes.LEFT; + map[goog.events.KeyCodes.DOWN] = goog.events.KeyCodes.RIGHT; + + var newKeyCode = map[e.keyCode]; + e.keyCode = newKeyCode || e.keyCode; + } + return Blockly.Toolbox.TreeNode.superClass_.onKeyDown.call(this, e); +}; + +/** + * A blank separator node in the tree. + * @param {Object=} config The configuration for the tree. See + * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config + * will be used. + * @constructor + * @extends {Blockly.Toolbox.TreeNode} + */ +Blockly.Toolbox.TreeSeparator = function(config) { + Blockly.Toolbox.TreeNode.call(this, null, '', config); +}; +goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode); |