block_svg.js 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2012 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 Methods for graphically rendering a block as SVG.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.BlockSvg');
  26. goog.require('Blockly.Block');
  27. goog.require('Blockly.ContextMenu');
  28. goog.require('Blockly.Touch');
  29. goog.require('Blockly.RenderedConnection');
  30. goog.require('goog.Timer');
  31. goog.require('goog.asserts');
  32. goog.require('goog.dom');
  33. goog.require('goog.math.Coordinate');
  34. goog.require('goog.userAgent');
  35. /**
  36. * Class for a block's SVG representation.
  37. * Not normally called directly, workspace.newBlock() is preferred.
  38. * @param {!Blockly.Workspace} workspace The block's workspace.
  39. * @param {?string} prototypeName Name of the language object containing
  40. * type-specific functions for this block.
  41. * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
  42. * create a new id.
  43. * @extends {Blockly.Block}
  44. * @constructor
  45. */
  46. Blockly.BlockSvg = function(workspace, prototypeName, opt_id) {
  47. // Create core elements for the block.
  48. /**
  49. * @type {SVGElement}
  50. * @private
  51. */
  52. this.svgGroup_ = Blockly.createSvgElement('g', {}, null);
  53. /**
  54. * @type {SVGElement}
  55. * @private
  56. */
  57. this.svgPathDark_ = Blockly.createSvgElement('path',
  58. {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'},
  59. this.svgGroup_);
  60. /**
  61. * @type {SVGElement}
  62. * @private
  63. */
  64. this.svgPath_ = Blockly.createSvgElement('path', {'class': 'blocklyPath'},
  65. this.svgGroup_);
  66. /**
  67. * @type {SVGElement}
  68. * @private
  69. */
  70. this.svgPathLight_ = Blockly.createSvgElement('path',
  71. {'class': 'blocklyPathLight'}, this.svgGroup_);
  72. this.svgPath_.tooltip = this;
  73. /** @type {boolean} */
  74. this.rendered = false;
  75. Blockly.Tooltip.bindMouseEvents(this.svgPath_);
  76. Blockly.BlockSvg.superClass_.constructor.call(this,
  77. workspace, prototypeName, opt_id);
  78. };
  79. goog.inherits(Blockly.BlockSvg, Blockly.Block);
  80. /**
  81. * Height of this block, not including any statement blocks above or below.
  82. */
  83. Blockly.BlockSvg.prototype.height = 0;
  84. /**
  85. * Width of this block, including any connected value blocks.
  86. */
  87. Blockly.BlockSvg.prototype.width = 0;
  88. /**
  89. * Original location of block being dragged.
  90. * @type {goog.math.Coordinate}
  91. * @private
  92. */
  93. Blockly.BlockSvg.prototype.dragStartXY_ = null;
  94. /**
  95. * Constant for identifying rows that are to be rendered inline.
  96. * Don't collide with Blockly.INPUT_VALUE and friends.
  97. * @const
  98. */
  99. Blockly.BlockSvg.INLINE = -1;
  100. /**
  101. * Create and initialize the SVG representation of the block.
  102. * May be called more than once.
  103. */
  104. Blockly.BlockSvg.prototype.initSvg = function() {
  105. goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.');
  106. for (var i = 0, input; input = this.inputList[i]; i++) {
  107. input.init();
  108. }
  109. var icons = this.getIcons();
  110. for (var i = 0; i < icons.length; i++) {
  111. icons[i].createIcon();
  112. }
  113. this.updateColour();
  114. this.updateMovable();
  115. if (!this.workspace.options.readOnly && !this.eventsInit_) {
  116. Blockly.bindEventWithChecks_(this.getSvgRoot(), 'mousedown', this,
  117. this.onMouseDown_);
  118. var thisBlock = this;
  119. Blockly.bindEvent_(this.getSvgRoot(), 'touchstart', null,
  120. function(e) {Blockly.longStart_(e, thisBlock);});
  121. }
  122. this.eventsInit_ = true;
  123. if (!this.getSvgRoot().parentNode) {
  124. this.workspace.getCanvas().appendChild(this.getSvgRoot());
  125. }
  126. };
  127. /**
  128. * Select this block. Highlight it visually.
  129. */
  130. Blockly.BlockSvg.prototype.select = function() {
  131. if (this.isShadow() && this.getParent()) {
  132. // Shadow blocks should not be selected.
  133. this.getParent().select();
  134. return;
  135. }
  136. if (Blockly.selected == this) {
  137. return;
  138. }
  139. var oldId = null;
  140. if (Blockly.selected) {
  141. oldId = Blockly.selected.id;
  142. // Unselect any previously selected block.
  143. Blockly.Events.disable();
  144. try {
  145. Blockly.selected.unselect();
  146. } finally {
  147. Blockly.Events.enable();
  148. }
  149. }
  150. var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id);
  151. event.workspaceId = this.workspace.id;
  152. Blockly.Events.fire(event);
  153. Blockly.selected = this;
  154. this.addSelect();
  155. };
  156. /**
  157. * Unselect this block. Remove its highlighting.
  158. */
  159. Blockly.BlockSvg.prototype.unselect = function() {
  160. if (Blockly.selected != this) {
  161. return;
  162. }
  163. var event = new Blockly.Events.Ui(null, 'selected', this.id, null);
  164. event.workspaceId = this.workspace.id;
  165. Blockly.Events.fire(event);
  166. Blockly.selected = null;
  167. this.removeSelect();
  168. };
  169. /**
  170. * Block's mutator icon (if any).
  171. * @type {Blockly.Mutator}
  172. */
  173. Blockly.BlockSvg.prototype.mutator = null;
  174. /**
  175. * Block's comment icon (if any).
  176. * @type {Blockly.Comment}
  177. */
  178. Blockly.BlockSvg.prototype.comment = null;
  179. /**
  180. * Block's warning icon (if any).
  181. * @type {Blockly.Warning}
  182. */
  183. Blockly.BlockSvg.prototype.warning = null;
  184. /**
  185. * Returns a list of mutator, comment, and warning icons.
  186. * @return {!Array} List of icons.
  187. */
  188. Blockly.BlockSvg.prototype.getIcons = function() {
  189. var icons = [];
  190. if (this.mutator) {
  191. icons.push(this.mutator);
  192. }
  193. if (this.comment) {
  194. icons.push(this.comment);
  195. }
  196. if (this.warning) {
  197. icons.push(this.warning);
  198. }
  199. return icons;
  200. };
  201. /**
  202. * Wrapper function called when a mouseUp occurs during a drag operation.
  203. * @type {Array.<!Array>}
  204. * @private
  205. */
  206. Blockly.BlockSvg.onMouseUpWrapper_ = null;
  207. /**
  208. * Wrapper function called when a mouseMove occurs during a drag operation.
  209. * @type {Array.<!Array>}
  210. * @private
  211. */
  212. Blockly.BlockSvg.onMouseMoveWrapper_ = null;
  213. /**
  214. * Stop binding to the global mouseup and mousemove events.
  215. * @package
  216. */
  217. Blockly.BlockSvg.terminateDrag = function() {
  218. Blockly.BlockSvg.disconnectUiStop_();
  219. if (Blockly.BlockSvg.onMouseUpWrapper_) {
  220. Blockly.unbindEvent_(Blockly.BlockSvg.onMouseUpWrapper_);
  221. Blockly.BlockSvg.onMouseUpWrapper_ = null;
  222. }
  223. if (Blockly.BlockSvg.onMouseMoveWrapper_) {
  224. Blockly.unbindEvent_(Blockly.BlockSvg.onMouseMoveWrapper_);
  225. Blockly.BlockSvg.onMouseMoveWrapper_ = null;
  226. }
  227. var selected = Blockly.selected;
  228. if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
  229. // Terminate a drag operation.
  230. if (selected) {
  231. // Update the connection locations.
  232. var xy = selected.getRelativeToSurfaceXY();
  233. var dxy = goog.math.Coordinate.difference(xy, selected.dragStartXY_);
  234. var event = new Blockly.Events.Move(selected);
  235. event.oldCoordinate = selected.dragStartXY_;
  236. event.recordNew();
  237. Blockly.Events.fire(event);
  238. selected.moveConnections_(dxy.x, dxy.y);
  239. delete selected.draggedBubbles_;
  240. selected.setDragging_(false);
  241. selected.render();
  242. selected.workspace.setResizesEnabled(true);
  243. // Ensure that any snap and bump are part of this move's event group.
  244. var group = Blockly.Events.getGroup();
  245. setTimeout(function() {
  246. Blockly.Events.setGroup(group);
  247. selected.snapToGrid();
  248. Blockly.Events.setGroup(false);
  249. }, Blockly.BUMP_DELAY / 2);
  250. setTimeout(function() {
  251. Blockly.Events.setGroup(group);
  252. selected.bumpNeighbours_();
  253. Blockly.Events.setGroup(false);
  254. }, Blockly.BUMP_DELAY);
  255. }
  256. }
  257. Blockly.dragMode_ = Blockly.DRAG_NONE;
  258. Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
  259. };
  260. /**
  261. * Set parent of this block to be a new block or null.
  262. * @param {Blockly.BlockSvg} newParent New parent block.
  263. */
  264. Blockly.BlockSvg.prototype.setParent = function(newParent) {
  265. if (newParent == this.parentBlock_) {
  266. return;
  267. }
  268. var svgRoot = this.getSvgRoot();
  269. if (this.parentBlock_ && svgRoot) {
  270. // Move this block up the DOM. Keep track of x/y translations.
  271. var xy = this.getRelativeToSurfaceXY();
  272. this.workspace.getCanvas().appendChild(svgRoot);
  273. svgRoot.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
  274. }
  275. Blockly.Field.startCache();
  276. Blockly.BlockSvg.superClass_.setParent.call(this, newParent);
  277. Blockly.Field.stopCache();
  278. if (newParent) {
  279. var oldXY = this.getRelativeToSurfaceXY();
  280. newParent.getSvgRoot().appendChild(svgRoot);
  281. var newXY = this.getRelativeToSurfaceXY();
  282. // Move the connections to match the child's new position.
  283. this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y);
  284. }
  285. };
  286. /**
  287. * Return the coordinates of the top-left corner of this block relative to the
  288. * drawing surface's origin (0,0).
  289. * @return {!goog.math.Coordinate} Object with .x and .y properties.
  290. */
  291. Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() {
  292. var x = 0;
  293. var y = 0;
  294. var element = this.getSvgRoot();
  295. if (element) {
  296. do {
  297. // Loop through this block and every parent.
  298. var xy = Blockly.getRelativeXY_(element);
  299. x += xy.x;
  300. y += xy.y;
  301. element = element.parentNode;
  302. } while (element && element != this.workspace.getCanvas());
  303. }
  304. return new goog.math.Coordinate(x, y);
  305. };
  306. /**
  307. * Move a block by a relative offset.
  308. * @param {number} dx Horizontal offset.
  309. * @param {number} dy Vertical offset.
  310. */
  311. Blockly.BlockSvg.prototype.moveBy = function(dx, dy) {
  312. goog.asserts.assert(!this.parentBlock_, 'Block has parent.');
  313. var event = new Blockly.Events.Move(this);
  314. var xy = this.getRelativeToSurfaceXY();
  315. this.getSvgRoot().setAttribute('transform',
  316. 'translate(' + (xy.x + dx) + ',' + (xy.y + dy) + ')');
  317. this.moveConnections_(dx, dy);
  318. event.recordNew();
  319. this.workspace.resizeContents();
  320. Blockly.Events.fire(event);
  321. };
  322. /**
  323. * Snap this block to the nearest grid point.
  324. */
  325. Blockly.BlockSvg.prototype.snapToGrid = function() {
  326. if (!this.workspace) {
  327. return; // Deleted block.
  328. }
  329. if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
  330. return; // Don't bump blocks during a drag.
  331. }
  332. if (this.getParent()) {
  333. return; // Only snap top-level blocks.
  334. }
  335. if (this.isInFlyout) {
  336. return; // Don't move blocks around in a flyout.
  337. }
  338. if (!this.workspace.options.gridOptions ||
  339. !this.workspace.options.gridOptions['snap']) {
  340. return; // Config says no snapping.
  341. }
  342. var spacing = this.workspace.options.gridOptions['spacing'];
  343. var half = spacing / 2;
  344. var xy = this.getRelativeToSurfaceXY();
  345. var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x;
  346. var dy = Math.round((xy.y - half) / spacing) * spacing + half - xy.y;
  347. dx = Math.round(dx);
  348. dy = Math.round(dy);
  349. if (dx != 0 || dy != 0) {
  350. this.moveBy(dx, dy);
  351. }
  352. };
  353. /**
  354. * Returns a bounding box describing the dimensions of this block
  355. * and any blocks stacked below it.
  356. * @return {!{height: number, width: number}} Object with height and width
  357. * properties.
  358. */
  359. Blockly.BlockSvg.prototype.getHeightWidth = function() {
  360. var height = this.height;
  361. var width = this.width;
  362. // Recursively add size of subsequent blocks.
  363. var nextBlock = this.getNextBlock();
  364. if (nextBlock) {
  365. var nextHeightWidth = nextBlock.getHeightWidth();
  366. height += nextHeightWidth.height - 4; // Height of tab.
  367. width = Math.max(width, nextHeightWidth.width);
  368. } else if (!this.nextConnection && !this.outputConnection) {
  369. // Add a bit of margin under blocks with no bottom tab.
  370. height += 2;
  371. }
  372. return {height: height, width: width};
  373. };
  374. /**
  375. * Returns the coordinates of a bounding box describing the dimensions of this
  376. * block and any blocks stacked below it.
  377. * @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
  378. * Object with top left and bottom right coordinates of the bounding box.
  379. */
  380. Blockly.BlockSvg.prototype.getBoundingRectangle = function() {
  381. var blockXY = this.getRelativeToSurfaceXY(this);
  382. var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
  383. var blockBounds = this.getHeightWidth();
  384. var topLeft;
  385. var bottomRight;
  386. if (this.RTL) {
  387. // Width has the tab built into it already so subtract it here.
  388. topLeft = new goog.math.Coordinate(blockXY.x - (blockBounds.width - tab),
  389. blockXY.y);
  390. // Add the width of the tab/puzzle piece knob to the x coordinate
  391. // since X is the corner of the rectangle, not the whole puzzle piece.
  392. bottomRight = new goog.math.Coordinate(blockXY.x + tab,
  393. blockXY.y + blockBounds.height);
  394. } else {
  395. // Subtract the width of the tab/puzzle piece knob to the x coordinate
  396. // since X is the corner of the rectangle, not the whole puzzle piece.
  397. topLeft = new goog.math.Coordinate(blockXY.x - tab, blockXY.y);
  398. // Width has the tab built into it already so subtract it here.
  399. bottomRight = new goog.math.Coordinate(blockXY.x + blockBounds.width - tab,
  400. blockXY.y + blockBounds.height);
  401. }
  402. return {topLeft: topLeft, bottomRight: bottomRight};
  403. };
  404. /**
  405. * Set whether the block is collapsed or not.
  406. * @param {boolean} collapsed True if collapsed.
  407. */
  408. Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) {
  409. if (this.collapsed_ == collapsed) {
  410. return;
  411. }
  412. var renderList = [];
  413. // Show/hide the inputs.
  414. for (var i = 0, input; input = this.inputList[i]; i++) {
  415. renderList.push.apply(renderList, input.setVisible(!collapsed));
  416. }
  417. var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
  418. if (collapsed) {
  419. var icons = this.getIcons();
  420. for (var i = 0; i < icons.length; i++) {
  421. icons[i].setVisible(false);
  422. }
  423. var text = this.toString(Blockly.COLLAPSE_CHARS);
  424. this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init();
  425. } else {
  426. this.removeInput(COLLAPSED_INPUT_NAME);
  427. // Clear any warnings inherited from enclosed blocks.
  428. this.setWarningText(null);
  429. }
  430. Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed);
  431. if (!renderList.length) {
  432. // No child blocks, just render this block.
  433. renderList[0] = this;
  434. }
  435. if (this.rendered) {
  436. for (var i = 0, block; block = renderList[i]; i++) {
  437. block.render();
  438. }
  439. // Don't bump neighbours.
  440. // Although bumping neighbours would make sense, users often collapse
  441. // all their functions and store them next to each other. Expanding and
  442. // bumping causes all their definitions to go out of alignment.
  443. }
  444. };
  445. /**
  446. * Open the next (or previous) FieldTextInput.
  447. * @param {Blockly.Field|Blockly.Block} start Current location.
  448. * @param {boolean} forward If true go forward, otherwise backward.
  449. */
  450. Blockly.BlockSvg.prototype.tab = function(start, forward) {
  451. // This function need not be efficient since it runs once on a keypress.
  452. // Create an ordered list of all text fields and connected inputs.
  453. var list = [];
  454. for (var i = 0, input; input = this.inputList[i]; i++) {
  455. for (var j = 0, field; field = input.fieldRow[j]; j++) {
  456. if (field instanceof Blockly.FieldTextInput) {
  457. // TODO: Also support dropdown fields.
  458. list.push(field);
  459. }
  460. }
  461. if (input.connection) {
  462. var block = input.connection.targetBlock();
  463. if (block) {
  464. list.push(block);
  465. }
  466. }
  467. }
  468. var i = list.indexOf(start);
  469. if (i == -1) {
  470. // No start location, start at the beginning or end.
  471. i = forward ? -1 : list.length;
  472. }
  473. var target = list[forward ? i + 1 : i - 1];
  474. if (!target) {
  475. // Ran off of list.
  476. var parent = this.getParent();
  477. if (parent) {
  478. parent.tab(this, forward);
  479. }
  480. } else if (target instanceof Blockly.Field) {
  481. target.showEditor_();
  482. } else {
  483. target.tab(null, forward);
  484. }
  485. };
  486. /**
  487. * Handle a mouse-down on an SVG block.
  488. * @param {!Event} e Mouse down event or touch start event.
  489. * @private
  490. */
  491. Blockly.BlockSvg.prototype.onMouseDown_ = function(e) {
  492. if (this.workspace.options.readOnly) {
  493. return;
  494. }
  495. if (this.isInFlyout) {
  496. // longStart's simulation of right-clicks for longpresses on touch devices
  497. // calls the onMouseDown_ function defined on the prototype of the object
  498. // the was longpressed (in this case, a Blockly.BlockSvg). In this case
  499. // that behaviour is wrong, because Blockly.Flyout.prototype.blockMouseDown
  500. // should be called for a mousedown on a block in the flyout, which blocks
  501. // execution of the block's onMouseDown_ function.
  502. if (e.type == 'touchstart' && Blockly.isRightButton(e)) {
  503. Blockly.Flyout.blockRightClick_(e, this);
  504. e.stopPropagation();
  505. e.preventDefault();
  506. }
  507. return;
  508. }
  509. if (this.isInMutator) {
  510. // Mutator's coordinate system could be out of date because the bubble was
  511. // dragged, the block was moved, the parent workspace zoomed, etc.
  512. this.workspace.resize();
  513. }
  514. this.workspace.updateScreenCalculationsIfScrolled();
  515. this.workspace.markFocused();
  516. Blockly.terminateDrag_();
  517. this.select();
  518. Blockly.hideChaff();
  519. if (Blockly.isRightButton(e)) {
  520. // Right-click.
  521. this.showContextMenu_(e);
  522. // Click, not drag, so stop waiting for other touches from this identifier.
  523. Blockly.Touch.clearTouchIdentifier();
  524. } else if (!this.isMovable()) {
  525. // Allow immovable blocks to be selected and context menued, but not
  526. // dragged. Let this event bubble up to document, so the workspace may be
  527. // dragged instead.
  528. return;
  529. } else {
  530. if (!Blockly.Events.getGroup()) {
  531. Blockly.Events.setGroup(true);
  532. }
  533. // Left-click (or middle click)
  534. Blockly.Css.setCursor(Blockly.Css.Cursor.CLOSED);
  535. this.dragStartXY_ = this.getRelativeToSurfaceXY();
  536. this.workspace.startDrag(e, this.dragStartXY_);
  537. Blockly.dragMode_ = Blockly.DRAG_STICKY;
  538. Blockly.BlockSvg.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(document,
  539. 'mouseup', this, this.onMouseUp_);
  540. Blockly.BlockSvg.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(
  541. document, 'mousemove', this, this.onMouseMove_);
  542. // Build a list of bubbles that need to be moved and where they started.
  543. this.draggedBubbles_ = [];
  544. var descendants = this.getDescendants();
  545. for (var i = 0, descendant; descendant = descendants[i]; i++) {
  546. var icons = descendant.getIcons();
  547. for (var j = 0; j < icons.length; j++) {
  548. var data = icons[j].getIconLocation();
  549. data.bubble = icons[j];
  550. this.draggedBubbles_.push(data);
  551. }
  552. }
  553. }
  554. // This event has been handled. No need to bubble up to the document.
  555. e.stopPropagation();
  556. e.preventDefault();
  557. };
  558. /**
  559. * Handle a mouse-up anywhere in the SVG pane. Is only registered when a
  560. * block is clicked. We can't use mouseUp on the block since a fast-moving
  561. * cursor can briefly escape the block before it catches up.
  562. * @param {!Event} e Mouse up event.
  563. * @private
  564. */
  565. Blockly.BlockSvg.prototype.onMouseUp_ = function(e) {
  566. Blockly.Touch.clearTouchIdentifier();
  567. if (Blockly.dragMode_ != Blockly.DRAG_FREE &&
  568. !Blockly.WidgetDiv.isVisible()) {
  569. Blockly.Events.fire(
  570. new Blockly.Events.Ui(this, 'click', undefined, undefined));
  571. }
  572. Blockly.terminateDrag_();
  573. if (Blockly.selected && Blockly.highlightedConnection_) {
  574. // Connect two blocks together.
  575. Blockly.localConnection_.connect(Blockly.highlightedConnection_);
  576. if (this.rendered) {
  577. // Trigger a connection animation.
  578. // Determine which connection is inferior (lower in the source stack).
  579. var inferiorConnection = Blockly.localConnection_.isSuperior() ?
  580. Blockly.highlightedConnection_ : Blockly.localConnection_;
  581. inferiorConnection.getSourceBlock().connectionUiEffect();
  582. }
  583. if (this.workspace.trashcan) {
  584. // Don't throw an object in the trash can if it just got connected.
  585. this.workspace.trashcan.close();
  586. }
  587. } else if (!this.getParent() && Blockly.selected.isDeletable() &&
  588. this.workspace.isDeleteArea(e)) {
  589. var trashcan = this.workspace.trashcan;
  590. if (trashcan) {
  591. goog.Timer.callOnce(trashcan.close, 100, trashcan);
  592. }
  593. Blockly.selected.dispose(false, true);
  594. }
  595. if (Blockly.highlightedConnection_) {
  596. Blockly.highlightedConnection_.unhighlight();
  597. Blockly.highlightedConnection_ = null;
  598. }
  599. Blockly.Css.setCursor(Blockly.Css.Cursor.OPEN);
  600. if (!Blockly.WidgetDiv.isVisible()) {
  601. Blockly.Events.setGroup(false);
  602. }
  603. };
  604. /**
  605. * Load the block's help page in a new window.
  606. * @private
  607. */
  608. Blockly.BlockSvg.prototype.showHelp_ = function() {
  609. var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl;
  610. if (url) {
  611. window.open(url);
  612. }
  613. };
  614. /**
  615. * Show the context menu for this block.
  616. * @param {!Event} e Mouse event.
  617. * @private
  618. */
  619. Blockly.BlockSvg.prototype.showContextMenu_ = function(e) {
  620. if (this.workspace.options.readOnly || !this.contextMenu) {
  621. return;
  622. }
  623. // Save the current block in a variable for use in closures.
  624. var block = this;
  625. var menuOptions = [];
  626. if (this.isDeletable() && this.isMovable() && !block.isInFlyout) {
  627. // Option to duplicate this block.
  628. var duplicateOption = {
  629. text: Blockly.Msg.DUPLICATE_BLOCK,
  630. enabled: true,
  631. callback: function() {
  632. Blockly.duplicate_(block);
  633. }
  634. };
  635. if (this.getDescendants().length > this.workspace.remainingCapacity()) {
  636. duplicateOption.enabled = false;
  637. }
  638. menuOptions.push(duplicateOption);
  639. if (this.isEditable() && !this.collapsed_ &&
  640. this.workspace.options.comments) {
  641. // Option to add/remove a comment.
  642. var commentOption = {enabled: !goog.userAgent.IE};
  643. if (this.comment) {
  644. commentOption.text = Blockly.Msg.REMOVE_COMMENT;
  645. commentOption.callback = function() {
  646. block.setCommentText(null);
  647. };
  648. } else {
  649. commentOption.text = Blockly.Msg.ADD_COMMENT;
  650. commentOption.callback = function() {
  651. block.setCommentText('');
  652. };
  653. }
  654. menuOptions.push(commentOption);
  655. }
  656. // Option to make block inline.
  657. if (!this.collapsed_) {
  658. for (var i = 1; i < this.inputList.length; i++) {
  659. if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT &&
  660. this.inputList[i].type != Blockly.NEXT_STATEMENT) {
  661. // Only display this option if there are two value or dummy inputs
  662. // next to each other.
  663. var inlineOption = {enabled: true};
  664. var isInline = this.getInputsInline();
  665. inlineOption.text = isInline ?
  666. Blockly.Msg.EXTERNAL_INPUTS : Blockly.Msg.INLINE_INPUTS;
  667. inlineOption.callback = function() {
  668. block.setInputsInline(!isInline);
  669. };
  670. menuOptions.push(inlineOption);
  671. break;
  672. }
  673. }
  674. }
  675. if (this.workspace.options.collapse) {
  676. // Option to collapse/expand block.
  677. if (this.collapsed_) {
  678. var expandOption = {enabled: true};
  679. expandOption.text = Blockly.Msg.EXPAND_BLOCK;
  680. expandOption.callback = function() {
  681. block.setCollapsed(false);
  682. };
  683. menuOptions.push(expandOption);
  684. } else {
  685. var collapseOption = {enabled: true};
  686. collapseOption.text = Blockly.Msg.COLLAPSE_BLOCK;
  687. collapseOption.callback = function() {
  688. block.setCollapsed(true);
  689. };
  690. menuOptions.push(collapseOption);
  691. }
  692. }
  693. if (this.workspace.options.disable) {
  694. // Option to disable/enable block.
  695. var disableOption = {
  696. text: this.disabled ?
  697. Blockly.Msg.ENABLE_BLOCK : Blockly.Msg.DISABLE_BLOCK,
  698. enabled: !this.getInheritedDisabled(),
  699. callback: function() {
  700. block.setDisabled(!block.disabled);
  701. }
  702. };
  703. menuOptions.push(disableOption);
  704. }
  705. // Option to delete this block.
  706. // Count the number of blocks that are nested in this block.
  707. var descendantCount = this.getDescendants().length;
  708. var nextBlock = this.getNextBlock();
  709. if (nextBlock) {
  710. // Blocks in the current stack would survive this block's deletion.
  711. descendantCount -= nextBlock.getDescendants().length;
  712. }
  713. var deleteOption = {
  714. text: descendantCount == 1 ? Blockly.Msg.DELETE_BLOCK :
  715. Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(descendantCount)),
  716. enabled: true,
  717. callback: function() {
  718. Blockly.Events.setGroup(true);
  719. block.dispose(true, true);
  720. Blockly.Events.setGroup(false);
  721. }
  722. };
  723. menuOptions.push(deleteOption);
  724. }
  725. // Option to get help.
  726. var url = goog.isFunction(this.helpUrl) ? this.helpUrl() : this.helpUrl;
  727. var helpOption = {enabled: !!url};
  728. helpOption.text = Blockly.Msg.HELP;
  729. helpOption.callback = function() {
  730. block.showHelp_();
  731. };
  732. menuOptions.push(helpOption);
  733. // Allow the block to add or modify menuOptions.
  734. if (this.customContextMenu && !block.isInFlyout) {
  735. this.customContextMenu(menuOptions);
  736. }
  737. Blockly.ContextMenu.show(e, menuOptions, this.RTL);
  738. Blockly.ContextMenu.currentBlock = this;
  739. };
  740. /**
  741. * Move the connections for this block and all blocks attached under it.
  742. * Also update any attached bubbles.
  743. * @param {number} dx Horizontal offset from current location.
  744. * @param {number} dy Vertical offset from current location.
  745. * @private
  746. */
  747. Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) {
  748. if (!this.rendered) {
  749. // Rendering is required to lay out the blocks.
  750. // This is probably an invisible block attached to a collapsed block.
  751. return;
  752. }
  753. var myConnections = this.getConnections_(false);
  754. for (var i = 0; i < myConnections.length; i++) {
  755. myConnections[i].moveBy(dx, dy);
  756. }
  757. var icons = this.getIcons();
  758. for (var i = 0; i < icons.length; i++) {
  759. icons[i].computeIconLocation();
  760. }
  761. // Recurse through all blocks attached under this one.
  762. for (var i = 0; i < this.childBlocks_.length; i++) {
  763. this.childBlocks_[i].moveConnections_(dx, dy);
  764. }
  765. };
  766. /**
  767. * Recursively adds or removes the dragging class to this node and its children.
  768. * @param {boolean} adding True if adding, false if removing.
  769. * @private
  770. */
  771. Blockly.BlockSvg.prototype.setDragging_ = function(adding) {
  772. if (adding) {
  773. var group = this.getSvgRoot();
  774. group.translate_ = '';
  775. group.skew_ = '';
  776. Blockly.draggingConnections_ =
  777. Blockly.draggingConnections_.concat(this.getConnections_(true));
  778. Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
  779. 'blocklyDragging');
  780. } else {
  781. Blockly.draggingConnections_ = [];
  782. Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
  783. 'blocklyDragging');
  784. }
  785. // Recurse through all blocks attached under this one.
  786. for (var i = 0; i < this.childBlocks_.length; i++) {
  787. this.childBlocks_[i].setDragging_(adding);
  788. }
  789. };
  790. /**
  791. * Drag this block to follow the mouse.
  792. * @param {!Event} e Mouse move event.
  793. * @private
  794. */
  795. Blockly.BlockSvg.prototype.onMouseMove_ = function(e) {
  796. if (e.type == 'mousemove' && e.clientX <= 1 && e.clientY == 0 &&
  797. e.button == 0) {
  798. /* HACK:
  799. Safari Mobile 6.0 and Chrome for Android 18.0 fire rogue mousemove
  800. events on certain touch actions. Ignore events with these signatures.
  801. This may result in a one-pixel blind spot in other browsers,
  802. but this shouldn't be noticeable. */
  803. e.stopPropagation();
  804. return;
  805. }
  806. var oldXY = this.getRelativeToSurfaceXY();
  807. var newXY = this.workspace.moveDrag(e);
  808. if (Blockly.dragMode_ == Blockly.DRAG_STICKY) {
  809. // Still dragging within the sticky DRAG_RADIUS.
  810. var dr = goog.math.Coordinate.distance(oldXY, newXY) * this.workspace.scale;
  811. if (dr > Blockly.DRAG_RADIUS) {
  812. // Switch to unrestricted dragging.
  813. Blockly.dragMode_ = Blockly.DRAG_FREE;
  814. Blockly.longStop_();
  815. this.workspace.setResizesEnabled(false);
  816. if (this.parentBlock_) {
  817. // Push this block to the very top of the stack.
  818. this.unplug();
  819. var group = this.getSvgRoot();
  820. group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')';
  821. this.disconnectUiEffect();
  822. }
  823. this.setDragging_(true);
  824. }
  825. }
  826. if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
  827. // Unrestricted dragging.
  828. var dxy = goog.math.Coordinate.difference(oldXY, this.dragStartXY_);
  829. var group = this.getSvgRoot();
  830. group.translate_ = 'translate(' + newXY.x + ',' + newXY.y + ')';
  831. group.setAttribute('transform', group.translate_ + group.skew_);
  832. // Drag all the nested bubbles.
  833. for (var i = 0; i < this.draggedBubbles_.length; i++) {
  834. var commentData = this.draggedBubbles_[i];
  835. commentData.bubble.setIconLocation(
  836. goog.math.Coordinate.sum(commentData, dxy));
  837. }
  838. // Check to see if any of this block's connections are within range of
  839. // another block's connection.
  840. var myConnections = this.getConnections_(false);
  841. // Also check the last connection on this stack
  842. var lastOnStack = this.lastConnectionInStack_();
  843. if (lastOnStack && lastOnStack != this.nextConnection) {
  844. myConnections.push(lastOnStack);
  845. }
  846. var closestConnection = null;
  847. var localConnection = null;
  848. var radiusConnection = Blockly.SNAP_RADIUS;
  849. for (var i = 0; i < myConnections.length; i++) {
  850. var myConnection = myConnections[i];
  851. var neighbour = myConnection.closest(radiusConnection, dxy);
  852. if (neighbour.connection) {
  853. closestConnection = neighbour.connection;
  854. localConnection = myConnection;
  855. radiusConnection = neighbour.radius;
  856. }
  857. }
  858. // Remove connection highlighting if needed.
  859. if (Blockly.highlightedConnection_ &&
  860. Blockly.highlightedConnection_ != closestConnection) {
  861. Blockly.highlightedConnection_.unhighlight();
  862. Blockly.highlightedConnection_ = null;
  863. Blockly.localConnection_ = null;
  864. }
  865. // Add connection highlighting if needed.
  866. if (closestConnection &&
  867. closestConnection != Blockly.highlightedConnection_) {
  868. closestConnection.highlight();
  869. Blockly.highlightedConnection_ = closestConnection;
  870. Blockly.localConnection_ = localConnection;
  871. }
  872. // Provide visual indication of whether the block will be deleted if
  873. // dropped here.
  874. if (this.isDeletable()) {
  875. this.workspace.isDeleteArea(e);
  876. }
  877. }
  878. // This event has been handled. No need to bubble up to the document.
  879. e.stopPropagation();
  880. e.preventDefault();
  881. };
  882. /**
  883. * Add or remove the UI indicating if this block is movable or not.
  884. */
  885. Blockly.BlockSvg.prototype.updateMovable = function() {
  886. if (this.isMovable()) {
  887. Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
  888. 'blocklyDraggable');
  889. } else {
  890. Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
  891. 'blocklyDraggable');
  892. }
  893. };
  894. /**
  895. * Set whether this block is movable or not.
  896. * @param {boolean} movable True if movable.
  897. */
  898. Blockly.BlockSvg.prototype.setMovable = function(movable) {
  899. Blockly.BlockSvg.superClass_.setMovable.call(this, movable);
  900. this.updateMovable();
  901. };
  902. /**
  903. * Set whether this block is editable or not.
  904. * @param {boolean} editable True if editable.
  905. */
  906. Blockly.BlockSvg.prototype.setEditable = function(editable) {
  907. Blockly.BlockSvg.superClass_.setEditable.call(this, editable);
  908. var icons = this.getIcons();
  909. for (var i = 0; i < icons.length; i++) {
  910. icons[i].updateEditable();
  911. }
  912. };
  913. /**
  914. * Set whether this block is a shadow block or not.
  915. * @param {boolean} shadow True if a shadow.
  916. */
  917. Blockly.BlockSvg.prototype.setShadow = function(shadow) {
  918. Blockly.BlockSvg.superClass_.setShadow.call(this, shadow);
  919. this.updateColour();
  920. };
  921. /**
  922. * Return the root node of the SVG or null if none exists.
  923. * @return {Element} The root SVG node (probably a group).
  924. */
  925. Blockly.BlockSvg.prototype.getSvgRoot = function() {
  926. return this.svgGroup_;
  927. };
  928. /**
  929. * Dispose of this block.
  930. * @param {boolean} healStack If true, then try to heal any gap by connecting
  931. * the next statement with the previous statement. Otherwise, dispose of
  932. * all children of this block.
  933. * @param {boolean} animate If true, show a disposal animation and sound.
  934. */
  935. Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
  936. if (!this.workspace) {
  937. // The block has already been deleted.
  938. return;
  939. }
  940. Blockly.Tooltip.hide();
  941. Blockly.Field.startCache();
  942. // Save the block's workspace temporarily so we can resize the
  943. // contents once the block is disposed.
  944. var blockWorkspace = this.workspace;
  945. // If this block is being dragged, unlink the mouse events.
  946. if (Blockly.selected == this) {
  947. this.unselect();
  948. Blockly.terminateDrag_();
  949. }
  950. // If this block has a context menu open, close it.
  951. if (Blockly.ContextMenu.currentBlock == this) {
  952. Blockly.ContextMenu.hide();
  953. }
  954. if (animate && this.rendered) {
  955. this.unplug(healStack);
  956. this.disposeUiEffect();
  957. }
  958. // Stop rerendering.
  959. this.rendered = false;
  960. Blockly.Events.disable();
  961. try {
  962. var icons = this.getIcons();
  963. for (var i = 0; i < icons.length; i++) {
  964. icons[i].dispose();
  965. }
  966. } finally {
  967. Blockly.Events.enable();
  968. }
  969. Blockly.BlockSvg.superClass_.dispose.call(this, healStack);
  970. goog.dom.removeNode(this.svgGroup_);
  971. blockWorkspace.resizeContents();
  972. // Sever JavaScript to DOM connections.
  973. this.svgGroup_ = null;
  974. this.svgPath_ = null;
  975. this.svgPathLight_ = null;
  976. this.svgPathDark_ = null;
  977. Blockly.Field.stopCache();
  978. };
  979. /**
  980. * Play some UI effects (sound, animation) when disposing of a block.
  981. */
  982. Blockly.BlockSvg.prototype.disposeUiEffect = function() {
  983. this.workspace.playAudio('delete');
  984. var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_),
  985. this.workspace);
  986. // Deeply clone the current block.
  987. var clone = this.svgGroup_.cloneNode(true);
  988. clone.translateX_ = xy.x;
  989. clone.translateY_ = xy.y;
  990. clone.setAttribute('transform',
  991. 'translate(' + clone.translateX_ + ',' + clone.translateY_ + ')');
  992. this.workspace.getParentSvg().appendChild(clone);
  993. clone.bBox_ = clone.getBBox();
  994. // Start the animation.
  995. Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date,
  996. this.workspace.scale);
  997. };
  998. /**
  999. * Animate a cloned block and eventually dispose of it.
  1000. * This is a class method, not an instace method since the original block has
  1001. * been destroyed and is no longer accessible.
  1002. * @param {!Element} clone SVG element to animate and dispose of.
  1003. * @param {boolean} rtl True if RTL, false if LTR.
  1004. * @param {!Date} start Date of animation's start.
  1005. * @param {number} workspaceScale Scale of workspace.
  1006. * @private
  1007. */
  1008. Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) {
  1009. var ms = new Date - start;
  1010. var percent = ms / 150;
  1011. if (percent > 1) {
  1012. goog.dom.removeNode(clone);
  1013. } else {
  1014. var x = clone.translateX_ +
  1015. (rtl ? -1 : 1) * clone.bBox_.width * workspaceScale / 2 * percent;
  1016. var y = clone.translateY_ + clone.bBox_.height * workspaceScale * percent;
  1017. var scale = (1 - percent) * workspaceScale;
  1018. clone.setAttribute('transform', 'translate(' + x + ',' + y + ')' +
  1019. ' scale(' + scale + ')');
  1020. var closure = function() {
  1021. Blockly.BlockSvg.disposeUiStep_(clone, rtl, start, workspaceScale);
  1022. };
  1023. setTimeout(closure, 10);
  1024. }
  1025. };
  1026. /**
  1027. * Play some UI effects (sound, ripple) after a connection has been established.
  1028. */
  1029. Blockly.BlockSvg.prototype.connectionUiEffect = function() {
  1030. this.workspace.playAudio('click');
  1031. if (this.workspace.scale < 1) {
  1032. return; // Too small to care about visual effects.
  1033. }
  1034. // Determine the absolute coordinates of the inferior block.
  1035. var xy = Blockly.getSvgXY_(/** @type {!Element} */ (this.svgGroup_),
  1036. this.workspace);
  1037. // Offset the coordinates based on the two connection types, fix scale.
  1038. if (this.outputConnection) {
  1039. xy.x += (this.RTL ? 3 : -3) * this.workspace.scale;
  1040. xy.y += 13 * this.workspace.scale;
  1041. } else if (this.previousConnection) {
  1042. xy.x += (this.RTL ? -23 : 23) * this.workspace.scale;
  1043. xy.y += 3 * this.workspace.scale;
  1044. }
  1045. var ripple = Blockly.createSvgElement('circle',
  1046. {'cx': xy.x, 'cy': xy.y, 'r': 0, 'fill': 'none',
  1047. 'stroke': '#888', 'stroke-width': 10},
  1048. this.workspace.getParentSvg());
  1049. // Start the animation.
  1050. Blockly.BlockSvg.connectionUiStep_(ripple, new Date, this.workspace.scale);
  1051. };
  1052. /**
  1053. * Expand a ripple around a connection.
  1054. * @param {!Element} ripple Element to animate.
  1055. * @param {!Date} start Date of animation's start.
  1056. * @param {number} workspaceScale Scale of workspace.
  1057. * @private
  1058. */
  1059. Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) {
  1060. var ms = new Date - start;
  1061. var percent = ms / 150;
  1062. if (percent > 1) {
  1063. goog.dom.removeNode(ripple);
  1064. } else {
  1065. ripple.setAttribute('r', percent * 25 * workspaceScale);
  1066. ripple.style.opacity = 1 - percent;
  1067. var closure = function() {
  1068. Blockly.BlockSvg.connectionUiStep_(ripple, start, workspaceScale);
  1069. };
  1070. Blockly.BlockSvg.disconnectUiStop_.pid_ = setTimeout(closure, 10);
  1071. }
  1072. };
  1073. /**
  1074. * Play some UI effects (sound, animation) when disconnecting a block.
  1075. */
  1076. Blockly.BlockSvg.prototype.disconnectUiEffect = function() {
  1077. this.workspace.playAudio('disconnect');
  1078. if (this.workspace.scale < 1) {
  1079. return; // Too small to care about visual effects.
  1080. }
  1081. // Horizontal distance for bottom of block to wiggle.
  1082. var DISPLACEMENT = 10;
  1083. // Scale magnitude of skew to height of block.
  1084. var height = this.getHeightWidth().height;
  1085. var magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180;
  1086. if (!this.RTL) {
  1087. magnitude *= -1;
  1088. }
  1089. // Start the animation.
  1090. Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date);
  1091. };
  1092. /**
  1093. * Animate a brief wiggle of a disconnected block.
  1094. * @param {!Element} group SVG element to animate.
  1095. * @param {number} magnitude Maximum degrees skew (reversed for RTL).
  1096. * @param {!Date} start Date of animation's start.
  1097. * @private
  1098. */
  1099. Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) {
  1100. var DURATION = 200; // Milliseconds.
  1101. var WIGGLES = 3; // Half oscillations.
  1102. var ms = new Date - start;
  1103. var percent = ms / DURATION;
  1104. if (percent > 1) {
  1105. group.skew_ = '';
  1106. } else {
  1107. var skew = Math.round(Math.sin(percent * Math.PI * WIGGLES) *
  1108. (1 - percent) * magnitude);
  1109. group.skew_ = 'skewX(' + skew + ')';
  1110. var closure = function() {
  1111. Blockly.BlockSvg.disconnectUiStep_(group, magnitude, start);
  1112. };
  1113. Blockly.BlockSvg.disconnectUiStop_.group = group;
  1114. Blockly.BlockSvg.disconnectUiStop_.pid = setTimeout(closure, 10);
  1115. }
  1116. group.setAttribute('transform', group.translate_ + group.skew_);
  1117. };
  1118. /**
  1119. * Stop the disconnect UI animation immediately.
  1120. * @private
  1121. */
  1122. Blockly.BlockSvg.disconnectUiStop_ = function() {
  1123. if (Blockly.BlockSvg.disconnectUiStop_.group) {
  1124. clearTimeout(Blockly.BlockSvg.disconnectUiStop_.pid);
  1125. var group = Blockly.BlockSvg.disconnectUiStop_.group;
  1126. group.skew_ = '';
  1127. group.setAttribute('transform', group.translate_);
  1128. Blockly.BlockSvg.disconnectUiStop_.group = null;
  1129. }
  1130. };
  1131. /**
  1132. * PID of disconnect UI animation. There can only be one at a time.
  1133. * @type {number}
  1134. */
  1135. Blockly.BlockSvg.disconnectUiStop_.pid = 0;
  1136. /**
  1137. * SVG group of wobbling block. There can only be one at a time.
  1138. * @type {Element}
  1139. */
  1140. Blockly.BlockSvg.disconnectUiStop_.group = null;
  1141. /**
  1142. * Change the colour of a block.
  1143. */
  1144. Blockly.BlockSvg.prototype.updateColour = function() {
  1145. if (this.disabled) {
  1146. // Disabled blocks don't have colour.
  1147. return;
  1148. }
  1149. var hexColour = this.getColour();
  1150. var rgb = goog.color.hexToRgb(hexColour);
  1151. if (this.isShadow()) {
  1152. rgb = goog.color.lighten(rgb, 0.6);
  1153. hexColour = goog.color.rgbArrayToHex(rgb);
  1154. this.svgPathLight_.style.display = 'none';
  1155. this.svgPathDark_.setAttribute('fill', hexColour);
  1156. } else {
  1157. this.svgPathLight_.style.display = '';
  1158. var hexLight = goog.color.rgbArrayToHex(goog.color.lighten(rgb, 0.3));
  1159. var hexDark = goog.color.rgbArrayToHex(goog.color.darken(rgb, 0.2));
  1160. this.svgPathLight_.setAttribute('stroke', hexLight);
  1161. this.svgPathDark_.setAttribute('fill', hexDark);
  1162. }
  1163. this.svgPath_.setAttribute('fill', hexColour);
  1164. var icons = this.getIcons();
  1165. for (var i = 0; i < icons.length; i++) {
  1166. icons[i].updateColour();
  1167. }
  1168. // Bump every dropdown to change its colour.
  1169. for (var x = 0, input; input = this.inputList[x]; x++) {
  1170. for (var y = 0, field; field = input.fieldRow[y]; y++) {
  1171. field.setText(null);
  1172. }
  1173. }
  1174. };
  1175. /**
  1176. * Enable or disable a block.
  1177. */
  1178. Blockly.BlockSvg.prototype.updateDisabled = function() {
  1179. var hasClass = Blockly.hasClass_(/** @type {!Element} */ (this.svgGroup_),
  1180. 'blocklyDisabled');
  1181. if (this.disabled || this.getInheritedDisabled()) {
  1182. if (!hasClass) {
  1183. Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
  1184. 'blocklyDisabled');
  1185. this.svgPath_.setAttribute('fill',
  1186. 'url(#' + this.workspace.options.disabledPatternId + ')');
  1187. }
  1188. } else {
  1189. if (hasClass) {
  1190. Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
  1191. 'blocklyDisabled');
  1192. this.updateColour();
  1193. }
  1194. }
  1195. var children = this.getChildren();
  1196. for (var i = 0, child; child = children[i]; i++) {
  1197. child.updateDisabled();
  1198. }
  1199. };
  1200. /**
  1201. * Returns the comment on this block (or '' if none).
  1202. * @return {string} Block's comment.
  1203. */
  1204. Blockly.BlockSvg.prototype.getCommentText = function() {
  1205. if (this.comment) {
  1206. var comment = this.comment.getText();
  1207. // Trim off trailing whitespace.
  1208. return comment.replace(/\s+$/, '').replace(/ +\n/g, '\n');
  1209. }
  1210. return '';
  1211. };
  1212. /**
  1213. * Set this block's comment text.
  1214. * @param {?string} text The text, or null to delete.
  1215. */
  1216. Blockly.BlockSvg.prototype.setCommentText = function(text) {
  1217. var changedState = false;
  1218. if (goog.isString(text)) {
  1219. if (!this.comment) {
  1220. this.comment = new Blockly.Comment(this);
  1221. changedState = true;
  1222. }
  1223. this.comment.setText(/** @type {string} */ (text));
  1224. } else {
  1225. if (this.comment) {
  1226. this.comment.dispose();
  1227. changedState = true;
  1228. }
  1229. }
  1230. if (changedState && this.rendered) {
  1231. this.render();
  1232. // Adding or removing a comment icon will cause the block to change shape.
  1233. this.bumpNeighbours_();
  1234. }
  1235. };
  1236. /**
  1237. * Set this block's warning text.
  1238. * @param {?string} text The text, or null to delete.
  1239. * @param {string=} opt_id An optional ID for the warning text to be able to
  1240. * maintain multiple warnings.
  1241. */
  1242. Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) {
  1243. if (!this.setWarningText.pid_) {
  1244. // Create a database of warning PIDs.
  1245. // Only runs once per block (and only those with warnings).
  1246. this.setWarningText.pid_ = Object.create(null);
  1247. }
  1248. var id = opt_id || '';
  1249. if (!id) {
  1250. // Kill all previous pending processes, this edit supercedes them all.
  1251. for (var n in this.setWarningText.pid_) {
  1252. clearTimeout(this.setWarningText.pid_[n]);
  1253. delete this.setWarningText.pid_[n];
  1254. }
  1255. } else if (this.setWarningText.pid_[id]) {
  1256. // Only queue up the latest change. Kill any earlier pending process.
  1257. clearTimeout(this.setWarningText.pid_[id]);
  1258. delete this.setWarningText.pid_[id];
  1259. }
  1260. if (Blockly.dragMode_ == Blockly.DRAG_FREE) {
  1261. // Don't change the warning text during a drag.
  1262. // Wait until the drag finishes.
  1263. var thisBlock = this;
  1264. this.setWarningText.pid_[id] = setTimeout(function() {
  1265. if (thisBlock.workspace) { // Check block wasn't deleted.
  1266. delete thisBlock.setWarningText.pid_[id];
  1267. thisBlock.setWarningText(text, id);
  1268. }
  1269. }, 100);
  1270. return;
  1271. }
  1272. if (this.isInFlyout) {
  1273. text = null;
  1274. }
  1275. // Bubble up to add a warning on top-most collapsed block.
  1276. var parent = this.getSurroundParent();
  1277. var collapsedParent = null;
  1278. while (parent) {
  1279. if (parent.isCollapsed()) {
  1280. collapsedParent = parent;
  1281. }
  1282. parent = parent.getSurroundParent();
  1283. }
  1284. if (collapsedParent) {
  1285. collapsedParent.setWarningText(text, 'collapsed ' + this.id + ' ' + id);
  1286. }
  1287. var changedState = false;
  1288. if (goog.isString(text)) {
  1289. if (!this.warning) {
  1290. this.warning = new Blockly.Warning(this);
  1291. changedState = true;
  1292. }
  1293. this.warning.setText(/** @type {string} */ (text), id);
  1294. } else {
  1295. // Dispose all warnings if no id is given.
  1296. if (this.warning && !id) {
  1297. this.warning.dispose();
  1298. changedState = true;
  1299. } else if (this.warning) {
  1300. var oldText = this.warning.getText();
  1301. this.warning.setText('', id);
  1302. var newText = this.warning.getText();
  1303. if (!newText) {
  1304. this.warning.dispose();
  1305. }
  1306. changedState = oldText == newText;
  1307. }
  1308. }
  1309. if (changedState && this.rendered) {
  1310. this.render();
  1311. // Adding or removing a warning icon will cause the block to change shape.
  1312. this.bumpNeighbours_();
  1313. }
  1314. };
  1315. /**
  1316. * Give this block a mutator dialog.
  1317. * @param {Blockly.Mutator} mutator A mutator dialog instance or null to remove.
  1318. */
  1319. Blockly.BlockSvg.prototype.setMutator = function(mutator) {
  1320. if (this.mutator && this.mutator !== mutator) {
  1321. this.mutator.dispose();
  1322. }
  1323. if (mutator) {
  1324. mutator.block_ = this;
  1325. this.mutator = mutator;
  1326. mutator.createIcon();
  1327. }
  1328. };
  1329. /**
  1330. * Set whether the block is disabled or not.
  1331. * @param {boolean} disabled True if disabled.
  1332. */
  1333. Blockly.BlockSvg.prototype.setDisabled = function(disabled) {
  1334. if (this.disabled != disabled) {
  1335. Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled);
  1336. if (this.rendered) {
  1337. this.updateDisabled();
  1338. }
  1339. }
  1340. };
  1341. /**
  1342. * Set whether the block is highlighted or not.
  1343. * @param {boolean} highlighted True if highlighted.
  1344. */
  1345. Blockly.BlockSvg.prototype.setHighlighted = function(highlighted) {
  1346. if (!this.rendered) {
  1347. return;
  1348. }
  1349. if (highlighted) {
  1350. this.svgPath_.setAttribute('filter',
  1351. 'url(#' + this.workspace.options.embossFilterId + ')');
  1352. this.svgPathLight_.style.display = 'none';
  1353. } else {
  1354. this.svgPath_.removeAttribute('filter');
  1355. this.svgPathLight_.style.display = 'block';
  1356. }
  1357. };
  1358. /**
  1359. * Select this block. Highlight it visually.
  1360. */
  1361. Blockly.BlockSvg.prototype.addSelect = function() {
  1362. Blockly.addClass_(/** @type {!Element} */ (this.svgGroup_),
  1363. 'blocklySelected');
  1364. // Move the selected block to the top of the stack.
  1365. var block = this;
  1366. do {
  1367. var root = block.getSvgRoot();
  1368. root.parentNode.appendChild(root);
  1369. block = block.getParent();
  1370. } while (block);
  1371. };
  1372. /**
  1373. * Unselect this block. Remove its highlighting.
  1374. */
  1375. Blockly.BlockSvg.prototype.removeSelect = function() {
  1376. Blockly.removeClass_(/** @type {!Element} */ (this.svgGroup_),
  1377. 'blocklySelected');
  1378. };
  1379. // Overrides of functions on Blockly.Block that take into account whether the
  1380. // block has been rendered.
  1381. /**
  1382. * Change the colour of a block.
  1383. * @param {number|string} colour HSV hue value, or #RRGGBB string.
  1384. */
  1385. Blockly.BlockSvg.prototype.setColour = function(colour) {
  1386. Blockly.BlockSvg.superClass_.setColour.call(this, colour);
  1387. if (this.rendered) {
  1388. this.updateColour();
  1389. }
  1390. };
  1391. /**
  1392. * Set whether this block can chain onto the bottom of another block.
  1393. * @param {boolean} newBoolean True if there can be a previous statement.
  1394. * @param {string|Array.<string>|null|undefined} opt_check Statement type or
  1395. * list of statement types. Null/undefined if any type could be connected.
  1396. */
  1397. Blockly.BlockSvg.prototype.setPreviousStatement =
  1398. function(newBoolean, opt_check) {
  1399. /* eslint-disable indent */
  1400. Blockly.BlockSvg.superClass_.setPreviousStatement.call(this, newBoolean,
  1401. opt_check);
  1402. if (this.rendered) {
  1403. this.render();
  1404. this.bumpNeighbours_();
  1405. }
  1406. }; /* eslint-enable indent */
  1407. /**
  1408. * Set whether another block can chain onto the bottom of this block.
  1409. * @param {boolean} newBoolean True if there can be a next statement.
  1410. * @param {string|Array.<string>|null|undefined} opt_check Statement type or
  1411. * list of statement types. Null/undefined if any type could be connected.
  1412. */
  1413. Blockly.BlockSvg.prototype.setNextStatement = function(newBoolean, opt_check) {
  1414. Blockly.BlockSvg.superClass_.setNextStatement.call(this, newBoolean,
  1415. opt_check);
  1416. if (this.rendered) {
  1417. this.render();
  1418. this.bumpNeighbours_();
  1419. }
  1420. };
  1421. /**
  1422. * Set whether this block returns a value.
  1423. * @param {boolean} newBoolean True if there is an output.
  1424. * @param {string|Array.<string>|null|undefined} opt_check Returned type or list
  1425. * of returned types. Null or undefined if any type could be returned
  1426. * (e.g. variable get).
  1427. */
  1428. Blockly.BlockSvg.prototype.setOutput = function(newBoolean, opt_check) {
  1429. Blockly.BlockSvg.superClass_.setOutput.call(this, newBoolean, opt_check);
  1430. if (this.rendered) {
  1431. this.render();
  1432. this.bumpNeighbours_();
  1433. }
  1434. };
  1435. /**
  1436. * Set whether value inputs are arranged horizontally or vertically.
  1437. * @param {boolean} newBoolean True if inputs are horizontal.
  1438. */
  1439. Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) {
  1440. Blockly.BlockSvg.superClass_.setInputsInline.call(this, newBoolean);
  1441. if (this.rendered) {
  1442. this.render();
  1443. this.bumpNeighbours_();
  1444. }
  1445. };
  1446. /**
  1447. * Remove an input from this block.
  1448. * @param {string} name The name of the input.
  1449. * @param {boolean=} opt_quiet True to prevent error if input is not present.
  1450. * @throws {goog.asserts.AssertionError} if the input is not present and
  1451. * opt_quiet is not true.
  1452. */
  1453. Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) {
  1454. Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet);
  1455. if (this.rendered) {
  1456. this.render();
  1457. // Removing an input will cause the block to change shape.
  1458. this.bumpNeighbours_();
  1459. }
  1460. };
  1461. /**
  1462. * Move a numbered input to a different location on this block.
  1463. * @param {number} inputIndex Index of the input to move.
  1464. * @param {number} refIndex Index of input that should be after the moved input.
  1465. */
  1466. Blockly.BlockSvg.prototype.moveNumberedInputBefore = function(
  1467. inputIndex, refIndex) {
  1468. Blockly.BlockSvg.superClass_.moveNumberedInputBefore.call(this, inputIndex,
  1469. refIndex);
  1470. if (this.rendered) {
  1471. this.render();
  1472. // Moving an input will cause the block to change shape.
  1473. this.bumpNeighbours_();
  1474. }
  1475. };
  1476. /**
  1477. * Add a value input, statement input or local variable to this block.
  1478. * @param {number} type Either Blockly.INPUT_VALUE or Blockly.NEXT_STATEMENT or
  1479. * Blockly.DUMMY_INPUT.
  1480. * @param {string} name Language-neutral identifier which may used to find this
  1481. * input again. Should be unique to this block.
  1482. * @return {!Blockly.Input} The input object created.
  1483. * @private
  1484. */
  1485. Blockly.BlockSvg.prototype.appendInput_ = function(type, name) {
  1486. var input = Blockly.BlockSvg.superClass_.appendInput_.call(this, type, name);
  1487. if (this.rendered) {
  1488. this.render();
  1489. // Adding an input will cause the block to change shape.
  1490. this.bumpNeighbours_();
  1491. }
  1492. return input;
  1493. };
  1494. /**
  1495. * Returns connections originating from this block.
  1496. * @param {boolean} all If true, return all connections even hidden ones.
  1497. * Otherwise, for a non-rendered block return an empty list, and for a
  1498. * collapsed block don't return inputs connections.
  1499. * @return {!Array.<!Blockly.Connection>} Array of connections.
  1500. * @private
  1501. */
  1502. Blockly.BlockSvg.prototype.getConnections_ = function(all) {
  1503. var myConnections = [];
  1504. if (all || this.rendered) {
  1505. if (this.outputConnection) {
  1506. myConnections.push(this.outputConnection);
  1507. }
  1508. if (this.previousConnection) {
  1509. myConnections.push(this.previousConnection);
  1510. }
  1511. if (this.nextConnection) {
  1512. myConnections.push(this.nextConnection);
  1513. }
  1514. if (all || !this.collapsed_) {
  1515. for (var i = 0, input; input = this.inputList[i]; i++) {
  1516. if (input.connection) {
  1517. myConnections.push(input.connection);
  1518. }
  1519. }
  1520. }
  1521. }
  1522. return myConnections;
  1523. };
  1524. /**
  1525. * Create a connection of the specified type.
  1526. * @param {number} type The type of the connection to create.
  1527. * @return {!Blockly.RenderedConnection} A new connection of the specified type.
  1528. * @private
  1529. */
  1530. Blockly.BlockSvg.prototype.makeConnection_ = function(type) {
  1531. return new Blockly.RenderedConnection(this, type);
  1532. };