flyout.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248
  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 Flyout tray containing blocks which may be created.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.Flyout');
  26. goog.require('Blockly.Block');
  27. goog.require('Blockly.Comment');
  28. goog.require('Blockly.Events');
  29. goog.require('Blockly.WorkspaceSvg');
  30. goog.require('goog.dom');
  31. goog.require('goog.events');
  32. goog.require('goog.math.Rect');
  33. goog.require('goog.userAgent');
  34. /**
  35. * Class for a flyout.
  36. * @param {!Object} workspaceOptions Dictionary of options for the workspace.
  37. * @constructor
  38. */
  39. Blockly.Flyout = function(workspaceOptions) {
  40. workspaceOptions.getMetrics = this.getMetrics_.bind(this);
  41. workspaceOptions.setMetrics = this.setMetrics_.bind(this);
  42. /**
  43. * @type {!Blockly.Workspace}
  44. * @private
  45. */
  46. this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
  47. this.workspace_.isFlyout = true;
  48. /**
  49. * Is RTL vs LTR.
  50. * @type {boolean}
  51. */
  52. this.RTL = !!workspaceOptions.RTL;
  53. /**
  54. * Flyout should be laid out horizontally vs vertically.
  55. * @type {boolean}
  56. * @private
  57. */
  58. this.horizontalLayout_ = workspaceOptions.horizontalLayout;
  59. /**
  60. * Position of the toolbox and flyout relative to the workspace.
  61. * @type {number}
  62. * @private
  63. */
  64. this.toolboxPosition_ = workspaceOptions.toolboxPosition;
  65. /**
  66. * Opaque data that can be passed to Blockly.unbindEvent_.
  67. * @type {!Array.<!Array>}
  68. * @private
  69. */
  70. this.eventWrappers_ = [];
  71. /**
  72. * List of background buttons that lurk behind each block to catch clicks
  73. * landing in the blocks' lakes and bays.
  74. * @type {!Array.<!Element>}
  75. * @private
  76. */
  77. this.buttons_ = [];
  78. /**
  79. * List of event listeners.
  80. * @type {!Array.<!Array>}
  81. * @private
  82. */
  83. this.listeners_ = [];
  84. /**
  85. * List of blocks that should always be disabled.
  86. * @type {!Array.<!Blockly.Block>}
  87. * @private
  88. */
  89. this.permanentlyDisabled_ = [];
  90. };
  91. /**
  92. * Does the flyout automatically close when a block is created?
  93. * @type {boolean}
  94. */
  95. Blockly.Flyout.prototype.autoClose = true;
  96. /**
  97. * Corner radius of the flyout background.
  98. * @type {number}
  99. * @const
  100. */
  101. Blockly.Flyout.prototype.CORNER_RADIUS = 8;
  102. /**
  103. * Number of pixels the mouse must move before a drag/scroll starts. Because the
  104. * drag-intention is determined when this is reached, it is larger than
  105. * Blockly.DRAG_RADIUS so that the drag-direction is clearer.
  106. */
  107. Blockly.Flyout.prototype.DRAG_RADIUS = 10;
  108. /**
  109. * Margin around the edges of the blocks in the flyout.
  110. * @type {number}
  111. * @const
  112. */
  113. Blockly.Flyout.prototype.MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS;
  114. /**
  115. * Top/bottom padding between scrollbar and edge of flyout background.
  116. * @type {number}
  117. * @const
  118. */
  119. Blockly.Flyout.prototype.SCROLLBAR_PADDING = 2;
  120. /**
  121. * Width of flyout.
  122. * @type {number}
  123. * @private
  124. */
  125. Blockly.Flyout.prototype.width_ = 0;
  126. /**
  127. * Height of flyout.
  128. * @type {number}
  129. * @private
  130. */
  131. Blockly.Flyout.prototype.height_ = 0;
  132. /**
  133. * Is the flyout dragging (scrolling)?
  134. * DRAG_NONE - no drag is ongoing or state is undetermined.
  135. * DRAG_STICKY - still within the sticky drag radius.
  136. * DRAG_FREE - in scroll mode (never create a new block).
  137. * @private
  138. */
  139. Blockly.Flyout.prototype.dragMode_ = Blockly.DRAG_NONE;
  140. /**
  141. * Range of a drag angle from a flyout considered "dragging toward workspace".
  142. * Drags that are within the bounds of this many degrees from the orthogonal
  143. * line to the flyout edge are considered to be "drags toward the workspace".
  144. * Example:
  145. * Flyout Edge Workspace
  146. * [block] / <-within this angle, drags "toward workspace" |
  147. * [block] ---- orthogonal to flyout boundary ---- |
  148. * [block] \ |
  149. * The angle is given in degrees from the orthogonal.
  150. *
  151. * This is used to know when to create a new block and when to scroll the
  152. * flyout. Setting it to 360 means that all drags create a new block.
  153. * @type {number}
  154. * @private
  155. */
  156. Blockly.Flyout.prototype.dragAngleRange_ = 80;
  157. /**
  158. * Creates the flyout's DOM. Only needs to be called once.
  159. * @return {!Element} The flyout's SVG group.
  160. */
  161. Blockly.Flyout.prototype.createDom = function() {
  162. /*
  163. <g>
  164. <path class="blocklyFlyoutBackground"/>
  165. <g class="blocklyFlyout"></g>
  166. </g>
  167. */
  168. this.svgGroup_ = Blockly.createSvgElement('g',
  169. {'class': 'blocklyFlyout'}, null);
  170. this.svgBackground_ = Blockly.createSvgElement('path',
  171. {'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
  172. this.svgGroup_.appendChild(this.workspace_.createDom());
  173. return this.svgGroup_;
  174. };
  175. /**
  176. * Initializes the flyout.
  177. * @param {!Blockly.Workspace} targetWorkspace The workspace in which to create
  178. * new blocks.
  179. */
  180. Blockly.Flyout.prototype.init = function(targetWorkspace) {
  181. this.targetWorkspace_ = targetWorkspace;
  182. this.workspace_.targetWorkspace = targetWorkspace;
  183. // Add scrollbar.
  184. this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
  185. this.horizontalLayout_, false);
  186. this.hide();
  187. Array.prototype.push.apply(this.eventWrappers_,
  188. Blockly.bindEvent_(this.svgGroup_, 'wheel', this, this.wheel_));
  189. if (!this.autoClose) {
  190. this.filterWrapper_ = this.filterForCapacity_.bind(this);
  191. this.targetWorkspace_.addChangeListener(this.filterWrapper_);
  192. }
  193. // Dragging the flyout up and down.
  194. Array.prototype.push.apply(this.eventWrappers_,
  195. Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_));
  196. };
  197. /**
  198. * Dispose of this flyout.
  199. * Unlink from all DOM elements to prevent memory leaks.
  200. */
  201. Blockly.Flyout.prototype.dispose = function() {
  202. this.hide();
  203. Blockly.unbindEvent_(this.eventWrappers_);
  204. if (this.filterWrapper_) {
  205. this.targetWorkspace_.removeChangeListener(this.filterWrapper_);
  206. this.filterWrapper_ = null;
  207. }
  208. if (this.scrollbar_) {
  209. this.scrollbar_.dispose();
  210. this.scrollbar_ = null;
  211. }
  212. if (this.workspace_) {
  213. this.workspace_.targetWorkspace = null;
  214. this.workspace_.dispose();
  215. this.workspace_ = null;
  216. }
  217. if (this.svgGroup_) {
  218. goog.dom.removeNode(this.svgGroup_);
  219. this.svgGroup_ = null;
  220. }
  221. this.svgBackground_ = null;
  222. this.targetWorkspace_ = null;
  223. };
  224. /**
  225. * Get the width of the flyout.
  226. * @return {number} The width of the flyout.
  227. */
  228. Blockly.Flyout.prototype.getWidth = function() {
  229. return this.width_;
  230. };
  231. /**
  232. * Get the height of the flyout.
  233. * @return {number} The width of the flyout.
  234. */
  235. Blockly.Flyout.prototype.getHeight = function() {
  236. return this.height_;
  237. };
  238. /**
  239. * Return an object with all the metrics required to size scrollbars for the
  240. * flyout. The following properties are computed:
  241. * .viewHeight: Height of the visible rectangle,
  242. * .viewWidth: Width of the visible rectangle,
  243. * .contentHeight: Height of the contents,
  244. * .contentWidth: Width of the contents,
  245. * .viewTop: Offset of top edge of visible rectangle from parent,
  246. * .contentTop: Offset of the top-most content from the y=0 coordinate,
  247. * .absoluteTop: Top-edge of view.
  248. * .viewLeft: Offset of the left edge of visible rectangle from parent,
  249. * .contentLeft: Offset of the left-most content from the x=0 coordinate,
  250. * .absoluteLeft: Left-edge of view.
  251. * @return {Object} Contains size and position metrics of the flyout.
  252. * @private
  253. */
  254. Blockly.Flyout.prototype.getMetrics_ = function() {
  255. if (!this.isVisible()) {
  256. // Flyout is hidden.
  257. return null;
  258. }
  259. try {
  260. var optionBox = this.workspace_.getCanvas().getBBox();
  261. } catch (e) {
  262. // Firefox has trouble with hidden elements (Bug 528969).
  263. var optionBox = {height: 0, y: 0, width: 0, x: 0};
  264. }
  265. var absoluteTop = this.SCROLLBAR_PADDING;
  266. var absoluteLeft = this.SCROLLBAR_PADDING;
  267. if (this.horizontalLayout_) {
  268. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
  269. absoluteTop = 0;
  270. }
  271. var viewHeight = this.height_;
  272. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
  273. viewHeight += this.MARGIN - this.SCROLLBAR_PADDING;
  274. }
  275. var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
  276. } else {
  277. absoluteLeft = 0;
  278. var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
  279. var viewWidth = this.width_;
  280. if (!this.RTL) {
  281. viewWidth -= this.SCROLLBAR_PADDING;
  282. }
  283. }
  284. var metrics = {
  285. viewHeight: viewHeight,
  286. viewWidth: viewWidth,
  287. contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale,
  288. contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale,
  289. viewTop: -this.workspace_.scrollY,
  290. viewLeft: -this.workspace_.scrollX,
  291. contentTop: optionBox.y,
  292. contentLeft: optionBox.x,
  293. absoluteTop: absoluteTop,
  294. absoluteLeft: absoluteLeft
  295. };
  296. return metrics;
  297. };
  298. /**
  299. * Sets the translation of the flyout to match the scrollbars.
  300. * @param {!Object} xyRatio Contains a y property which is a float
  301. * between 0 and 1 specifying the degree of scrolling and a
  302. * similar x property.
  303. * @private
  304. */
  305. Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) {
  306. var metrics = this.getMetrics_();
  307. // This is a fix to an apparent race condition.
  308. if (!metrics) {
  309. return;
  310. }
  311. if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) {
  312. this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y;
  313. } else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) {
  314. this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x;
  315. }
  316. this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
  317. this.workspace_.scrollY + metrics.absoluteTop);
  318. };
  319. /**
  320. * Move the flyout to the edge of the workspace.
  321. */
  322. Blockly.Flyout.prototype.position = function() {
  323. if (!this.isVisible()) {
  324. return;
  325. }
  326. var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics();
  327. if (!targetWorkspaceMetrics) {
  328. // Hidden components will return null.
  329. return;
  330. }
  331. var edgeWidth = this.horizontalLayout_ ?
  332. targetWorkspaceMetrics.viewWidth : this.width_;
  333. edgeWidth -= this.CORNER_RADIUS;
  334. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
  335. edgeWidth *= -1;
  336. }
  337. this.setBackgroundPath_(edgeWidth,
  338. this.horizontalLayout_ ? this.height_ :
  339. targetWorkspaceMetrics.viewHeight);
  340. var x = targetWorkspaceMetrics.absoluteLeft;
  341. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
  342. x += targetWorkspaceMetrics.viewWidth;
  343. x -= this.width_;
  344. }
  345. var y = targetWorkspaceMetrics.absoluteTop;
  346. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
  347. y += targetWorkspaceMetrics.viewHeight;
  348. y -= this.height_;
  349. }
  350. this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
  351. // Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
  352. // horizontal.
  353. if (this.horizontalLayout_) {
  354. this.width_ = targetWorkspaceMetrics.viewWidth;
  355. } else {
  356. this.height_ = targetWorkspaceMetrics.viewHeight;
  357. }
  358. // Update the scrollbar (if one exists).
  359. if (this.scrollbar_) {
  360. this.scrollbar_.resize();
  361. }
  362. };
  363. /**
  364. * Create and set the path for the visible boundaries of the flyout.
  365. * @param {number} width The width of the flyout, not including the
  366. * rounded corners.
  367. * @param {number} height The height of the flyout, not including
  368. * rounded corners.
  369. * @private
  370. */
  371. Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) {
  372. if (this.horizontalLayout_) {
  373. this.setBackgroundPathHorizontal_(width, height);
  374. } else {
  375. this.setBackgroundPathVertical_(width, height);
  376. }
  377. };
  378. /**
  379. * Create and set the path for the visible boundaries of the flyout in vertical
  380. * mode.
  381. * @param {number} width The width of the flyout, not including the
  382. * rounded corners.
  383. * @param {number} height The height of the flyout, not including
  384. * rounded corners.
  385. * @private
  386. */
  387. Blockly.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) {
  388. var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT;
  389. // Decide whether to start on the left or right.
  390. var path = ['M ' + (atRight ? this.width_ : 0) + ',0'];
  391. // Top.
  392. path.push('h', width);
  393. // Rounded corner.
  394. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
  395. atRight ? 0 : 1,
  396. atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
  397. this.CORNER_RADIUS);
  398. // Side closest to workspace.
  399. path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2));
  400. // Rounded corner.
  401. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
  402. atRight ? 0 : 1,
  403. atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
  404. this.CORNER_RADIUS);
  405. // Bottom.
  406. path.push('h', -width);
  407. path.push('z');
  408. this.svgBackground_.setAttribute('d', path.join(' '));
  409. };
  410. /**
  411. * Create and set the path for the visible boundaries of the flyout in
  412. * horizontal mode.
  413. * @param {number} width The width of the flyout, not including the
  414. * rounded corners.
  415. * @param {number} height The height of the flyout, not including
  416. * rounded corners.
  417. * @private
  418. */
  419. Blockly.Flyout.prototype.setBackgroundPathHorizontal_ = function(width,
  420. height) {
  421. var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP;
  422. // Start at top left.
  423. var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
  424. if (atTop) {
  425. // Top.
  426. path.push('h', width + this.CORNER_RADIUS);
  427. // Right.
  428. path.push('v', height);
  429. // Bottom.
  430. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
  431. -this.CORNER_RADIUS, this.CORNER_RADIUS);
  432. path.push('h', -1 * (width - this.CORNER_RADIUS));
  433. // Left.
  434. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
  435. -this.CORNER_RADIUS, -this.CORNER_RADIUS);
  436. path.push('z');
  437. } else {
  438. // Top.
  439. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
  440. this.CORNER_RADIUS, -this.CORNER_RADIUS);
  441. path.push('h', width - this.CORNER_RADIUS);
  442. // Right.
  443. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
  444. this.CORNER_RADIUS, this.CORNER_RADIUS);
  445. path.push('v', height - this.CORNER_RADIUS);
  446. // Bottom.
  447. path.push('h', -width - this.CORNER_RADIUS);
  448. // Left.
  449. path.push('z');
  450. }
  451. this.svgBackground_.setAttribute('d', path.join(' '));
  452. };
  453. /**
  454. * Scroll the flyout to the top.
  455. */
  456. Blockly.Flyout.prototype.scrollToStart = function() {
  457. this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? Infinity : 0);
  458. };
  459. /**
  460. * Scroll the flyout.
  461. * @param {!Event} e Mouse wheel scroll event.
  462. * @private
  463. */
  464. Blockly.Flyout.prototype.wheel_ = function(e) {
  465. var delta = this.horizontalLayout_ ? e.deltaX : e.deltaY;
  466. if (delta) {
  467. if (goog.userAgent.GECKO) {
  468. // Firefox's deltas are a tenth that of Chrome/Safari.
  469. delta *= 10;
  470. }
  471. var metrics = this.getMetrics_();
  472. var pos = this.horizontalLayout_ ? metrics.viewLeft + delta :
  473. metrics.viewTop + delta;
  474. var limit = this.horizontalLayout_ ?
  475. metrics.contentWidth - metrics.viewWidth :
  476. metrics.contentHeight - metrics.viewHeight;
  477. pos = Math.min(pos, limit);
  478. pos = Math.max(pos, 0);
  479. this.scrollbar_.set(pos);
  480. }
  481. // Don't scroll the page.
  482. e.preventDefault();
  483. // Don't propagate mousewheel event (zooming).
  484. e.stopPropagation();
  485. };
  486. /**
  487. * Is the flyout visible?
  488. * @return {boolean} True if visible.
  489. */
  490. Blockly.Flyout.prototype.isVisible = function() {
  491. return this.svgGroup_ && this.svgGroup_.style.display == 'block';
  492. };
  493. /**
  494. * Hide and empty the flyout.
  495. */
  496. Blockly.Flyout.prototype.hide = function() {
  497. if (!this.isVisible()) {
  498. return;
  499. }
  500. this.svgGroup_.style.display = 'none';
  501. // Delete all the event listeners.
  502. for (var x = 0, listen; listen = this.listeners_[x]; x++) {
  503. Blockly.unbindEvent_(listen);
  504. }
  505. this.listeners_.length = 0;
  506. if (this.reflowWrapper_) {
  507. this.workspace_.removeChangeListener(this.reflowWrapper_);
  508. this.reflowWrapper_ = null;
  509. }
  510. // Do NOT delete the blocks here. Wait until Flyout.show.
  511. // https://neil.fraser.name/news/2014/08/09/
  512. };
  513. /**
  514. * Show and populate the flyout.
  515. * @param {!Array|string} xmlList List of blocks to show.
  516. * Variables and procedures have a custom set of blocks.
  517. */
  518. Blockly.Flyout.prototype.show = function(xmlList) {
  519. this.hide();
  520. this.clearOldBlocks_();
  521. if (xmlList == Blockly.Variables.NAME_TYPE) {
  522. // Special category for variables.
  523. xmlList =
  524. Blockly.Variables.flyoutCategory(this.workspace_.targetWorkspace);
  525. } else if (xmlList == Blockly.Procedures.NAME_TYPE) {
  526. // Special category for procedures.
  527. xmlList =
  528. Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace);
  529. }
  530. this.svgGroup_.style.display = 'block';
  531. // Create the blocks to be shown in this flyout.
  532. var blocks = [];
  533. var gaps = [];
  534. this.permanentlyDisabled_.length = 0;
  535. for (var i = 0, xml; xml = xmlList[i]; i++) {
  536. if (xml.tagName && xml.tagName.toUpperCase() == 'BLOCK') {
  537. var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_);
  538. if (curBlock.disabled) {
  539. // Record blocks that were initially disabled.
  540. // Do not enable these blocks as a result of capacity filtering.
  541. this.permanentlyDisabled_.push(curBlock);
  542. }
  543. blocks.push(curBlock);
  544. var gap = parseInt(xml.getAttribute('gap'), 10);
  545. gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap);
  546. }
  547. }
  548. this.layoutBlocks_(blocks, gaps);
  549. // IE 11 is an incompetent browser that fails to fire mouseout events.
  550. // When the mouse is over the background, deselect all blocks.
  551. var deselectAll = function() {
  552. var topBlocks = this.workspace_.getTopBlocks(false);
  553. for (var i = 0, block; block = topBlocks[i]; i++) {
  554. block.removeSelect();
  555. }
  556. };
  557. this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
  558. this, deselectAll));
  559. if (this.horizontalLayout_) {
  560. this.height_ = 0;
  561. } else {
  562. this.width_ = 0;
  563. }
  564. this.reflow();
  565. this.filterForCapacity_();
  566. // Correctly position the flyout's scrollbar when it opens.
  567. this.position();
  568. this.reflowWrapper_ = this.reflow.bind(this);
  569. this.workspace_.addChangeListener(this.reflowWrapper_);
  570. };
  571. /**
  572. * Lay out the blocks in the flyout.
  573. * @param {!Array.<!Blockly.BlockSvg>} blocks The blocks to lay out.
  574. * @param {!Array.<number>} gaps The visible gaps between blocks.
  575. * @private
  576. */
  577. Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps) {
  578. var margin = this.MARGIN;
  579. var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH;
  580. var cursorY = margin;
  581. if (this.horizontalLayout_ && this.RTL) {
  582. blocks = blocks.reverse();
  583. }
  584. for (var i = 0, block; block = blocks[i]; i++) {
  585. var allBlocks = block.getDescendants();
  586. for (var j = 0, child; child = allBlocks[j]; j++) {
  587. // Mark blocks as being inside a flyout. This is used to detect and
  588. // prevent the closure of the flyout if the user right-clicks on such a
  589. // block.
  590. child.isInFlyout = true;
  591. }
  592. block.render();
  593. var root = block.getSvgRoot();
  594. var blockHW = block.getHeightWidth();
  595. var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
  596. if (this.horizontalLayout_) {
  597. cursorX += tab;
  598. }
  599. if (this.horizontalLayout_ && this.RTL) {
  600. block.moveBy(cursorX + blockHW.width - tab, cursorY);
  601. } else {
  602. block.moveBy(cursorX, cursorY);
  603. }
  604. if (this.horizontalLayout_) {
  605. cursorX += (blockHW.width + gaps[i] - tab);
  606. } else {
  607. cursorY += blockHW.height + gaps[i];
  608. }
  609. // Create an invisible rectangle under the block to act as a button. Just
  610. // using the block as a button is poor, since blocks have holes in them.
  611. var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null);
  612. rect.tooltip = block;
  613. Blockly.Tooltip.bindMouseEvents(rect);
  614. // Add the rectangles under the blocks, so that the blocks' tooltips work.
  615. this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
  616. block.flyoutRect_ = rect;
  617. this.buttons_[i] = rect;
  618. this.addBlockListeners_(root, block, rect);
  619. }
  620. };
  621. /**
  622. * Delete blocks and background buttons from a previous showing of the flyout.
  623. * @private
  624. */
  625. Blockly.Flyout.prototype.clearOldBlocks_ = function() {
  626. // Delete any blocks from a previous showing.
  627. var oldBlocks = this.workspace_.getTopBlocks(false);
  628. for (var i = 0, block; block = oldBlocks[i]; i++) {
  629. if (block.workspace == this.workspace_) {
  630. block.dispose(false, false);
  631. }
  632. }
  633. // Delete any background buttons from a previous showing.
  634. for (var j = 0, rect; rect = this.buttons_[j]; j++) {
  635. goog.dom.removeNode(rect);
  636. }
  637. this.buttons_.length = 0;
  638. };
  639. /**
  640. * Add listeners to a block that has been added to the flyout.
  641. * @param {!Element} root The root node of the SVG group the block is in.
  642. * @param {!Blockly.Block} block The block to add listeners for.
  643. * @param {!Element} rect The invisible rectangle under the block that acts as
  644. * a button for that block.
  645. * @private
  646. */
  647. Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
  648. this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
  649. this.blockMouseDown_(block)));
  650. this.listeners_.push(Blockly.bindEvent_(rect, 'mousedown', null,
  651. this.blockMouseDown_(block)));
  652. this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block,
  653. block.addSelect));
  654. this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block,
  655. block.removeSelect));
  656. this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block,
  657. block.addSelect));
  658. this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block,
  659. block.removeSelect));
  660. };
  661. /**
  662. * Handle a mouse-down on an SVG block in a non-closing flyout.
  663. * @param {!Blockly.Block} block The flyout block to copy.
  664. * @return {!Function} Function to call when block is clicked.
  665. * @private
  666. */
  667. Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
  668. var flyout = this;
  669. return function(e) {
  670. Blockly.terminateDrag_();
  671. Blockly.hideChaff(true);
  672. if (Blockly.isRightButton(e)) {
  673. // Right-click.
  674. block.showContextMenu_(e);
  675. } else {
  676. // Left-click (or middle click)
  677. Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
  678. // Record the current mouse position.
  679. flyout.startDragMouseY_ = e.clientY;
  680. flyout.startDragMouseX_ = e.clientX;
  681. Blockly.Flyout.startDownEvent_ = e;
  682. Blockly.Flyout.startBlock_ = block;
  683. Blockly.Flyout.startFlyout_ = flyout;
  684. Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document,
  685. 'mouseup', flyout, flyout.onMouseUp_);
  686. Blockly.Flyout.onMouseMoveBlockWrapper_ = Blockly.bindEvent_(document,
  687. 'mousemove', flyout, flyout.onMouseMoveBlock_);
  688. }
  689. // This event has been handled. No need to bubble up to the document.
  690. e.stopPropagation();
  691. e.preventDefault();
  692. };
  693. };
  694. /**
  695. * Mouse down on the flyout background. Start a vertical scroll drag.
  696. * @param {!Event} e Mouse down event.
  697. * @private
  698. */
  699. Blockly.Flyout.prototype.onMouseDown_ = function(e) {
  700. if (Blockly.isRightButton(e)) {
  701. return;
  702. }
  703. Blockly.hideChaff(true);
  704. this.dragMode_ = Blockly.DRAG_FREE;
  705. this.startDragMouseY_ = e.clientY;
  706. this.startDragMouseX_ = e.clientX;
  707. Blockly.Flyout.startFlyout_ = this;
  708. Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove',
  709. this, this.onMouseMove_);
  710. Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup',
  711. this, Blockly.Flyout.terminateDrag_);
  712. // This event has been handled. No need to bubble up to the document.
  713. e.preventDefault();
  714. e.stopPropagation();
  715. };
  716. /**
  717. * Handle a mouse-up anywhere in the SVG pane. Is only registered when a
  718. * block is clicked. We can't use mouseUp on the block since a fast-moving
  719. * cursor can briefly escape the block before it catches up.
  720. * @param {!Event} e Mouse up event.
  721. * @private
  722. */
  723. Blockly.Flyout.prototype.onMouseUp_ = function(e) {
  724. if (!this.workspace_.isDragging()) {
  725. if (this.autoClose) {
  726. this.createBlockFunc_(Blockly.Flyout.startBlock_)(
  727. Blockly.Flyout.startDownEvent_);
  728. } else if (!Blockly.WidgetDiv.isVisible()) {
  729. Blockly.Events.fire(
  730. new Blockly.Events.Ui(Blockly.Flyout.startBlock_, 'click',
  731. undefined, undefined));
  732. }
  733. }
  734. Blockly.terminateDrag_();
  735. };
  736. /**
  737. * Handle a mouse-move to vertically drag the flyout.
  738. * @param {!Event} e Mouse move event.
  739. * @private
  740. */
  741. Blockly.Flyout.prototype.onMouseMove_ = function(e) {
  742. var metrics = this.getMetrics_();
  743. if (this.horizontalLayout_) {
  744. if (metrics.contentWidth - metrics.viewWidth < 0) {
  745. return;
  746. }
  747. var dx = e.clientX - this.startDragMouseX_;
  748. this.startDragMouseX_ = e.clientX;
  749. var x = metrics.viewLeft - dx;
  750. x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth);
  751. this.scrollbar_.set(x);
  752. } else {
  753. if (metrics.contentHeight - metrics.viewHeight < 0) {
  754. return;
  755. }
  756. var dy = e.clientY - this.startDragMouseY_;
  757. this.startDragMouseY_ = e.clientY;
  758. var y = metrics.viewTop - dy;
  759. y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight);
  760. this.scrollbar_.set(y);
  761. }
  762. };
  763. /**
  764. * Mouse button is down on a block in a non-closing flyout. Create the block
  765. * if the mouse moves beyond a small radius. This allows one to play with
  766. * fields without instantiating blocks that instantly self-destruct.
  767. * @param {!Event} e Mouse move event.
  768. * @private
  769. */
  770. Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) {
  771. if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 &&
  772. e.button == 0) {
  773. /* HACK:
  774. Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove events
  775. on certain touch actions. Ignore events with these signatures.
  776. This may result in a one-pixel blind spot in other browsers,
  777. but this shouldn't be noticeable. */
  778. e.stopPropagation();
  779. return;
  780. }
  781. var dx = e.clientX - Blockly.Flyout.startDownEvent_.clientX;
  782. var dy = e.clientY - Blockly.Flyout.startDownEvent_.clientY;
  783. var createBlock = this.determineDragIntention_(dx, dy);
  784. if (createBlock) {
  785. this.createBlockFunc_(Blockly.Flyout.startBlock_)(
  786. Blockly.Flyout.startDownEvent_);
  787. } else if (this.dragMode_ == Blockly.DRAG_FREE) {
  788. // Do a scroll.
  789. this.onMouseMove_(e);
  790. }
  791. e.stopPropagation();
  792. };
  793. /**
  794. * Determine the intention of a drag.
  795. * Updates dragMode_ based on a drag delta and the current mode,
  796. * and returns true if we should create a new block.
  797. * @param {number} dx X delta of the drag.
  798. * @param {number} dy Y delta of the drag.
  799. * @return {boolean} True if a new block should be created.
  800. * @private
  801. */
  802. Blockly.Flyout.prototype.determineDragIntention_ = function(dx, dy) {
  803. if (this.dragMode_ == Blockly.DRAG_FREE) {
  804. // Once in free mode, always stay in free mode and never create a block.
  805. return false;
  806. }
  807. var dragDistance = Math.sqrt(dx * dx + dy * dy);
  808. if (dragDistance < this.DRAG_RADIUS) {
  809. // Still within the sticky drag radius.
  810. this.dragMode_ = Blockly.DRAG_STICKY;
  811. return false;
  812. } else {
  813. if (this.isDragTowardWorkspace_(dx, dy) || !this.scrollbar_.isVisible()) {
  814. // Immediately create a block.
  815. return true;
  816. } else {
  817. // Immediately move to free mode - the drag is away from the workspace.
  818. this.dragMode_ = Blockly.DRAG_FREE;
  819. return false;
  820. }
  821. }
  822. };
  823. /**
  824. * Determine if a drag delta is toward the workspace, based on the position
  825. * and orientation of the flyout. This is used in determineDragIntention_ to
  826. * determine if a new block should be created or if the flyout should scroll.
  827. * @param {number} dx X delta of the drag.
  828. * @param {number} dy Y delta of the drag.
  829. * @return {boolean} true if the drag is toward the workspace.
  830. * @private
  831. */
  832. Blockly.Flyout.prototype.isDragTowardWorkspace_ = function(dx, dy) {
  833. // Direction goes from -180 to 180, with 0 toward the right and 90 on top.
  834. var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
  835. var draggingTowardWorkspace = false;
  836. var range = this.dragAngleRange_;
  837. if (this.horizontalLayout_) {
  838. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
  839. // Horizontal at top.
  840. if (dragDirection < 90 + range && dragDirection > 90 - range) {
  841. draggingTowardWorkspace = true;
  842. }
  843. } else {
  844. // Horizontal at bottom.
  845. if (dragDirection > -90 - range && dragDirection < -90 + range) {
  846. draggingTowardWorkspace = true;
  847. }
  848. }
  849. } else {
  850. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
  851. // Vertical at left.
  852. if (dragDirection < range && dragDirection > -range) {
  853. draggingTowardWorkspace = true;
  854. }
  855. } else {
  856. // Vertical at right.
  857. if (dragDirection < -180 + range || dragDirection > 180 - range) {
  858. draggingTowardWorkspace = true;
  859. }
  860. }
  861. }
  862. return draggingTowardWorkspace;
  863. };
  864. /**
  865. * Create a copy of this block on the workspace.
  866. * @param {!Blockly.Block} originBlock The flyout block to copy.
  867. * @return {!Function} Function to call when block is clicked.
  868. * @private
  869. */
  870. Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
  871. var flyout = this;
  872. return function(e) {
  873. if (Blockly.isRightButton(e)) {
  874. // Right-click. Don't create a block, let the context menu show.
  875. return;
  876. }
  877. if (originBlock.disabled) {
  878. // Beyond capacity.
  879. return;
  880. }
  881. Blockly.Events.disable();
  882. try {
  883. var block = flyout.placeNewBlock_(originBlock);
  884. } finally {
  885. Blockly.Events.enable();
  886. }
  887. if (Blockly.Events.isEnabled()) {
  888. Blockly.Events.setGroup(true);
  889. Blockly.Events.fire(new Blockly.Events.Create(block));
  890. }
  891. if (flyout.autoClose) {
  892. flyout.hide();
  893. } else {
  894. flyout.filterForCapacity_();
  895. }
  896. // Start a dragging operation on the new block.
  897. block.onMouseDown_(e);
  898. Blockly.dragMode_ = Blockly.DRAG_FREE;
  899. block.setDragging_(true);
  900. };
  901. };
  902. /**
  903. * Copy a block from the flyout to the workspace and position it correctly.
  904. * @param {!Blockly.Block} originBlock The flyout block to copy..
  905. * @return {!Blockly.Block} The new block in the main workspace.
  906. * @private
  907. */
  908. Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock) {
  909. var targetWorkspace = this.targetWorkspace_;
  910. var svgRootOld = originBlock.getSvgRoot();
  911. if (!svgRootOld) {
  912. throw 'originBlock is not rendered.';
  913. }
  914. // Figure out where the original block is on the screen, relative to the upper
  915. // left corner of the main workspace.
  916. var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace);
  917. // Take into account that the flyout might have been scrolled horizontally
  918. // (separately from the main workspace).
  919. // Generally a no-op in vertical mode but likely to happen in horizontal
  920. // mode.
  921. var scrollX = this.workspace_.scrollX;
  922. var scale = this.workspace_.scale;
  923. xyOld.x += scrollX / scale - scrollX;
  924. // If the flyout is on the right side, (0, 0) in the flyout is offset to
  925. // the right of (0, 0) in the main workspace. Add an offset to take that
  926. // into account.
  927. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
  928. scrollX = targetWorkspace.getMetrics().viewWidth - this.width_;
  929. scale = targetWorkspace.scale;
  930. // Scale the scroll (getSvgXY_ did not do this).
  931. xyOld.x += scrollX / scale - scrollX;
  932. }
  933. // Take into account that the flyout might have been scrolled vertically
  934. // (separately from the main workspace).
  935. // Generally a no-op in horizontal mode but likely to happen in vertical
  936. // mode.
  937. var scrollY = this.workspace_.scrollY;
  938. scale = this.workspace_.scale;
  939. xyOld.y += scrollY / scale - scrollY;
  940. // If the flyout is on the bottom, (0, 0) in the flyout is offset to be below
  941. // (0, 0) in the main workspace. Add an offset to take that into account.
  942. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
  943. scrollY = targetWorkspace.getMetrics().viewHeight - this.height_;
  944. scale = targetWorkspace.scale;
  945. xyOld.y += scrollY / scale - scrollY;
  946. }
  947. // Create the new block by cloning the block in the flyout (via XML).
  948. var xml = Blockly.Xml.blockToDom(originBlock);
  949. var block = Blockly.Xml.domToBlock(xml, targetWorkspace);
  950. var svgRootNew = block.getSvgRoot();
  951. if (!svgRootNew) {
  952. throw 'block is not rendered.';
  953. }
  954. // Figure out where the new block got placed on the screen, relative to the
  955. // upper left corner of the workspace. This may not be the same as the
  956. // original block because the flyout's origin may not be the same as the
  957. // main workspace's origin.
  958. var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace);
  959. // Scale the scroll (getSvgXY_ did not do this).
  960. xyNew.x +=
  961. targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX;
  962. xyNew.y +=
  963. targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY;
  964. // If the flyout is collapsible and the workspace can't be scrolled.
  965. if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) {
  966. xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale;
  967. xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale;
  968. }
  969. // Move the new block to where the old block is.
  970. block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y);
  971. return block;
  972. };
  973. /**
  974. * Filter the blocks on the flyout to disable the ones that are above the
  975. * capacity limit.
  976. * @private
  977. */
  978. Blockly.Flyout.prototype.filterForCapacity_ = function() {
  979. var remainingCapacity = this.targetWorkspace_.remainingCapacity();
  980. var blocks = this.workspace_.getTopBlocks(false);
  981. for (var i = 0, block; block = blocks[i]; i++) {
  982. if (this.permanentlyDisabled_.indexOf(block) == -1) {
  983. var allBlocks = block.getDescendants();
  984. block.setDisabled(allBlocks.length > remainingCapacity);
  985. }
  986. }
  987. };
  988. /**
  989. * Return the deletion rectangle for this flyout.
  990. * @return {goog.math.Rect} Rectangle in which to delete.
  991. */
  992. Blockly.Flyout.prototype.getClientRect = function() {
  993. if (!this.svgGroup_) {
  994. return null;
  995. }
  996. var flyoutRect = this.svgGroup_.getBoundingClientRect();
  997. // BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
  998. // area are still deleted. Must be larger than the largest screen size,
  999. // but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
  1000. var BIG_NUM = 1000000000;
  1001. var x = flyoutRect.left;
  1002. var y = flyoutRect.top;
  1003. var width = flyoutRect.width;
  1004. var height = flyoutRect.height;
  1005. if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
  1006. return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2,
  1007. BIG_NUM + height);
  1008. } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
  1009. return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2,
  1010. BIG_NUM + height);
  1011. } else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
  1012. return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width,
  1013. BIG_NUM * 2);
  1014. } else { // Right
  1015. return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, BIG_NUM * 2);
  1016. }
  1017. };
  1018. /**
  1019. * Stop binding to the global mouseup and mousemove events.
  1020. * @private
  1021. */
  1022. Blockly.Flyout.terminateDrag_ = function() {
  1023. if (Blockly.Flyout.startFlyout_) {
  1024. Blockly.Flyout.startFlyout_.dragMode_ = Blockly.DRAG_NONE;
  1025. }
  1026. if (Blockly.Flyout.onMouseUpWrapper_) {
  1027. Blockly.unbindEvent_(Blockly.Flyout.onMouseUpWrapper_);
  1028. Blockly.Flyout.onMouseUpWrapper_ = null;
  1029. }
  1030. if (Blockly.Flyout.onMouseMoveBlockWrapper_) {
  1031. Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveBlockWrapper_);
  1032. Blockly.Flyout.onMouseMoveBlockWrapper_ = null;
  1033. }
  1034. if (Blockly.Flyout.onMouseMoveWrapper_) {
  1035. Blockly.unbindEvent_(Blockly.Flyout.onMouseMoveWrapper_);
  1036. Blockly.Flyout.onMouseMoveWrapper_ = null;
  1037. }
  1038. if (Blockly.Flyout.onMouseUpWrapper_) {
  1039. Blockly.unbindEvent_(Blockly.Flyout.onMouseUpWrapper_);
  1040. Blockly.Flyout.onMouseUpWrapper_ = null;
  1041. }
  1042. Blockly.Flyout.startDownEvent_ = null;
  1043. Blockly.Flyout.startBlock_ = null;
  1044. Blockly.Flyout.startFlyout_ = null;
  1045. };
  1046. /**
  1047. * Compute height of flyout. Position button under each block.
  1048. * For RTL: Lay out the blocks right-aligned.
  1049. * @param {!Array<!Blockly.Block>} blocks The blocks to reflow.
  1050. */
  1051. Blockly.Flyout.prototype.reflowHorizontal = function(blocks) {
  1052. this.workspace_.scale = this.targetWorkspace_.scale;
  1053. var flyoutHeight = 0;
  1054. for (var i = 0, block; block = blocks[i]; i++) {
  1055. flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
  1056. }
  1057. flyoutHeight += this.MARGIN * 1.5;
  1058. flyoutHeight *= this.workspace_.scale;
  1059. flyoutHeight += Blockly.Scrollbar.scrollbarThickness;
  1060. if (this.height_ != flyoutHeight) {
  1061. for (var i = 0, block; block = blocks[i]; i++) {
  1062. var blockHW = block.getHeightWidth();
  1063. if (block.flyoutRect_) {
  1064. block.flyoutRect_.setAttribute('width', blockHW.width);
  1065. block.flyoutRect_.setAttribute('height', blockHW.height);
  1066. // Rectangles behind blocks with output tabs are shifted a bit.
  1067. var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
  1068. var blockXY = block.getRelativeToSurfaceXY();
  1069. block.flyoutRect_.setAttribute('y', blockXY.y);
  1070. block.flyoutRect_.setAttribute('x',
  1071. this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
  1072. // For hat blocks we want to shift them down by the hat height
  1073. // since the y coordinate is the corner, not the top of the hat.
  1074. var hatOffset =
  1075. block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0;
  1076. if (hatOffset) {
  1077. block.moveBy(0, hatOffset);
  1078. }
  1079. block.flyoutRect_.setAttribute('y', blockXY.y);
  1080. }
  1081. }
  1082. // Record the height for .getMetrics_ and .position.
  1083. this.height_ = flyoutHeight;
  1084. // Call this since it is possible the trash and zoom buttons need
  1085. // to move. e.g. on a bottom positioned flyout when zoom is clicked.
  1086. this.targetWorkspace_.resize();
  1087. }
  1088. };
  1089. /**
  1090. * Compute width of flyout. Position button under each block.
  1091. * For RTL: Lay out the blocks right-aligned.
  1092. * @param {!Array<!Blockly.Block>} blocks The blocks to reflow.
  1093. */
  1094. Blockly.Flyout.prototype.reflowVertical = function(blocks) {
  1095. this.workspace_.scale = this.targetWorkspace_.scale;
  1096. var flyoutWidth = 0;
  1097. for (var i = 0, block; block = blocks[i]; i++) {
  1098. var width = block.getHeightWidth().width;
  1099. if (block.outputConnection) {
  1100. width -= Blockly.BlockSvg.TAB_WIDTH;
  1101. }
  1102. flyoutWidth = Math.max(flyoutWidth, width);
  1103. }
  1104. flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH;
  1105. flyoutWidth *= this.workspace_.scale;
  1106. flyoutWidth += Blockly.Scrollbar.scrollbarThickness;
  1107. if (this.width_ != flyoutWidth) {
  1108. for (var i = 0, block; block = blocks[i]; i++) {
  1109. var blockHW = block.getHeightWidth();
  1110. if (this.RTL) {
  1111. // With the flyoutWidth known, right-align the blocks.
  1112. var oldX = block.getRelativeToSurfaceXY().x;
  1113. var newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
  1114. newX -= Blockly.BlockSvg.TAB_WIDTH;
  1115. block.moveBy(newX - oldX, 0);
  1116. }
  1117. if (block.flyoutRect_) {
  1118. block.flyoutRect_.setAttribute('width', blockHW.width);
  1119. block.flyoutRect_.setAttribute('height', blockHW.height);
  1120. // Blocks with output tabs are shifted a bit.
  1121. var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
  1122. var blockXY = block.getRelativeToSurfaceXY();
  1123. block.flyoutRect_.setAttribute('x',
  1124. this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
  1125. // For hat blocks we want to shift them down by the hat height
  1126. // since the y coordinate is the corner, not the top of the hat.
  1127. var hatOffset =
  1128. block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0;
  1129. if (hatOffset) {
  1130. block.moveBy(0, hatOffset);
  1131. }
  1132. block.flyoutRect_.setAttribute('y', blockXY.y);
  1133. }
  1134. }
  1135. // Record the width for .getMetrics_ and .position.
  1136. this.width_ = flyoutWidth;
  1137. // Call this since it is possible the trash and zoom buttons need
  1138. // to move. e.g. on a bottom positioned flyout when zoom is clicked.
  1139. this.targetWorkspace_.resize();
  1140. }
  1141. };
  1142. /**
  1143. * Reflow blocks and their buttons.
  1144. */
  1145. Blockly.Flyout.prototype.reflow = function() {
  1146. if (this.reflowWrapper_) {
  1147. this.workspace_.removeChangeListener(this.reflowWrapper_);
  1148. }
  1149. var blocks = this.workspace_.getTopBlocks(false);
  1150. if (this.horizontalLayout_) {
  1151. this.reflowHorizontal(blocks);
  1152. } else {
  1153. this.reflowVertical(blocks);
  1154. }
  1155. if (this.reflowWrapper_) {
  1156. this.workspace_.addChangeListener(this.reflowWrapper_);
  1157. }
  1158. };