flyout.js 46 KB

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