toolbox.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2011 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Toolbox from whence to create blocks.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Toolbox');
  26. goog.require('Blockly.Flyout');
  27. goog.require('Blockly.Touch');
  28. goog.require('goog.dom');
  29. goog.require('goog.dom.TagName');
  30. goog.require('goog.events');
  31. goog.require('goog.events.BrowserFeature');
  32. goog.require('goog.html.SafeHtml');
  33. goog.require('goog.html.SafeStyle');
  34. goog.require('goog.math.Rect');
  35. goog.require('goog.style');
  36. goog.require('goog.ui.tree.TreeControl');
  37. goog.require('goog.ui.tree.TreeNode');
  38. /**
  39. * Class for a Toolbox.
  40. * Creates the toolbox's DOM.
  41. * @param {!Blockly.Workspace} workspace The workspace in which to create new
  42. * blocks.
  43. * @constructor
  44. */
  45. Blockly.Toolbox = function(workspace) {
  46. /**
  47. * @type {!Blockly.Workspace}
  48. * @private
  49. */
  50. this.workspace_ = workspace;
  51. /**
  52. * Is RTL vs LTR.
  53. * @type {boolean}
  54. */
  55. this.RTL = workspace.options.RTL;
  56. /**
  57. * Whether the toolbox should be laid out horizontally.
  58. * @type {boolean}
  59. * @private
  60. */
  61. this.horizontalLayout_ = workspace.options.horizontalLayout;
  62. /**
  63. * Position of the toolbox and flyout relative to the workspace.
  64. * @type {number}
  65. */
  66. this.toolboxPosition = workspace.options.toolboxPosition;
  67. /**
  68. * Configuration constants for Closure's tree UI.
  69. * @type {Object.<string,*>}
  70. * @private
  71. */
  72. this.config_ = {
  73. indentWidth: 19,
  74. cssRoot: 'blocklyTreeRoot',
  75. cssHideRoot: 'blocklyHidden',
  76. cssItem: '',
  77. cssTreeRow: 'blocklyTreeRow',
  78. cssItemLabel: 'blocklyTreeLabel',
  79. cssTreeIcon: 'blocklyTreeIcon',
  80. cssExpandedFolderIcon: 'blocklyTreeIconOpen',
  81. cssFileIcon: 'blocklyTreeIconNone',
  82. cssSelectedRow: 'blocklyTreeSelected'
  83. };
  84. /**
  85. * Configuration constants for tree separator.
  86. * @type {Object.<string,*>}
  87. * @private
  88. */
  89. this.treeSeparatorConfig_ = {
  90. cssTreeRow: 'blocklyTreeSeparator'
  91. };
  92. if (this.horizontalLayout_) {
  93. this.config_['cssTreeRow'] =
  94. this.config_['cssTreeRow'] +
  95. (workspace.RTL ?
  96. ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
  97. this.treeSeparatorConfig_['cssTreeRow'] =
  98. 'blocklyTreeSeparatorHorizontal ' +
  99. (workspace.RTL ?
  100. 'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree');
  101. this.config_['cssTreeIcon'] = '';
  102. }
  103. };
  104. /**
  105. * Width of the toolbox, which changes only in vertical layout.
  106. * @type {number}
  107. */
  108. Blockly.Toolbox.prototype.width = 0;
  109. /**
  110. * Height of the toolbox, which changes only in horizontal layout.
  111. * @type {number}
  112. */
  113. Blockly.Toolbox.prototype.height = 0;
  114. /**
  115. * The SVG group currently selected.
  116. * @type {SVGGElement}
  117. * @private
  118. */
  119. Blockly.Toolbox.prototype.selectedOption_ = null;
  120. /**
  121. * The tree node most recently selected.
  122. * @type {goog.ui.tree.BaseNode}
  123. * @private
  124. */
  125. Blockly.Toolbox.prototype.lastCategory_ = null;
  126. /**
  127. * Initializes the toolbox.
  128. */
  129. Blockly.Toolbox.prototype.init = function() {
  130. var workspace = this.workspace_;
  131. var svg = this.workspace_.getParentSvg();
  132. /**
  133. * HTML container for the Toolbox menu.
  134. * @type {Element}
  135. */
  136. this.HtmlDiv =
  137. goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyToolboxDiv');
  138. this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR');
  139. svg.parentNode.insertBefore(this.HtmlDiv, svg);
  140. //document.body.appendChild(this.HtmlDiv);
  141. // Clicking on toolbox closes popups.
  142. Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
  143. function(e) {
  144. if (Blockly.isRightButton(e) || e.target == this.HtmlDiv) {
  145. // Close flyout.
  146. Blockly.hideChaff(false);
  147. } else {
  148. // Just close popups.
  149. Blockly.hideChaff(true);
  150. }
  151. Blockly.Touch.clearTouchIdentifier(); // Don't block future drags.
  152. });
  153. var workspaceOptions = {
  154. disabledPatternId: workspace.options.disabledPatternId,
  155. parentWorkspace: workspace,
  156. RTL: workspace.RTL,
  157. oneBasedIndex: workspace.options.oneBasedIndex,
  158. horizontalLayout: workspace.horizontalLayout,
  159. toolboxPosition: workspace.options.toolboxPosition
  160. };
  161. /**
  162. * @type {!Blockly.Flyout}
  163. * @private
  164. */
  165. this.flyout_ = new Blockly.Flyout(workspaceOptions);
  166. goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_);
  167. this.flyout_.init(workspace);
  168. this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
  169. this.config_['cssCollapsedFolderIcon'] =
  170. 'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr');
  171. var tree = new Blockly.Toolbox.TreeControl(this, this.config_);
  172. this.tree_ = tree;
  173. tree.setShowRootNode(false);
  174. tree.setShowLines(false);
  175. tree.setShowExpandIcons(false);
  176. tree.setSelectedItem(null);
  177. /*this.HtmlDiv.style.display = 'block';
  178. this.hasColours_ = false;
  179. this.populate_(workspace.options.languageTree);*/
  180. var openNode = this.populate_(workspace.options.languageTree);
  181. tree.render(this.HtmlDiv);
  182. if (openNode) {
  183. tree.setSelectedItem(openNode);
  184. }
  185. this.addColour_();
  186. this.position();
  187. };
  188. /**
  189. * Dispose of this toolbox.
  190. */
  191. Blockly.Toolbox.prototype.dispose = function() {
  192. this.flyout_.dispose();
  193. this.tree_.dispose();
  194. goog.dom.removeNode(this.HtmlDiv);
  195. this.workspace_ = null;
  196. this.lastCategory_ = null;
  197. };
  198. /**
  199. * Get the width of the toolbox.
  200. * @return {number} The width of the toolbox.
  201. */
  202. Blockly.Toolbox.prototype.getWidth = function() {
  203. return this.width;
  204. };
  205. /**
  206. * Get the height of the toolbox.
  207. * @return {number} The width of the toolbox.
  208. */
  209. Blockly.Toolbox.prototype.getHeight = function() {
  210. return this.height;
  211. };
  212. /**
  213. * Move the toolbox to the edge.
  214. */
  215. Blockly.Toolbox.prototype.position = function() {
  216. var treeDiv = this.HtmlDiv;
  217. if (!treeDiv) {
  218. // Not initialized yet.
  219. return;
  220. }
  221. var svg = this.workspace_.getParentSvg();
  222. var svgBox = goog.style.getBorderBox(svg);
  223. var svgPosition = goog.style.getPageOffset(svg);
  224. var svgSize = Blockly.svgSize(svg);
  225. if (this.horizontalLayout_) {
  226. treeDiv.style.left = '0';
  227. treeDiv.style.height = 'auto';
  228. treeDiv.style.width = svgSize.width + 'px';
  229. this.height = treeDiv.offsetHeight;
  230. if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
  231. treeDiv.style.top = '0';
  232. } else { // Bottom
  233. treeDiv.style.bottom = '0';
  234. }
  235. } else {
  236. if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
  237. treeDiv.style.right = '0';
  238. } else { // Left
  239. treeDiv.style.left = '0';
  240. }
  241. treeDiv.style.height = svgSize.height + 'px';
  242. this.width = treeDiv.offsetWidth;
  243. }
  244. this.flyout_.position();
  245. };
  246. /**
  247. * Fill the toolbox with categories and blocks.
  248. * @param {!Node} newTree DOM tree of blocks.
  249. * @return {Node} Tree node to open at startup (or null).
  250. * @private
  251. */
  252. Blockly.Toolbox.prototype.populate_ = function(newTree) {
  253. this.tree_.removeChildren(); // Delete any existing content.
  254. this.tree_.blocks = [];
  255. this.hasColours_ = false;
  256. var openNode =
  257. this.syncTrees_(newTree, this.tree_, this.workspace_.options.pathToMedia);
  258. if (this.tree_.blocks.length) {
  259. throw 'Toolbox cannot have both blocks and categories in the root level.';
  260. }
  261. // Fire a resize event since the toolbox may have changed width and height.
  262. this.workspace_.resizeContents();
  263. return openNode;
  264. };
  265. /**
  266. * Sync trees of the toolbox.
  267. * @param {!Node} treeIn DOM tree of blocks.
  268. * @param {!Blockly.Toolbox.TreeControl} treeOut
  269. * @param {string} pathToMedia
  270. * @return {Node} Tree node to open at startup (or null).
  271. * @private
  272. */
  273. Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) {
  274. var openNode = null;
  275. var lastElement = null;
  276. for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) {
  277. if (!childIn.tagName) {
  278. // Skip over text.
  279. continue;
  280. }
  281. switch (childIn.tagName.toUpperCase()) {
  282. case 'CATEGORY':
  283. var childOut = this.tree_.createNode(childIn.getAttribute('name'));
  284. childOut.blocks = [];
  285. treeOut.add(childOut);
  286. var custom = childIn.getAttribute('custom');
  287. if (custom) {
  288. // Variables and procedures are special dynamic categories.
  289. childOut.blocks = custom;
  290. } else {
  291. var newOpenNode = this.syncTrees_(childIn, childOut, pathToMedia);
  292. if (newOpenNode) {
  293. openNode = newOpenNode;
  294. }
  295. }
  296. var colour = childIn.getAttribute('colour');
  297. if (goog.isString(colour)) {
  298. if (colour.match(/^#[0-9a-fA-F]{6}$/)) {
  299. childOut.hexColour = colour;
  300. } else {
  301. childOut.hexColour = Blockly.hueToRgb(colour);
  302. }
  303. this.hasColours_ = true;
  304. } else {
  305. childOut.hexColour = '';
  306. }
  307. if (childIn.getAttribute('expanded') == 'true') {
  308. if (childOut.blocks.length) {
  309. // This is a category that directly contians blocks.
  310. // After the tree is rendered, open this category and show flyout.
  311. openNode = childOut;
  312. }
  313. childOut.setExpanded(true);
  314. } else {
  315. childOut.setExpanded(false);
  316. }
  317. lastElement = childIn;
  318. break;
  319. case 'SEP':
  320. if (lastElement) {
  321. if (lastElement.tagName.toUpperCase() == 'CATEGORY') {
  322. // Separator between two categories.
  323. // <sep></sep>
  324. treeOut.add(new Blockly.Toolbox.TreeSeparator(
  325. this.treeSeparatorConfig_));
  326. } else {
  327. // Change the gap between two blocks.
  328. // <sep gap="36"></sep>
  329. // The default gap is 24, can be set larger or smaller.
  330. // Note that a deprecated method is to add a gap to a block.
  331. // <block type="math_arithmetic" gap="8"></block>
  332. var newGap = parseFloat(childIn.getAttribute('gap'));
  333. if (!isNaN(newGap) && lastElement) {
  334. lastElement.setAttribute('gap', newGap);
  335. }
  336. }
  337. }
  338. break;
  339. case 'BLOCK':
  340. case 'SHADOW':
  341. case 'LABEL':
  342. case 'BUTTON':
  343. treeOut.blocks.push(childIn);
  344. lastElement = childIn;
  345. break;
  346. }
  347. }
  348. return openNode;
  349. };
  350. /**
  351. * Recursively add colours to this toolbox.
  352. * @param {Blockly.Toolbox.TreeNode} opt_tree Starting point of tree.
  353. * Defaults to the root node.
  354. * @private
  355. */
  356. Blockly.Toolbox.prototype.addColour_ = function(opt_tree) {
  357. var tree = opt_tree || this.tree_;
  358. var children = tree.getChildren();
  359. for (var i = 0, child; child = children[i]; i++) {
  360. var element = child.getRowElement();
  361. if (element) {
  362. if (this.hasColours_) {
  363. var border = '8px solid ' + (child.hexColour || '#ddd');
  364. } else {
  365. var border = 'none';
  366. }
  367. if (this.workspace_.RTL) {
  368. element.style.borderRight = border;
  369. } else {
  370. element.style.borderLeft = border;
  371. }
  372. }
  373. this.addColour_(child);
  374. }
  375. };
  376. /**
  377. * Unhighlight any previously specified option.
  378. */
  379. Blockly.Toolbox.prototype.clearSelection = function() {
  380. this.tree_.setSelectedItem(null);
  381. };
  382. /**
  383. * Return the deletion rectangle for this toolbox.
  384. * @return {goog.math.Rect} Rectangle in which to delete.
  385. */
  386. Blockly.Toolbox.prototype.getClientRect = function() {
  387. if (!this.HtmlDiv) {
  388. return null;
  389. }
  390. // BIG_NUM is offscreen padding so that blocks dragged beyond the toolbox
  391. // area are still deleted. Must be smaller than Infinity, but larger than
  392. // the largest screen size.
  393. var BIG_NUM = 10000000;
  394. var toolboxRect = this.HtmlDiv.getBoundingClientRect();
  395. var x = toolboxRect.left;
  396. var y = toolboxRect.top;
  397. var width = toolboxRect.width;
  398. var height = toolboxRect.height;
  399. // Assumes that the toolbox is on the SVG edge. If this changes
  400. // (e.g. toolboxes in mutators) then this code will need to be more complex.
  401. if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
  402. return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width,
  403. 2 * BIG_NUM);
  404. } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
  405. return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
  406. } else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
  407. return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM,
  408. BIG_NUM + y + height);
  409. } else { // Bottom
  410. return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width);
  411. }
  412. };
  413. /**
  414. * Update the flyout's contents without closing it. Should be used in response
  415. * to a change in one of the dynamic categories, such as variables or
  416. * procedures.
  417. */
  418. Blockly.Toolbox.prototype.refreshSelection = function() {
  419. var selectedItem = this.tree_.getSelectedItem();
  420. if (selectedItem && selectedItem.blocks) {
  421. this.flyout_.show(selectedItem.blocks);
  422. }
  423. };
  424. // Extending Closure's Tree UI.
  425. /**
  426. * Extention of a TreeControl object that uses a custom tree node.
  427. * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree.
  428. * @param {Object} config The configuration for the tree. See
  429. * goog.ui.tree.TreeControl.DefaultConfig.
  430. * @constructor
  431. * @extends {goog.ui.tree.TreeControl}
  432. */
  433. Blockly.Toolbox.TreeControl = function(toolbox, config) {
  434. this.toolbox_ = toolbox;
  435. goog.ui.tree.TreeControl.call(this, goog.html.SafeHtml.EMPTY, config);
  436. };
  437. goog.inherits(Blockly.Toolbox.TreeControl, goog.ui.tree.TreeControl);
  438. /**
  439. * Adds touch handling to TreeControl.
  440. * @override
  441. */
  442. Blockly.Toolbox.TreeControl.prototype.enterDocument = function() {
  443. Blockly.Toolbox.TreeControl.superClass_.enterDocument.call(this);
  444. var el = this.getElement();
  445. // Add touch handler.
  446. if (goog.events.BrowserFeature.TOUCH_ENABLED) {
  447. Blockly.bindEventWithChecks_(el, goog.events.EventType.TOUCHSTART, this,
  448. this.handleTouchEvent_);
  449. }
  450. };
  451. /**
  452. * Handles touch events.
  453. * @param {!goog.events.BrowserEvent} e The browser event.
  454. * @private
  455. */
  456. Blockly.Toolbox.TreeControl.prototype.handleTouchEvent_ = function(e) {
  457. e.preventDefault();
  458. var node = this.getNodeFromEvent_(e);
  459. if (node && e.type === goog.events.EventType.TOUCHSTART) {
  460. // Fire asynchronously since onMouseDown takes long enough that the browser
  461. // would fire the default mouse event before this method returns.
  462. setTimeout(function() {
  463. node.onMouseDown(e); // Same behaviour for click and touch.
  464. }, 1);
  465. }
  466. };
  467. /**
  468. * Creates a new tree node using a custom tree node.
  469. * @param {string=} opt_html The HTML content of the node label.
  470. * @return {!goog.ui.tree.TreeNode} The new item.
  471. * @override
  472. */
  473. Blockly.Toolbox.TreeControl.prototype.createNode = function(opt_html) {
  474. return new Blockly.Toolbox.TreeNode(this.toolbox_, opt_html ?
  475. goog.html.SafeHtml.htmlEscape(opt_html) : goog.html.SafeHtml.EMPTY,
  476. this.getConfig(), this.getDomHelper());
  477. };
  478. /**
  479. * Display/hide the flyout when an item is selected.
  480. * @param {goog.ui.tree.BaseNode} node The item to select.
  481. * @override
  482. */
  483. Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) {
  484. var toolbox = this.toolbox_;
  485. if (node == this.selectedItem_ || node == toolbox.tree_) {
  486. return;
  487. }
  488. if (toolbox.lastCategory_) {
  489. toolbox.lastCategory_.getRowElement().style.backgroundColor = '';
  490. }
  491. if (node) {
  492. var hexColour = node.hexColour || '#57e';
  493. node.getRowElement().style.backgroundColor = hexColour;
  494. // Add colours to child nodes which may have been collapsed and thus
  495. // not rendered.
  496. toolbox.addColour_(node);
  497. }
  498. var oldNode = this.getSelectedItem();
  499. goog.ui.tree.TreeControl.prototype.setSelectedItem.call(this, node);
  500. if (node && node.blocks && node.blocks.length) {
  501. toolbox.flyout_.show(node.blocks);
  502. // Scroll the flyout to the top if the category has changed.
  503. if (toolbox.lastCategory_ != node) {
  504. toolbox.flyout_.scrollToStart();
  505. }
  506. } else {
  507. // Hide the flyout.
  508. toolbox.flyout_.hide();
  509. }
  510. if (oldNode != node && oldNode != this) {
  511. var event = new Blockly.Events.Ui(null, 'category',
  512. oldNode && oldNode.getHtml(), node && node.getHtml());
  513. event.workspaceId = toolbox.workspace_.id;
  514. Blockly.Events.fire(event);
  515. }
  516. if (node) {
  517. toolbox.lastCategory_ = node;
  518. }
  519. };
  520. /**
  521. * A single node in the tree, customized for Blockly's UI.
  522. * @param {Blockly.Toolbox} toolbox The parent toolbox for this tree.
  523. * @param {!goog.html.SafeHtml} html The HTML content of the node label.
  524. * @param {Object=} opt_config The configuration for the tree. See
  525. * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config
  526. * will be used.
  527. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  528. * @constructor
  529. * @extends {goog.ui.tree.TreeNode}
  530. */
  531. Blockly.Toolbox.TreeNode = function(toolbox, html, opt_config, opt_domHelper) {
  532. goog.ui.tree.TreeNode.call(this, html, opt_config, opt_domHelper);
  533. if (toolbox) {
  534. var resize = function() {
  535. // Even though the div hasn't changed size, the visible workspace
  536. // surface of the workspace has, so we may need to reposition everything.
  537. Blockly.svgResize(toolbox.workspace_);
  538. };
  539. // Fire a resize event since the toolbox may have changed width.
  540. goog.events.listen(toolbox.tree_,
  541. goog.ui.tree.BaseNode.EventType.EXPAND, resize);
  542. goog.events.listen(toolbox.tree_,
  543. goog.ui.tree.BaseNode.EventType.COLLAPSE, resize);
  544. }
  545. };
  546. goog.inherits(Blockly.Toolbox.TreeNode, goog.ui.tree.TreeNode);
  547. /**
  548. * Supress population of the +/- icon.
  549. * @return {!goog.html.SafeHtml} The source for the icon.
  550. * @override
  551. */
  552. Blockly.Toolbox.TreeNode.prototype.getExpandIconSafeHtml = function() {
  553. return goog.html.SafeHtml.create('span');
  554. };
  555. /**
  556. * Expand or collapse the node on mouse click.
  557. * @param {!goog.events.BrowserEvent} e The browser event.
  558. * @override
  559. */
  560. Blockly.Toolbox.TreeNode.prototype.onMouseDown = function(e) {
  561. // Expand icon.
  562. if (this.hasChildren() && this.isUserCollapsible_) {
  563. this.toggle();
  564. this.select();
  565. } else if (this.isSelected()) {
  566. this.getTree().setSelectedItem(null);
  567. } else {
  568. this.select();
  569. }
  570. this.updateRow();
  571. };
  572. /**
  573. * Supress the inherited double-click behaviour.
  574. * @param {!goog.events.BrowserEvent} e The browser event.
  575. * @override
  576. * @private
  577. */
  578. Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) {
  579. // NOP.
  580. };
  581. /**
  582. * Remap event.keyCode in horizontalLayout so that arrow
  583. * keys work properly and call original onKeyDown handler.
  584. * @param {!goog.events.BrowserEvent} e The browser event.
  585. * @return {boolean} The handled value.
  586. * @override
  587. * @private
  588. */
  589. Blockly.Toolbox.TreeNode.prototype.onKeyDown = function(e) {
  590. if (this.tree.toolbox_.horizontalLayout_) {
  591. var map = {};
  592. var next = goog.events.KeyCodes.DOWN
  593. var prev = goog.events.KeyCodes.UP
  594. map[goog.events.KeyCodes.RIGHT] = this.rightToLeft_ ? prev : next;
  595. map[goog.events.KeyCodes.LEFT] = this.rightToLeft_ ? next : prev;
  596. map[goog.events.KeyCodes.UP] = goog.events.KeyCodes.LEFT;
  597. map[goog.events.KeyCodes.DOWN] = goog.events.KeyCodes.RIGHT;
  598. var newKeyCode = map[e.keyCode];
  599. e.keyCode = newKeyCode || e.keyCode;
  600. }
  601. return Blockly.Toolbox.TreeNode.superClass_.onKeyDown.call(this, e);
  602. };
  603. /**
  604. * A blank separator node in the tree.
  605. * @param {Object=} config The configuration for the tree. See
  606. * goog.ui.tree.TreeControl.DefaultConfig. If not specified, a default config
  607. * will be used.
  608. * @constructor
  609. * @extends {Blockly.Toolbox.TreeNode}
  610. */
  611. Blockly.Toolbox.TreeSeparator = function(config) {
  612. Blockly.Toolbox.TreeNode.call(this, null, '', config);
  613. };
  614. goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode);