blockly 7.20211209.4 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/blockly.d.ts +18963 -18432
  2. package/blockly.min.js +5 -4
  3. package/blockly_compressed.js +4 -3
  4. package/blockly_compressed.js.map +1 -1
  5. package/blocks/blocks.js +47 -0
  6. package/blocks/colour.js +13 -3
  7. package/blocks/lists.js +22 -13
  8. package/blocks/logic.js +13 -3
  9. package/blocks/loops.js +24 -11
  10. package/blocks/math.js +12 -3
  11. package/blocks/procedures.js +45 -32
  12. package/blocks/text.js +22 -13
  13. package/blocks/variables.js +14 -3
  14. package/blocks/variables_dynamic.js +13 -3
  15. package/blocks_compressed.js +1 -1
  16. package/blocks_compressed.js.map +1 -1
  17. package/core/block.js +1869 -1814
  18. package/core/block_drag_surface.js +201 -200
  19. package/core/block_dragger.js +377 -373
  20. package/core/block_svg.js +1593 -1479
  21. package/core/blockly.js +8 -22
  22. package/core/blocks.js +9 -2
  23. package/core/browser_events.js +22 -5
  24. package/core/bubble.js +841 -797
  25. package/core/bubble_dragger.js +213 -206
  26. package/core/bump_objects.js +2 -2
  27. package/core/clipboard.js +9 -9
  28. package/core/comment.js +353 -332
  29. package/core/common.js +46 -17
  30. package/core/component_manager.js +181 -174
  31. package/core/config.js +87 -0
  32. package/core/connection.js +595 -584
  33. package/core/connection_checker.js +242 -244
  34. package/core/connection_db.js +235 -230
  35. package/core/contextmenu.js +9 -6
  36. package/core/contextmenu_items.js +1 -2
  37. package/core/contextmenu_registry.js +93 -89
  38. package/core/css.js +474 -474
  39. package/core/delete_area.js +45 -42
  40. package/core/drag_target.js +57 -56
  41. package/core/dropdowndiv.js +153 -163
  42. package/core/events/events.js +2 -2
  43. package/core/events/events_abstract.js +89 -77
  44. package/core/events/events_block_base.js +37 -36
  45. package/core/events/events_block_change.js +130 -124
  46. package/core/events/events_block_create.js +73 -71
  47. package/core/events/events_block_delete.js +84 -82
  48. package/core/events/events_block_drag.js +50 -49
  49. package/core/events/events_block_move.js +147 -140
  50. package/core/events/events_bubble_open.js +51 -50
  51. package/core/events/events_click.js +48 -44
  52. package/core/events/events_comment_base.js +72 -69
  53. package/core/events/events_comment_change.js +63 -61
  54. package/core/events/events_comment_create.js +44 -42
  55. package/core/events/events_comment_delete.js +42 -40
  56. package/core/events/events_comment_move.js +106 -104
  57. package/core/events/events_marker_move.js +65 -64
  58. package/core/events/events_selected.js +46 -45
  59. package/core/events/events_theme_change.js +36 -35
  60. package/core/events/events_toolbox_item_select.js +46 -45
  61. package/core/events/events_trashcan_open.js +37 -36
  62. package/core/events/events_ui.js +47 -46
  63. package/core/events/events_ui_base.js +30 -29
  64. package/core/events/events_var_base.js +37 -36
  65. package/core/events/events_var_create.js +50 -48
  66. package/core/events/events_var_delete.js +50 -48
  67. package/core/events/events_var_rename.js +51 -49
  68. package/core/events/events_viewport.js +66 -65
  69. package/core/events/utils.js +29 -14
  70. package/core/events/workspace_events.js +49 -55
  71. package/core/extensions.js +4 -3
  72. package/core/field.js +1061 -997
  73. package/core/field_angle.js +462 -442
  74. package/core/field_checkbox.js +194 -182
  75. package/core/field_colour.js +519 -505
  76. package/core/field_dropdown.js +617 -598
  77. package/core/field_image.js +229 -220
  78. package/core/field_label.js +102 -91
  79. package/core/field_label_serializable.js +42 -41
  80. package/core/field_multilineinput.js +372 -358
  81. package/core/field_number.js +272 -253
  82. package/core/field_textinput.js +499 -467
  83. package/core/field_variable.js +458 -420
  84. package/core/flyout_base.js +1005 -952
  85. package/core/flyout_button.js +277 -260
  86. package/core/flyout_horizontal.js +304 -302
  87. package/core/flyout_metrics_manager.js +64 -64
  88. package/core/flyout_vertical.js +306 -300
  89. package/core/generator.js +459 -446
  90. package/core/gesture.js +829 -813
  91. package/core/grid.js +166 -163
  92. package/core/icon.js +168 -159
  93. package/core/inject.js +7 -5
  94. package/core/input.js +257 -248
  95. package/core/insertion_marker_manager.js +655 -624
  96. package/core/internal_constants.js +0 -129
  97. package/core/keyboard_nav/ast_node.js +605 -596
  98. package/core/keyboard_nav/basic_cursor.js +166 -165
  99. package/core/keyboard_nav/cursor.js +99 -97
  100. package/core/keyboard_nav/marker.js +83 -79
  101. package/core/keyboard_nav/tab_navigate_cursor.js +18 -23
  102. package/core/marker_manager.js +153 -141
  103. package/core/menu.js +377 -372
  104. package/core/menuitem.js +223 -217
  105. package/core/metrics_manager.js +403 -390
  106. package/core/mutator.js +468 -437
  107. package/core/names.js +229 -188
  108. package/core/options.js +290 -284
  109. package/core/procedures.js +29 -17
  110. package/core/registry.js +19 -16
  111. package/core/rendered_connection.js +482 -463
  112. package/core/renderers/common/block_rendering.js +9 -3
  113. package/core/renderers/common/constants.js +1119 -1112
  114. package/core/renderers/common/debug.js +14 -0
  115. package/core/renderers/common/debugger.js +338 -316
  116. package/core/renderers/common/drawer.js +380 -370
  117. package/core/renderers/common/i_path_object.js +2 -2
  118. package/core/renderers/common/info.js +626 -618
  119. package/core/renderers/common/marker_svg.js +579 -541
  120. package/core/renderers/common/path_object.js +203 -200
  121. package/core/renderers/common/renderer.js +220 -218
  122. package/core/renderers/geras/constants.js +36 -36
  123. package/core/renderers/geras/drawer.js +155 -147
  124. package/core/renderers/geras/highlight_constants.js +244 -238
  125. package/core/renderers/geras/highlighter.js +231 -179
  126. package/core/renderers/geras/info.js +392 -369
  127. package/core/renderers/geras/measurables/inline_input.js +25 -19
  128. package/core/renderers/geras/measurables/statement_input.js +23 -17
  129. package/core/renderers/geras/path_object.js +106 -121
  130. package/core/renderers/geras/renderer.js +96 -98
  131. package/core/renderers/measurables/base.js +30 -18
  132. package/core/renderers/measurables/bottom_row.js +83 -80
  133. package/core/renderers/measurables/connection.js +22 -15
  134. package/core/renderers/measurables/external_value_input.js +35 -22
  135. package/core/renderers/measurables/field.js +35 -20
  136. package/core/renderers/measurables/hat.js +18 -13
  137. package/core/renderers/measurables/icon.js +24 -17
  138. package/core/renderers/measurables/in_row_spacer.js +15 -13
  139. package/core/renderers/measurables/inline_input.js +43 -33
  140. package/core/renderers/measurables/input_connection.js +41 -28
  141. package/core/renderers/measurables/input_row.js +50 -44
  142. package/core/renderers/measurables/jagged_edge.js +14 -12
  143. package/core/renderers/measurables/next_connection.js +16 -14
  144. package/core/renderers/measurables/output_connection.js +26 -20
  145. package/core/renderers/measurables/previous_connection.js +16 -15
  146. package/core/renderers/measurables/round_corner.js +20 -18
  147. package/core/renderers/measurables/row.js +184 -168
  148. package/core/renderers/measurables/spacer_row.js +38 -23
  149. package/core/renderers/measurables/square_corner.js +18 -16
  150. package/core/renderers/measurables/statement_input.js +23 -20
  151. package/core/renderers/measurables/top_row.js +88 -85
  152. package/core/renderers/minimalist/constants.js +8 -7
  153. package/core/renderers/minimalist/drawer.js +11 -10
  154. package/core/renderers/minimalist/info.js +18 -18
  155. package/core/renderers/minimalist/renderer.js +40 -39
  156. package/core/renderers/thrasos/info.js +258 -248
  157. package/core/renderers/thrasos/renderer.js +20 -20
  158. package/core/renderers/zelos/constants.js +898 -873
  159. package/core/renderers/zelos/drawer.js +186 -169
  160. package/core/renderers/zelos/info.js +502 -479
  161. package/core/renderers/zelos/marker_svg.js +129 -115
  162. package/core/renderers/zelos/measurables/bottom_row.js +31 -30
  163. package/core/renderers/zelos/measurables/inputs.js +22 -21
  164. package/core/renderers/zelos/measurables/row_elements.js +14 -13
  165. package/core/renderers/zelos/measurables/top_row.js +34 -33
  166. package/core/renderers/zelos/path_object.js +181 -180
  167. package/core/renderers/zelos/renderer.js +91 -92
  168. package/core/scrollbar.js +759 -713
  169. package/core/scrollbar_pair.js +250 -245
  170. package/core/serialization/blocks.js +26 -10
  171. package/core/serialization/workspaces.js +3 -2
  172. package/core/shortcut_registry.js +286 -277
  173. package/core/sprites.js +31 -0
  174. package/core/theme.js +135 -141
  175. package/core/theme_manager.js +147 -143
  176. package/core/toolbox/category.js +602 -576
  177. package/core/toolbox/collapsible_category.js +226 -227
  178. package/core/toolbox/separator.js +70 -61
  179. package/core/toolbox/toolbox.js +934 -927
  180. package/core/toolbox/toolbox_item.js +115 -99
  181. package/core/tooltip.js +108 -35
  182. package/core/touch.js +8 -3
  183. package/core/touch_gesture.js +254 -251
  184. package/core/trashcan.js +606 -595
  185. package/core/utils/coordinate.js +97 -95
  186. package/core/utils/dom.js +2 -2
  187. package/core/utils/global.js +2 -0
  188. package/core/utils/rect.js +41 -37
  189. package/core/utils/sentinel.js +25 -0
  190. package/core/utils/size.js +30 -27
  191. package/core/utils/svg.js +18 -16
  192. package/core/variable_map.js +325 -341
  193. package/core/variable_model.js +55 -54
  194. package/core/variables.js +9 -2
  195. package/core/variables_dynamic.js +3 -1
  196. package/core/warning.js +126 -120
  197. package/core/widgetdiv.js +4 -4
  198. package/core/workspace.js +685 -664
  199. package/core/workspace_audio.js +124 -118
  200. package/core/workspace_comment.js +308 -298
  201. package/core/workspace_comment_svg.js +1029 -951
  202. package/core/workspace_drag_surface_svg.js +147 -140
  203. package/core/workspace_dragger.js +70 -71
  204. package/core/workspace_svg.js +2322 -2297
  205. package/core/xml.js +30 -20
  206. package/core/zoom_controls.js +431 -439
  207. package/generators/dart/colour.js +56 -64
  208. package/generators/dart/lists.js +61 -50
  209. package/generators/dart/math.js +160 -148
  210. package/generators/dart/text.js +83 -61
  211. package/generators/javascript/colour.js +37 -34
  212. package/generators/javascript/lists.js +50 -43
  213. package/generators/javascript/math.js +123 -139
  214. package/generators/javascript/text.js +67 -81
  215. package/generators/lua/colour.js +25 -23
  216. package/generators/lua/lists.js +97 -69
  217. package/generators/lua/logic.js +1 -2
  218. package/generators/lua/math.js +182 -144
  219. package/generators/lua/text.js +116 -99
  220. package/generators/php/colour.js +38 -32
  221. package/generators/php/lists.js +109 -89
  222. package/generators/php/math.js +90 -81
  223. package/generators/php/text.js +63 -61
  224. package/generators/python/colour.js +18 -18
  225. package/generators/python/lists.js +38 -30
  226. package/generators/python/loops.js +12 -8
  227. package/generators/python/math.js +104 -106
  228. package/generators/python/text.js +34 -30
  229. package/msg/smn.js +436 -0
  230. package/package.json +7 -6
  231. package/blocks/all.js +0 -23
@@ -24,7 +24,6 @@ const common = goog.require('Blockly.common');
24
24
  const dom = goog.require('Blockly.utils.dom');
25
25
  const eventUtils = goog.require('Blockly.Events.utils');
26
26
  const idGenerator = goog.require('Blockly.utils.idGenerator');
27
- const object = goog.require('Blockly.utils.object');
28
27
  const toolbox = goog.require('Blockly.utils.toolbox');
29
28
  /* eslint-disable-next-line no-unused-vars */
30
29
  const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
@@ -59,1076 +58,1130 @@ goog.require('Blockly.blockRendering');
59
58
 
60
59
  /**
61
60
  * Class for a flyout.
62
- * @param {!Options} workspaceOptions Dictionary of options for the
63
- * workspace.
64
- * @constructor
65
61
  * @abstract
66
62
  * @implements {IFlyout}
67
63
  * @extends {DeleteArea}
68
64
  * @alias Blockly.Flyout
69
65
  */
70
- const Flyout = function(workspaceOptions) {
71
- Flyout.superClass_.constructor.call(this);
72
- workspaceOptions.setMetrics = this.setMetrics_.bind(this);
73
-
66
+ class Flyout extends DeleteArea {
74
67
  /**
75
- * @type {!WorkspaceSvg}
76
- * @protected
68
+ * @param {!Options} workspaceOptions Dictionary of options for the
69
+ * workspace.
77
70
  */
78
- this.workspace_ = new WorkspaceSvg(workspaceOptions);
79
- this.workspace_.setMetricsManager(
80
- new FlyoutMetricsManager(this.workspace_, this));
71
+ constructor(workspaceOptions) {
72
+ super();
73
+ workspaceOptions.setMetrics = this.setMetrics_.bind(this);
74
+
75
+ /**
76
+ * @type {!WorkspaceSvg}
77
+ * @protected
78
+ */
79
+ this.workspace_ = new WorkspaceSvg(workspaceOptions);
80
+ this.workspace_.setMetricsManager(
81
+ new FlyoutMetricsManager(this.workspace_, this));
82
+
83
+ this.workspace_.isFlyout = true;
84
+ // Keep the workspace visibility consistent with the flyout's visibility.
85
+ this.workspace_.setVisible(this.isVisible_);
86
+
87
+ /**
88
+ * The unique id for this component that is used to register with the
89
+ * ComponentManager.
90
+ * @type {string}
91
+ */
92
+ this.id = idGenerator.genUid();
93
+
94
+ /**
95
+ * Is RTL vs LTR.
96
+ * @type {boolean}
97
+ */
98
+ this.RTL = !!workspaceOptions.RTL;
99
+
100
+ /**
101
+ * Whether the flyout should be laid out horizontally or not.
102
+ * @type {boolean}
103
+ * @package
104
+ */
105
+ this.horizontalLayout = false;
106
+
107
+ /**
108
+ * Position of the toolbox and flyout relative to the workspace.
109
+ * @type {number}
110
+ * @protected
111
+ */
112
+ this.toolboxPosition_ = workspaceOptions.toolboxPosition;
113
+
114
+ /**
115
+ * Opaque data that can be passed to Blockly.unbindEvent_.
116
+ * @type {!Array<!Array>}
117
+ * @private
118
+ */
119
+ this.eventWrappers_ = [];
120
+
121
+ /**
122
+ * Function that will be registered as a change listener on the workspace
123
+ * to reflow when blocks in the flyout workspace change.
124
+ * @type {?Function}
125
+ * @private
126
+ */
127
+ this.reflowWrapper_ = null;
81
128
 
82
- this.workspace_.isFlyout = true;
83
- // Keep the workspace visibility consistent with the flyout's visibility.
84
- this.workspace_.setVisible(this.isVisible_);
129
+ /**
130
+ * Function that disables blocks in the flyout based on max block counts
131
+ * allowed in the target workspace. Registered as a change listener on the
132
+ * target workspace.
133
+ * @type {?Function}
134
+ * @private
135
+ */
136
+ this.filterWrapper_ = null;
85
137
 
86
- /**
87
- * The unique id for this component that is used to register with the
88
- * ComponentManager.
89
- * @type {string}
90
- */
91
- this.id = idGenerator.genUid();
138
+ /**
139
+ * List of background mats that lurk behind each block to catch clicks
140
+ * landing in the blocks' lakes and bays.
141
+ * @type {!Array<!SVGElement>}
142
+ * @private
143
+ */
144
+ this.mats_ = [];
145
+
146
+ /**
147
+ * List of visible buttons.
148
+ * @type {!Array<!FlyoutButton>}
149
+ * @protected
150
+ */
151
+ this.buttons_ = [];
152
+
153
+ /**
154
+ * List of event listeners.
155
+ * @type {!Array<!Array>}
156
+ * @private
157
+ */
158
+ this.listeners_ = [];
159
+
160
+ /**
161
+ * List of blocks that should always be disabled.
162
+ * @type {!Array<!Block>}
163
+ * @private
164
+ */
165
+ this.permanentlyDisabled_ = [];
166
+
167
+ /**
168
+ * Width of output tab.
169
+ * @type {number}
170
+ * @protected
171
+ * @const
172
+ */
173
+ this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH;
174
+
175
+ /**
176
+ * The target workspace
177
+ * @type {?WorkspaceSvg}
178
+ * @package
179
+ */
180
+ this.targetWorkspace = null;
181
+
182
+ /**
183
+ * A list of blocks that can be reused.
184
+ * @type {!Array<!BlockSvg>}
185
+ * @private
186
+ */
187
+ this.recycledBlocks_ = [];
188
+
189
+ /**
190
+ * Does the flyout automatically close when a block is created?
191
+ * @type {boolean}
192
+ */
193
+ this.autoClose = true;
194
+
195
+ /**
196
+ * Whether the flyout is visible.
197
+ * @type {boolean}
198
+ * @private
199
+ */
200
+ this.isVisible_ = false;
201
+
202
+ /**
203
+ * Whether the workspace containing this flyout is visible.
204
+ * @type {boolean}
205
+ * @private
206
+ */
207
+ this.containerVisible_ = true;
208
+
209
+ /**
210
+ * A map from blocks to the rects which are beneath them to act as input
211
+ * targets.
212
+ * @type {!WeakMap<!BlockSvg, !SVGElement>}
213
+ * @private
214
+ */
215
+ this.rectMap_ = new WeakMap();
216
+
217
+ /**
218
+ * Corner radius of the flyout background.
219
+ * @type {number}
220
+ * @const
221
+ */
222
+ this.CORNER_RADIUS = 8;
223
+
224
+ /**
225
+ * Margin around the edges of the blocks in the flyout.
226
+ * @type {number}
227
+ * @const
228
+ */
229
+ this.MARGIN = this.CORNER_RADIUS;
230
+
231
+ // TODO: Move GAP_X and GAP_Y to their appropriate files.
232
+
233
+ /**
234
+ * Gap between items in horizontal flyouts. Can be overridden with the "sep"
235
+ * element.
236
+ * @const {number}
237
+ */
238
+ this.GAP_X = this.MARGIN * 3;
239
+
240
+ /**
241
+ * Gap between items in vertical flyouts. Can be overridden with the "sep"
242
+ * element.
243
+ * @const {number}
244
+ */
245
+ this.GAP_Y = this.MARGIN * 3;
246
+
247
+ /**
248
+ * Top/bottom padding between scrollbar and edge of flyout background.
249
+ * @type {number}
250
+ * @const
251
+ */
252
+ this.SCROLLBAR_MARGIN = 2.5;
253
+
254
+ /**
255
+ * Width of flyout.
256
+ * @type {number}
257
+ * @protected
258
+ */
259
+ this.width_ = 0;
260
+
261
+ /**
262
+ * Height of flyout.
263
+ * @type {number}
264
+ * @protected
265
+ */
266
+ this.height_ = 0;
267
+
268
+ // clang-format off
269
+ /**
270
+ * Range of a drag angle from a flyout considered "dragging toward
271
+ * workspace". Drags that are within the bounds of this many degrees from
272
+ * the orthogonal line to the flyout edge are considered to be "drags toward
273
+ * the workspace".
274
+ * Example:
275
+ * Flyout Edge Workspace
276
+ * [block] / <-within this angle, drags "toward workspace" |
277
+ * [block] ---- orthogonal to flyout boundary ---- |
278
+ * [block] \ |
279
+ * The angle is given in degrees from the orthogonal.
280
+ *
281
+ * This is used to know when to create a new block and when to scroll the
282
+ * flyout. Setting it to 360 means that all drags create a new block.
283
+ * @type {number}
284
+ * @protected
285
+ */
286
+ // clang-format on
287
+ this.dragAngleRange_ = 70;
288
+
289
+ /**
290
+ * The path around the background of the flyout, which will be filled with a
291
+ * background colour.
292
+ * @type {?SVGPathElement}
293
+ * @protected
294
+ */
295
+ this.svgBackground_ = null;
296
+
297
+ /**
298
+ * The root SVG group for the button or label.
299
+ * @type {?SVGGElement}
300
+ * @protected
301
+ */
302
+ this.svgGroup_ = null;
303
+ }
92
304
 
93
305
  /**
94
- * Is RTL vs LTR.
95
- * @type {boolean}
306
+ * Creates the flyout's DOM. Only needs to be called once. The flyout can
307
+ * either exist as its own SVG element or be a g element nested inside a
308
+ * separate SVG element.
309
+ * @param {string|
310
+ * !Svg<!SVGSVGElement>|
311
+ * !Svg<!SVGGElement>} tagName The type of tag to
312
+ * put the flyout in. This should be <svg> or <g>.
313
+ * @return {!SVGElement} The flyout's SVG group.
96
314
  */
97
- this.RTL = !!workspaceOptions.RTL;
315
+ createDom(tagName) {
316
+ /*
317
+ <svg | g>
318
+ <path class="blocklyFlyoutBackground"/>
319
+ <g class="blocklyFlyout"></g>
320
+ </ svg | g>
321
+ */
322
+ // Setting style to display:none to start. The toolbox and flyout
323
+ // hide/show code will set up proper visibility and size later.
324
+ this.svgGroup_ = dom.createSvgElement(
325
+ tagName, {'class': 'blocklyFlyout', 'style': 'display: none'}, null);
326
+ this.svgBackground_ = dom.createSvgElement(
327
+ Svg.PATH, {'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
328
+ this.svgGroup_.appendChild(this.workspace_.createDom());
329
+ this.workspace_.getThemeManager().subscribe(
330
+ this.svgBackground_, 'flyoutBackgroundColour', 'fill');
331
+ this.workspace_.getThemeManager().subscribe(
332
+ this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
333
+ return this.svgGroup_;
334
+ }
98
335
 
99
336
  /**
100
- * Whether the flyout should be laid out horizontally or not.
101
- * @type {boolean}
102
- * @package
337
+ * Initializes the flyout.
338
+ * @param {!WorkspaceSvg} targetWorkspace The workspace in which to
339
+ * create new blocks.
103
340
  */
104
- this.horizontalLayout = false;
341
+ init(targetWorkspace) {
342
+ this.targetWorkspace = targetWorkspace;
343
+ this.workspace_.targetWorkspace = targetWorkspace;
344
+
345
+ this.workspace_.scrollbar = new ScrollbarPair(
346
+ this.workspace_, this.horizontalLayout, !this.horizontalLayout,
347
+ 'blocklyFlyoutScrollbar', this.SCROLLBAR_MARGIN);
348
+
349
+ this.hide();
350
+
351
+ Array.prototype.push.apply(
352
+ this.eventWrappers_,
353
+ browserEvents.conditionalBind(
354
+ /** @type {!SVGGElement} */ (this.svgGroup_), 'wheel', this,
355
+ this.wheel_));
356
+ if (!this.autoClose) {
357
+ this.filterWrapper_ = this.filterForCapacity_.bind(this);
358
+ this.targetWorkspace.addChangeListener(this.filterWrapper_);
359
+ }
360
+
361
+ // Dragging the flyout up and down.
362
+ Array.prototype.push.apply(
363
+ this.eventWrappers_,
364
+ browserEvents.conditionalBind(
365
+ /** @type {!SVGPathElement} */ (this.svgBackground_), 'mousedown',
366
+ this, this.onMouseDown_));
367
+
368
+ // A flyout connected to a workspace doesn't have its own current gesture.
369
+ this.workspace_.getGesture =
370
+ this.targetWorkspace.getGesture.bind(this.targetWorkspace);
371
+
372
+ // Get variables from the main workspace rather than the target workspace.
373
+ this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap());
374
+
375
+ this.workspace_.createPotentialVariableMap();
376
+
377
+ targetWorkspace.getComponentManager().addComponent({
378
+ component: this,
379
+ weight: 1,
380
+ capabilities: [
381
+ ComponentManager.Capability.DELETE_AREA,
382
+ ComponentManager.Capability.DRAG_TARGET,
383
+ ],
384
+ });
385
+ }
105
386
 
106
387
  /**
107
- * Position of the toolbox and flyout relative to the workspace.
108
- * @type {number}
109
- * @protected
388
+ * Dispose of this flyout.
389
+ * Unlink from all DOM elements to prevent memory leaks.
390
+ * @suppress {checkTypes}
110
391
  */
111
- this.toolboxPosition_ = workspaceOptions.toolboxPosition;
392
+ dispose() {
393
+ this.hide();
394
+ this.workspace_.getComponentManager().removeComponent(this.id);
395
+ browserEvents.unbind(this.eventWrappers_);
396
+ if (this.filterWrapper_) {
397
+ this.targetWorkspace.removeChangeListener(this.filterWrapper_);
398
+ this.filterWrapper_ = null;
399
+ }
400
+ if (this.workspace_) {
401
+ this.workspace_.getThemeManager().unsubscribe(this.svgBackground_);
402
+ this.workspace_.targetWorkspace = null;
403
+ this.workspace_.dispose();
404
+ this.workspace_ = null;
405
+ }
406
+ if (this.svgGroup_) {
407
+ dom.removeNode(this.svgGroup_);
408
+ this.svgGroup_ = null;
409
+ }
410
+ this.svgBackground_ = null;
411
+ this.targetWorkspace = null;
412
+ }
112
413
 
113
414
  /**
114
- * Opaque data that can be passed to Blockly.unbindEvent_.
115
- * @type {!Array<!Array>}
116
- * @private
415
+ * Get the width of the flyout.
416
+ * @return {number} The width of the flyout.
117
417
  */
118
- this.eventWrappers_ = [];
418
+ getWidth() {
419
+ return this.width_;
420
+ }
119
421
 
120
422
  /**
121
- * List of background mats that lurk behind each block to catch clicks
122
- * landing in the blocks' lakes and bays.
123
- * @type {!Array<!SVGElement>}
124
- * @private
423
+ * Get the height of the flyout.
424
+ * @return {number} The width of the flyout.
125
425
  */
126
- this.mats_ = [];
426
+ getHeight() {
427
+ return this.height_;
428
+ }
127
429
 
128
430
  /**
129
- * List of visible buttons.
130
- * @type {!Array<!FlyoutButton>}
131
- * @protected
431
+ * Get the scale (zoom level) of the flyout. By default,
432
+ * this matches the target workspace scale, but this can be overridden.
433
+ * @return {number} Flyout workspace scale.
132
434
  */
133
- this.buttons_ = [];
435
+ getFlyoutScale() {
436
+ return this.targetWorkspace.scale;
437
+ }
134
438
 
135
439
  /**
136
- * List of event listeners.
137
- * @type {!Array<!Array>}
138
- * @private
440
+ * Get the workspace inside the flyout.
441
+ * @return {!WorkspaceSvg} The workspace inside the flyout.
442
+ * @package
139
443
  */
140
- this.listeners_ = [];
444
+ getWorkspace() {
445
+ return this.workspace_;
446
+ }
141
447
 
142
448
  /**
143
- * List of blocks that should always be disabled.
144
- * @type {!Array<!Block>}
145
- * @private
449
+ * Is the flyout visible?
450
+ * @return {boolean} True if visible.
146
451
  */
147
- this.permanentlyDisabled_ = [];
452
+ isVisible() {
453
+ return this.isVisible_;
454
+ }
148
455
 
149
456
  /**
150
- * Width of output tab.
151
- * @type {number}
152
- * @protected
153
- * @const
457
+ * Set whether the flyout is visible. A value of true does not necessarily
458
+ * mean that the flyout is shown. It could be hidden because its container is
459
+ * hidden.
460
+ * @param {boolean} visible True if visible.
154
461
  */
155
- this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH;
462
+ setVisible(visible) {
463
+ const visibilityChanged = (visible !== this.isVisible());
464
+
465
+ this.isVisible_ = visible;
466
+ if (visibilityChanged) {
467
+ if (!this.autoClose) {
468
+ // Auto-close flyouts are ignored as drag targets, so only non
469
+ // auto-close flyouts need to have their drag target updated.
470
+ this.workspace_.recordDragTargets();
471
+ }
472
+ this.updateDisplay_();
473
+ }
474
+ }
156
475
 
157
476
  /**
158
- * The target workspace
159
- * @type {?WorkspaceSvg}
160
- * @package
477
+ * Set whether this flyout's container is visible.
478
+ * @param {boolean} visible Whether the container is visible.
161
479
  */
162
- this.targetWorkspace = null;
480
+ setContainerVisible(visible) {
481
+ const visibilityChanged = (visible !== this.containerVisible_);
482
+ this.containerVisible_ = visible;
483
+ if (visibilityChanged) {
484
+ this.updateDisplay_();
485
+ }
486
+ }
163
487
 
164
488
  /**
165
- * A list of blocks that can be reused.
166
- * @type {!Array<!BlockSvg>}
489
+ * Update the display property of the flyout based whether it thinks it should
490
+ * be visible and whether its containing workspace is visible.
167
491
  * @private
168
492
  */
169
- this.recycledBlocks_ = [];
170
- };
171
- object.inherits(Flyout, DeleteArea);
172
-
173
- /**
174
- * Does the flyout automatically close when a block is created?
175
- * @type {boolean}
176
- */
177
- Flyout.prototype.autoClose = true;
178
-
179
- /**
180
- * Whether the flyout is visible.
181
- * @type {boolean}
182
- * @private
183
- */
184
- Flyout.prototype.isVisible_ = false;
185
-
186
- /**
187
- * Whether the workspace containing this flyout is visible.
188
- * @type {boolean}
189
- * @private
190
- */
191
- Flyout.prototype.containerVisible_ = true;
192
-
193
- /**
194
- * Corner radius of the flyout background.
195
- * @type {number}
196
- * @const
197
- */
198
- Flyout.prototype.CORNER_RADIUS = 8;
199
-
200
- /**
201
- * Margin around the edges of the blocks in the flyout.
202
- * @type {number}
203
- * @const
204
- */
205
- Flyout.prototype.MARGIN = Flyout.prototype.CORNER_RADIUS;
206
-
207
- // TODO: Move GAP_X and GAP_Y to their appropriate files.
208
-
209
- /**
210
- * Gap between items in horizontal flyouts. Can be overridden with the "sep"
211
- * element.
212
- * @const {number}
213
- */
214
- Flyout.prototype.GAP_X = Flyout.prototype.MARGIN * 3;
215
-
216
- /**
217
- * Gap between items in vertical flyouts. Can be overridden with the "sep"
218
- * element.
219
- * @const {number}
220
- */
221
- Flyout.prototype.GAP_Y = Flyout.prototype.MARGIN * 3;
222
-
223
- /**
224
- * Top/bottom padding between scrollbar and edge of flyout background.
225
- * @type {number}
226
- * @const
227
- */
228
- Flyout.prototype.SCROLLBAR_MARGIN = 2.5;
229
-
230
- /**
231
- * Width of flyout.
232
- * @type {number}
233
- * @protected
234
- */
235
- Flyout.prototype.width_ = 0;
236
-
237
- /**
238
- * Height of flyout.
239
- * @type {number}
240
- * @protected
241
- */
242
- Flyout.prototype.height_ = 0;
243
-
244
- /**
245
- * Range of a drag angle from a flyout considered "dragging toward workspace".
246
- * Drags that are within the bounds of this many degrees from the orthogonal
247
- * line to the flyout edge are considered to be "drags toward the workspace".
248
- * Example:
249
- * Flyout Edge Workspace
250
- * [block] / <-within this angle, drags "toward workspace" |
251
- * [block] ---- orthogonal to flyout boundary ---- |
252
- * [block] \ |
253
- * The angle is given in degrees from the orthogonal.
254
- *
255
- * This is used to know when to create a new block and when to scroll the
256
- * flyout. Setting it to 360 means that all drags create a new block.
257
- * @type {number}
258
- * @protected
259
- */
260
- Flyout.prototype.dragAngleRange_ = 70;
261
-
262
- /**
263
- * Creates the flyout's DOM. Only needs to be called once. The flyout can
264
- * either exist as its own SVG element or be a g element nested inside a
265
- * separate SVG element.
266
- * @param {string|
267
- * !Svg<!SVGSVGElement>|
268
- * !Svg<!SVGGElement>} tagName The type of tag to
269
- * put the flyout in. This should be <svg> or <g>.
270
- * @return {!SVGElement} The flyout's SVG group.
271
- */
272
- Flyout.prototype.createDom = function(tagName) {
273
- /*
274
- <svg | g>
275
- <path class="blocklyFlyoutBackground"/>
276
- <g class="blocklyFlyout"></g>
277
- </ svg | g>
278
- */
279
- // Setting style to display:none to start. The toolbox and flyout
280
- // hide/show code will set up proper visibility and size later.
281
- this.svgGroup_ = dom.createSvgElement(
282
- tagName, {'class': 'blocklyFlyout', 'style': 'display: none'}, null);
283
- this.svgBackground_ = dom.createSvgElement(
284
- Svg.PATH, {'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
285
- this.svgGroup_.appendChild(this.workspace_.createDom());
286
- this.workspace_.getThemeManager().subscribe(
287
- this.svgBackground_, 'flyoutBackgroundColour', 'fill');
288
- this.workspace_.getThemeManager().subscribe(
289
- this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
290
- return this.svgGroup_;
291
- };
292
-
293
- /**
294
- * Initializes the flyout.
295
- * @param {!WorkspaceSvg} targetWorkspace The workspace in which to
296
- * create new blocks.
297
- */
298
- Flyout.prototype.init = function(targetWorkspace) {
299
- this.targetWorkspace = targetWorkspace;
300
- this.workspace_.targetWorkspace = targetWorkspace;
301
-
302
- this.workspace_.scrollbar = new ScrollbarPair(
303
- this.workspace_, this.horizontalLayout, !this.horizontalLayout,
304
- 'blocklyFlyoutScrollbar', this.SCROLLBAR_MARGIN);
305
-
306
- this.hide();
307
-
308
- Array.prototype.push.apply(
309
- this.eventWrappers_,
310
- browserEvents.conditionalBind(
311
- this.svgGroup_, 'wheel', this, this.wheel_));
312
- if (!this.autoClose) {
313
- this.filterWrapper_ = this.filterForCapacity_.bind(this);
314
- this.targetWorkspace.addChangeListener(this.filterWrapper_);
493
+ updateDisplay_() {
494
+ let show = true;
495
+ if (!this.containerVisible_) {
496
+ show = false;
497
+ } else {
498
+ show = this.isVisible();
499
+ }
500
+ this.svgGroup_.style.display = show ? 'block' : 'none';
501
+ // Update the scrollbar's visibility too since it should mimic the
502
+ // flyout's visibility.
503
+ this.workspace_.scrollbar.setContainerVisible(show);
315
504
  }
316
505
 
317
- // Dragging the flyout up and down.
318
- Array.prototype.push.apply(
319
- this.eventWrappers_,
320
- browserEvents.conditionalBind(
321
- this.svgBackground_, 'mousedown', this, this.onMouseDown_));
322
-
323
- // A flyout connected to a workspace doesn't have its own current gesture.
324
- this.workspace_.getGesture =
325
- this.targetWorkspace.getGesture.bind(this.targetWorkspace);
326
-
327
- // Get variables from the main workspace rather than the target workspace.
328
- this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap());
329
-
330
- this.workspace_.createPotentialVariableMap();
331
-
332
- targetWorkspace.getComponentManager().addComponent({
333
- component: this,
334
- weight: 1,
335
- capabilities: [
336
- ComponentManager.Capability.DELETE_AREA,
337
- ComponentManager.Capability.DRAG_TARGET,
338
- ],
339
- });
340
- };
506
+ /**
507
+ * Update the view based on coordinates calculated in position().
508
+ * @param {number} width The computed width of the flyout's SVG group
509
+ * @param {number} height The computed height of the flyout's SVG group.
510
+ * @param {number} x The computed x origin of the flyout's SVG group.
511
+ * @param {number} y The computed y origin of the flyout's SVG group.
512
+ * @protected
513
+ */
514
+ positionAt_(width, height, x, y) {
515
+ this.svgGroup_.setAttribute('width', width);
516
+ this.svgGroup_.setAttribute('height', height);
517
+ this.workspace_.setCachedParentSvgSize(width, height);
518
+
519
+ if (this.svgGroup_.tagName === 'svg') {
520
+ const transform = 'translate(' + x + 'px,' + y + 'px)';
521
+ dom.setCssTransform(this.svgGroup_, transform);
522
+ } else {
523
+ // IE and Edge don't support CSS transforms on SVG elements so
524
+ // it's important to set the transform on the SVG element itself
525
+ const transform = 'translate(' + x + ',' + y + ')';
526
+ this.svgGroup_.setAttribute('transform', transform);
527
+ }
341
528
 
342
- /**
343
- * Dispose of this flyout.
344
- * Unlink from all DOM elements to prevent memory leaks.
345
- * @suppress {checkTypes}
346
- */
347
- Flyout.prototype.dispose = function() {
348
- this.hide();
349
- this.workspace_.getComponentManager().removeComponent(this.id);
350
- browserEvents.unbind(this.eventWrappers_);
351
- if (this.filterWrapper_) {
352
- this.targetWorkspace.removeChangeListener(this.filterWrapper_);
353
- this.filterWrapper_ = null;
354
- }
355
- if (this.workspace_) {
356
- this.workspace_.getThemeManager().unsubscribe(this.svgBackground_);
357
- this.workspace_.targetWorkspace = null;
358
- this.workspace_.dispose();
359
- this.workspace_ = null;
360
- }
361
- if (this.svgGroup_) {
362
- dom.removeNode(this.svgGroup_);
363
- this.svgGroup_ = null;
529
+ // Update the scrollbar (if one exists).
530
+ const scrollbar = this.workspace_.scrollbar;
531
+ if (scrollbar) {
532
+ // Set the scrollbars origin to be the top left of the flyout.
533
+ scrollbar.setOrigin(x, y);
534
+ scrollbar.resize();
535
+ // If origin changed and metrics haven't changed enough to trigger
536
+ // reposition in resize, we need to call setPosition. See issue #4692.
537
+ if (scrollbar.hScroll) {
538
+ scrollbar.hScroll.setPosition(
539
+ scrollbar.hScroll.position.x, scrollbar.hScroll.position.y);
540
+ }
541
+ if (scrollbar.vScroll) {
542
+ scrollbar.vScroll.setPosition(
543
+ scrollbar.vScroll.position.x, scrollbar.vScroll.position.y);
544
+ }
545
+ }
364
546
  }
365
- this.svgBackground_ = null;
366
- this.targetWorkspace = null;
367
- };
368
-
369
- /**
370
- * Get the width of the flyout.
371
- * @return {number} The width of the flyout.
372
- */
373
- Flyout.prototype.getWidth = function() {
374
- return this.width_;
375
- };
376
-
377
- /**
378
- * Get the height of the flyout.
379
- * @return {number} The width of the flyout.
380
- */
381
- Flyout.prototype.getHeight = function() {
382
- return this.height_;
383
- };
384
547
 
385
- /**
386
- * Get the scale (zoom level) of the flyout. By default,
387
- * this matches the target workspace scale, but this can be overridden.
388
- * @return {number} Flyout workspace scale.
389
- */
390
- Flyout.prototype.getFlyoutScale = function() {
391
- return this.targetWorkspace.scale;
392
- };
548
+ /**
549
+ * Hide and empty the flyout.
550
+ */
551
+ hide() {
552
+ if (!this.isVisible()) {
553
+ return;
554
+ }
555
+ this.setVisible(false);
556
+ // Delete all the event listeners.
557
+ for (let i = 0, listen; (listen = this.listeners_[i]); i++) {
558
+ browserEvents.unbind(listen);
559
+ }
560
+ this.listeners_.length = 0;
561
+ if (this.reflowWrapper_) {
562
+ this.workspace_.removeChangeListener(this.reflowWrapper_);
563
+ this.reflowWrapper_ = null;
564
+ }
565
+ // Do NOT delete the blocks here. Wait until Flyout.show.
566
+ // https://neil.fraser.name/news/2014/08/09/
567
+ }
393
568
 
394
- /**
395
- * Get the workspace inside the flyout.
396
- * @return {!WorkspaceSvg} The workspace inside the flyout.
397
- * @package
398
- */
399
- Flyout.prototype.getWorkspace = function() {
400
- return this.workspace_;
401
- };
569
+ /**
570
+ * Show and populate the flyout.
571
+ * @param {!toolbox.FlyoutDefinition|string} flyoutDef Contents to display
572
+ * in the flyout. This is either an array of Nodes, a NodeList, a
573
+ * toolbox definition, or a string with the name of the dynamic category.
574
+ */
575
+ show(flyoutDef) {
576
+ this.workspace_.setResizesEnabled(false);
577
+ this.hide();
578
+ this.clearOldBlocks_();
402
579
 
403
- /**
404
- * Is the flyout visible?
405
- * @return {boolean} True if visible.
406
- */
407
- Flyout.prototype.isVisible = function() {
408
- return this.isVisible_;
409
- };
580
+ // Handle dynamic categories, represented by a name instead of a list.
581
+ if (typeof flyoutDef === 'string') {
582
+ flyoutDef = this.getDynamicCategoryContents_(flyoutDef);
583
+ }
584
+ this.setVisible(true);
585
+
586
+ // Parse the Array, Node or NodeList into a a list of flyout items.
587
+ const parsedContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef);
588
+ const flyoutInfo =
589
+ /** @type {{contents:!Array<!Object>, gaps:!Array<number>}} */ (
590
+ this.createFlyoutInfo_(parsedContent));
591
+
592
+ this.layout_(flyoutInfo.contents, flyoutInfo.gaps);
593
+
594
+ // IE 11 is an incompetent browser that fails to fire mouseout events.
595
+ // When the mouse is over the background, deselect all blocks.
596
+ const deselectAll =
597
+ /** @this {Flyout} */
598
+ function() {
599
+ const topBlocks = this.workspace_.getTopBlocks(false);
600
+ for (let i = 0, block; (block = topBlocks[i]); i++) {
601
+ block.removeSelect();
602
+ }
603
+ };
604
+
605
+ this.listeners_.push(browserEvents.conditionalBind(
606
+ /** @type {!SVGPathElement} */ (this.svgBackground_), 'mouseover', this,
607
+ deselectAll));
608
+
609
+ if (this.horizontalLayout) {
610
+ this.height_ = 0;
611
+ } else {
612
+ this.width_ = 0;
613
+ }
614
+ this.workspace_.setResizesEnabled(true);
615
+ this.reflow();
410
616
 
411
- /**
412
- * Set whether the flyout is visible. A value of true does not necessarily mean
413
- * that the flyout is shown. It could be hidden because its container is hidden.
414
- * @param {boolean} visible True if visible.
415
- */
416
- Flyout.prototype.setVisible = function(visible) {
417
- const visibilityChanged = (visible !== this.isVisible());
617
+ this.filterForCapacity_();
418
618
 
419
- this.isVisible_ = visible;
420
- if (visibilityChanged) {
421
- if (!this.autoClose) {
422
- // Auto-close flyouts are ignored as drag targets, so only non auto-close
423
- // flyouts need to have their drag target updated.
424
- this.workspace_.recordDragTargets();
425
- }
426
- this.updateDisplay_();
427
- }
428
- };
619
+ // Correctly position the flyout's scrollbar when it opens.
620
+ this.position();
429
621
 
430
- /**
431
- * Set whether this flyout's container is visible.
432
- * @param {boolean} visible Whether the container is visible.
433
- */
434
- Flyout.prototype.setContainerVisible = function(visible) {
435
- const visibilityChanged = (visible !== this.containerVisible_);
436
- this.containerVisible_ = visible;
437
- if (visibilityChanged) {
438
- this.updateDisplay_();
622
+ this.reflowWrapper_ = this.reflow.bind(this);
623
+ this.workspace_.addChangeListener(this.reflowWrapper_);
624
+ this.emptyRecycledBlocks_();
439
625
  }
440
- };
441
626
 
442
- /**
443
- * Update the display property of the flyout based whether it thinks it should
444
- * be visible and whether its containing workspace is visible.
445
- * @private
446
- */
447
- Flyout.prototype.updateDisplay_ = function() {
448
- let show = true;
449
- if (!this.containerVisible_) {
450
- show = false;
451
- } else {
452
- show = this.isVisible();
453
- }
454
- this.svgGroup_.style.display = show ? 'block' : 'none';
455
- // Update the scrollbar's visibility too since it should mimic the
456
- // flyout's visibility.
457
- this.workspace_.scrollbar.setContainerVisible(show);
458
- };
627
+ /**
628
+ * Create the contents array and gaps array necessary to create the layout for
629
+ * the flyout.
630
+ * @param {!toolbox.FlyoutItemInfoArray} parsedContent The array
631
+ * of objects to show in the flyout.
632
+ * @return {{contents:Array<Object>, gaps:Array<number>}} The list of contents
633
+ * and gaps needed to lay out the flyout.
634
+ * @private
635
+ */
636
+ createFlyoutInfo_(parsedContent) {
637
+ const contents = [];
638
+ const gaps = [];
639
+ this.permanentlyDisabled_.length = 0;
640
+ const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
641
+ for (let i = 0, contentInfo; (contentInfo = parsedContent[i]); i++) {
642
+ if (contentInfo['custom']) {
643
+ const customInfo =
644
+ /** @type {!toolbox.DynamicCategoryInfo} */ (contentInfo);
645
+ const categoryName = customInfo['custom'];
646
+ const flyoutDef = this.getDynamicCategoryContents_(categoryName);
647
+ const parsedDynamicContent = /** @type {!toolbox.FlyoutItemInfoArray} */
648
+ (toolbox.convertFlyoutDefToJsonArray(flyoutDef));
649
+ // Replace the element at i with the dynamic content it represents.
650
+ parsedContent.splice.apply(
651
+ parsedContent, [i, 1].concat(parsedDynamicContent));
652
+ contentInfo = parsedContent[i];
653
+ }
459
654
 
460
- /**
461
- * Update the view based on coordinates calculated in position().
462
- * @param {number} width The computed width of the flyout's SVG group
463
- * @param {number} height The computed height of the flyout's SVG group.
464
- * @param {number} x The computed x origin of the flyout's SVG group.
465
- * @param {number} y The computed y origin of the flyout's SVG group.
466
- * @protected
467
- */
468
- Flyout.prototype.positionAt_ = function(width, height, x, y) {
469
- this.svgGroup_.setAttribute('width', width);
470
- this.svgGroup_.setAttribute('height', height);
471
- this.workspace_.setCachedParentSvgSize(width, height);
472
-
473
- if (this.svgGroup_.tagName === 'svg') {
474
- const transform = 'translate(' + x + 'px,' + y + 'px)';
475
- dom.setCssTransform(this.svgGroup_, transform);
476
- } else {
477
- // IE and Edge don't support CSS transforms on SVG elements so
478
- // it's important to set the transform on the SVG element itself
479
- const transform = 'translate(' + x + ',' + y + ')';
480
- this.svgGroup_.setAttribute('transform', transform);
655
+ switch (contentInfo['kind'].toUpperCase()) {
656
+ case 'BLOCK': {
657
+ const blockInfo = /** @type {!toolbox.BlockInfo} */ (contentInfo);
658
+ const block = this.createFlyoutBlock_(blockInfo);
659
+ contents.push({type: 'block', block: block});
660
+ this.addBlockGap_(blockInfo, gaps, defaultGap);
661
+ break;
662
+ }
663
+ case 'SEP': {
664
+ const sepInfo = /** @type {!toolbox.SeparatorInfo} */ (contentInfo);
665
+ this.addSeparatorGap_(sepInfo, gaps, defaultGap);
666
+ break;
667
+ }
668
+ case 'LABEL': {
669
+ const labelInfo = /** @type {!toolbox.LabelInfo} */ (contentInfo);
670
+ // A label is a button with different styling.
671
+ const label = this.createButton_(labelInfo, /** isLabel */ true);
672
+ contents.push({type: 'button', button: label});
673
+ gaps.push(defaultGap);
674
+ break;
675
+ }
676
+ case 'BUTTON': {
677
+ const buttonInfo = /** @type {!toolbox.ButtonInfo} */ (contentInfo);
678
+ const button = this.createButton_(buttonInfo, /** isLabel */ false);
679
+ contents.push({type: 'button', button: button});
680
+ gaps.push(defaultGap);
681
+ break;
682
+ }
683
+ }
684
+ }
685
+ return {contents: contents, gaps: gaps};
481
686
  }
482
687
 
483
- // Update the scrollbar (if one exists).
484
- const scrollbar = this.workspace_.scrollbar;
485
- if (scrollbar) {
486
- // Set the scrollbars origin to be the top left of the flyout.
487
- scrollbar.setOrigin(x, y);
488
- scrollbar.resize();
489
- // If origin changed and metrics haven't changed enough to trigger
490
- // reposition in resize, we need to call setPosition. See issue #4692.
491
- if (scrollbar.hScroll) {
492
- scrollbar.hScroll.setPosition(
493
- scrollbar.hScroll.position.x, scrollbar.hScroll.position.y);
494
- }
495
- if (scrollbar.vScroll) {
496
- scrollbar.vScroll.setPosition(
497
- scrollbar.vScroll.position.x, scrollbar.vScroll.position.y);
688
+ /**
689
+ * Gets the flyout definition for the dynamic category.
690
+ * @param {string} categoryName The name of the dynamic category.
691
+ * @return {!toolbox.FlyoutDefinition} The definition of the
692
+ * flyout in one of its many forms.
693
+ * @private
694
+ */
695
+ getDynamicCategoryContents_(categoryName) {
696
+ // Look up the correct category generation function and call that to get a
697
+ // valid XML list.
698
+ const fnToApply =
699
+ this.workspace_.targetWorkspace.getToolboxCategoryCallback(
700
+ categoryName);
701
+ if (typeof fnToApply !== 'function') {
702
+ throw TypeError(
703
+ 'Couldn\'t find a callback function when opening' +
704
+ ' a toolbox category.');
498
705
  }
706
+ return fnToApply(this.workspace_.targetWorkspace);
499
707
  }
500
- };
501
708
 
502
- /**
503
- * Hide and empty the flyout.
504
- */
505
- Flyout.prototype.hide = function() {
506
- if (!this.isVisible()) {
507
- return;
508
- }
509
- this.setVisible(false);
510
- // Delete all the event listeners.
511
- for (let i = 0, listen; (listen = this.listeners_[i]); i++) {
512
- browserEvents.unbind(listen);
513
- }
514
- this.listeners_.length = 0;
515
- if (this.reflowWrapper_) {
516
- this.workspace_.removeChangeListener(this.reflowWrapper_);
517
- this.reflowWrapper_ = null;
709
+ /**
710
+ * Creates a flyout button or a flyout label.
711
+ * @param {!toolbox.ButtonOrLabelInfo} btnInfo
712
+ * The object holding information about a button or a label.
713
+ * @param {boolean} isLabel True if the button is a label, false otherwise.
714
+ * @return {!FlyoutButton} The object used to display the button in the
715
+ * flyout.
716
+ * @private
717
+ */
718
+ createButton_(btnInfo, isLabel) {
719
+ const {FlyoutButton} = goog.module.get('Blockly.FlyoutButton');
720
+ if (!FlyoutButton) {
721
+ throw Error('Missing require for Blockly.FlyoutButton');
722
+ }
723
+ const curButton = new FlyoutButton(
724
+ this.workspace_,
725
+ /** @type {!WorkspaceSvg} */ (this.targetWorkspace), btnInfo, isLabel);
726
+ return curButton;
518
727
  }
519
- // Do NOT delete the blocks here. Wait until Flyout.show.
520
- // https://neil.fraser.name/news/2014/08/09/
521
- };
522
728
 
523
- /**
524
- * Show and populate the flyout.
525
- * @param {!toolbox.FlyoutDefinition|string} flyoutDef Contents to display
526
- * in the flyout. This is either an array of Nodes, a NodeList, a
527
- * toolbox definition, or a string with the name of the dynamic category.
528
- */
529
- Flyout.prototype.show = function(flyoutDef) {
530
- this.workspace_.setResizesEnabled(false);
531
- this.hide();
532
- this.clearOldBlocks_();
533
-
534
- // Handle dynamic categories, represented by a name instead of a list.
535
- if (typeof flyoutDef === 'string') {
536
- flyoutDef = this.getDynamicCategoryContents_(flyoutDef);
537
- }
538
- this.setVisible(true);
539
-
540
- // Parse the Array, Node or NodeList into a a list of flyout items.
541
- const parsedContent = toolbox.convertFlyoutDefToJsonArray(flyoutDef);
542
- const flyoutInfo =
543
- /** @type {{contents:!Array<!Object>, gaps:!Array<number>}} */ (
544
- this.createFlyoutInfo_(parsedContent));
545
-
546
- this.layout_(flyoutInfo.contents, flyoutInfo.gaps);
547
-
548
- // IE 11 is an incompetent browser that fails to fire mouseout events.
549
- // When the mouse is over the background, deselect all blocks.
550
- const deselectAll =
551
- /** @this {Flyout} */
552
- function() {
553
- const topBlocks = this.workspace_.getTopBlocks(false);
554
- for (let i = 0, block; (block = topBlocks[i]); i++) {
555
- block.removeSelect();
729
+ /**
730
+ * Create a block from the xml and permanently disable any blocks that were
731
+ * defined as disabled.
732
+ * @param {!toolbox.BlockInfo} blockInfo The info of the block.
733
+ * @return {!BlockSvg} The block created from the blockInfo.
734
+ * @private
735
+ */
736
+ createFlyoutBlock_(blockInfo) {
737
+ let block;
738
+ if (blockInfo['blockxml']) {
739
+ const xml = typeof blockInfo['blockxml'] === 'string' ?
740
+ Xml.textToDom(blockInfo['blockxml']) :
741
+ blockInfo['blockxml'];
742
+ block = this.getRecycledBlock_(xml.getAttribute('type'));
743
+ if (!block) {
744
+ block = Xml.domToBlock(xml, this.workspace_);
745
+ }
746
+ } else {
747
+ block = this.getRecycledBlock_(blockInfo['type']);
748
+ if (!block) {
749
+ if (blockInfo['enabled'] === undefined) {
750
+ blockInfo['enabled'] = blockInfo['disabled'] !== 'true' &&
751
+ blockInfo['disabled'] !== true;
556
752
  }
557
- };
558
-
559
- this.listeners_.push(browserEvents.conditionalBind(
560
- this.svgBackground_, 'mouseover', this, deselectAll));
561
-
562
- if (this.horizontalLayout) {
563
- this.height_ = 0;
564
- } else {
565
- this.width_ = 0;
566
- }
567
- this.workspace_.setResizesEnabled(true);
568
- this.reflow();
569
-
570
- this.filterForCapacity_();
571
-
572
- // Correctly position the flyout's scrollbar when it opens.
573
- this.position();
574
-
575
- this.reflowWrapper_ = this.reflow.bind(this);
576
- this.workspace_.addChangeListener(this.reflowWrapper_);
577
- this.emptyRecycledBlocks_();
578
- };
753
+ block = blocks.append(
754
+ /** @type {blocks.State} */ (blockInfo), this.workspace_);
755
+ }
756
+ }
579
757
 
580
- /**
581
- * Create the contents array and gaps array necessary to create the layout for
582
- * the flyout.
583
- * @param {!toolbox.FlyoutItemInfoArray} parsedContent The array
584
- * of objects to show in the flyout.
585
- * @return {{contents:Array<Object>, gaps:Array<number>}} The list of contents
586
- * and gaps needed to lay out the flyout.
587
- * @private
588
- */
589
- Flyout.prototype.createFlyoutInfo_ = function(parsedContent) {
590
- const contents = [];
591
- const gaps = [];
592
- this.permanentlyDisabled_.length = 0;
593
- const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
594
- for (let i = 0, contentInfo; (contentInfo = parsedContent[i]); i++) {
595
- if (contentInfo['custom']) {
596
- const customInfo =
597
- /** @type {!toolbox.DynamicCategoryInfo} */ (contentInfo);
598
- const categoryName = customInfo['custom'];
599
- const flyoutDef = this.getDynamicCategoryContents_(categoryName);
600
- const parsedDynamicContent = /** @type {!toolbox.FlyoutItemInfoArray} */
601
- (toolbox.convertFlyoutDefToJsonArray(flyoutDef));
602
- // Replace the element at i with the dynamic content it represents.
603
- parsedContent.splice.apply(
604
- parsedContent, [i, 1].concat(parsedDynamicContent));
605
- contentInfo = parsedContent[i];
758
+ if (!block.isEnabled()) {
759
+ // Record blocks that were initially disabled.
760
+ // Do not enable these blocks as a result of capacity filtering.
761
+ this.permanentlyDisabled_.push(block);
606
762
  }
763
+ return /** @type {!BlockSvg} */ (block);
764
+ }
607
765
 
608
- switch (contentInfo['kind'].toUpperCase()) {
609
- case 'BLOCK': {
610
- const blockInfo = /** @type {!toolbox.BlockInfo} */ (contentInfo);
611
- const block = this.createFlyoutBlock_(blockInfo);
612
- contents.push({type: 'block', block: block});
613
- this.addBlockGap_(blockInfo, gaps, defaultGap);
614
- break;
615
- }
616
- case 'SEP': {
617
- const sepInfo = /** @type {!toolbox.SeparatorInfo} */ (contentInfo);
618
- this.addSeparatorGap_(sepInfo, gaps, defaultGap);
619
- break;
620
- }
621
- case 'LABEL': {
622
- const labelInfo = /** @type {!toolbox.LabelInfo} */ (contentInfo);
623
- // A label is a button with different styling.
624
- const label = this.createButton_(labelInfo, /** isLabel */ true);
625
- contents.push({type: 'button', button: label});
626
- gaps.push(defaultGap);
627
- break;
628
- }
629
- case 'BUTTON': {
630
- const buttonInfo = /** @type {!toolbox.ButtonInfo} */ (contentInfo);
631
- const button = this.createButton_(buttonInfo, /** isLabel */ false);
632
- contents.push({type: 'button', button: button});
633
- gaps.push(defaultGap);
766
+ /**
767
+ * Returns a block from the array of recycled blocks with the given type, or
768
+ * undefined if one cannot be found.
769
+ * @param {string} blockType The type of the block to try to recycle.
770
+ * @return {(!BlockSvg|undefined)} The recycled block, or undefined if
771
+ * one could not be recycled.
772
+ * @private
773
+ */
774
+ getRecycledBlock_(blockType) {
775
+ let index = -1;
776
+ for (let i = 0; i < this.recycledBlocks_.length; i++) {
777
+ if (this.recycledBlocks_[i].type === blockType) {
778
+ index = i;
634
779
  break;
635
780
  }
636
781
  }
782
+ return index === -1 ? undefined : this.recycledBlocks_.splice(index, 1)[0];
637
783
  }
638
- return {contents: contents, gaps: gaps};
639
- };
640
784
 
641
- /**
642
- * Gets the flyout definition for the dynamic category.
643
- * @param {string} categoryName The name of the dynamic category.
644
- * @return {!toolbox.FlyoutDefinition} The definition of the
645
- * flyout in one of its many forms.
646
- * @private
647
- */
648
- Flyout.prototype.getDynamicCategoryContents_ = function(categoryName) {
649
- // Look up the correct category generation function and call that to get a
650
- // valid XML list.
651
- const fnToApply =
652
- this.workspace_.targetWorkspace.getToolboxCategoryCallback(categoryName);
653
- if (typeof fnToApply !== 'function') {
654
- throw TypeError(
655
- 'Couldn\'t find a callback function when opening' +
656
- ' a toolbox category.');
785
+ /**
786
+ * Adds a gap in the flyout based on block info.
787
+ * @param {!toolbox.BlockInfo} blockInfo Information about a block.
788
+ * @param {!Array<number>} gaps The list of gaps between items in the flyout.
789
+ * @param {number} defaultGap The default gap between one element and the
790
+ * next.
791
+ * @private
792
+ */
793
+ addBlockGap_(blockInfo, gaps, defaultGap) {
794
+ let gap;
795
+ if (blockInfo['gap']) {
796
+ gap = parseInt(blockInfo['gap'], 10);
797
+ } else if (blockInfo['blockxml']) {
798
+ const xml = typeof blockInfo['blockxml'] === 'string' ?
799
+ Xml.textToDom(blockInfo['blockxml']) :
800
+ blockInfo['blockxml'];
801
+ gap = parseInt(xml.getAttribute('gap'), 10);
802
+ }
803
+ gaps.push(isNaN(gap) ? defaultGap : gap);
657
804
  }
658
- return fnToApply(this.workspace_.targetWorkspace);
659
- };
660
805
 
661
- /**
662
- * Creates a flyout button or a flyout label.
663
- * @param {!toolbox.ButtonOrLabelInfo} btnInfo
664
- * The object holding information about a button or a label.
665
- * @param {boolean} isLabel True if the button is a label, false otherwise.
666
- * @return {!FlyoutButton} The object used to display the button in the
667
- * flyout.
668
- * @private
669
- */
670
- Flyout.prototype.createButton_ = function(btnInfo, isLabel) {
671
- const {FlyoutButton} = goog.module.get('Blockly.FlyoutButton');
672
- if (!FlyoutButton) {
673
- throw Error('Missing require for Blockly.FlyoutButton');
806
+ /**
807
+ * Add the necessary gap in the flyout for a separator.
808
+ * @param {!toolbox.SeparatorInfo} sepInfo The object holding
809
+ * information about a separator.
810
+ * @param {!Array<number>} gaps The list gaps between items in the flyout.
811
+ * @param {number} defaultGap The default gap between the button and next
812
+ * element.
813
+ * @private
814
+ */
815
+ addSeparatorGap_(sepInfo, gaps, defaultGap) {
816
+ // Change the gap between two toolbox elements.
817
+ // <sep gap="36"></sep>
818
+ // The default gap is 24, can be set larger or smaller.
819
+ // This overwrites the gap attribute on the previous element.
820
+ const newGap = parseInt(sepInfo['gap'], 10);
821
+ // Ignore gaps before the first block.
822
+ if (!isNaN(newGap) && gaps.length > 0) {
823
+ gaps[gaps.length - 1] = newGap;
824
+ } else {
825
+ gaps.push(defaultGap);
826
+ }
674
827
  }
675
- const curButton = new FlyoutButton(
676
- this.workspace_,
677
- /** @type {!WorkspaceSvg} */ (this.targetWorkspace), btnInfo, isLabel);
678
- return curButton;
679
- };
680
828
 
681
- /**
682
- * Create a block from the xml and permanently disable any blocks that were
683
- * defined as disabled.
684
- * @param {!toolbox.BlockInfo} blockInfo The info of the block.
685
- * @return {!BlockSvg} The block created from the blockInfo.
686
- * @private
687
- */
688
- Flyout.prototype.createFlyoutBlock_ = function(blockInfo) {
689
- let block;
690
- if (blockInfo['blockxml']) {
691
- const xml = typeof blockInfo['blockxml'] === 'string' ?
692
- Xml.textToDom(blockInfo['blockxml']) :
693
- blockInfo['blockxml'];
694
- block = this.getRecycledBlock_(xml.getAttribute('type'));
695
- if (!block) {
696
- block = Xml.domToBlock(xml, this.workspace_);
829
+ /**
830
+ * Delete blocks, mats and buttons from a previous showing of the flyout.
831
+ * @private
832
+ */
833
+ clearOldBlocks_() {
834
+ // Delete any blocks from a previous showing.
835
+ const oldBlocks = this.workspace_.getTopBlocks(false);
836
+ for (let i = 0, block; (block = oldBlocks[i]); i++) {
837
+ if (this.blockIsRecyclable_(block)) {
838
+ this.recycleBlock_(block);
839
+ } else {
840
+ block.dispose(false, false);
841
+ }
697
842
  }
698
- } else {
699
- block = this.getRecycledBlock_(blockInfo['type']);
700
- if (!block) {
701
- if (blockInfo['enabled'] === undefined) {
702
- blockInfo['enabled'] =
703
- blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
843
+ // Delete any mats from a previous showing.
844
+ for (let j = 0; j < this.mats_.length; j++) {
845
+ const rect = this.mats_[j];
846
+ if (rect) {
847
+ Tooltip.unbindMouseEvents(rect);
848
+ dom.removeNode(rect);
704
849
  }
705
- block = blocks.append(
706
- /** @type {blocks.State} */ (blockInfo), this.workspace_);
707
850
  }
708
- }
851
+ this.mats_.length = 0;
852
+ // Delete any buttons from a previous showing.
853
+ for (let i = 0, button; (button = this.buttons_[i]); i++) {
854
+ button.dispose();
855
+ }
856
+ this.buttons_.length = 0;
709
857
 
710
- if (!block.isEnabled()) {
711
- // Record blocks that were initially disabled.
712
- // Do not enable these blocks as a result of capacity filtering.
713
- this.permanentlyDisabled_.push(block);
858
+ // Clear potential variables from the previous showing.
859
+ this.workspace_.getPotentialVariableMap().clear();
714
860
  }
715
- return /** @type {!BlockSvg} */ (block);
716
- };
717
861
 
718
- /**
719
- * Returns a block from the array of recycled blocks with the given type, or
720
- * undefined if one cannot be found.
721
- * @param {string} blockType The type of the block to try to recycle.
722
- * @return {(!BlockSvg|undefined)} The recycled block, or undefined if
723
- * one could not be recycled.
724
- * @private
725
- */
726
- Flyout.prototype.getRecycledBlock_ = function(blockType) {
727
- let index = -1;
728
- for (let i = 0; i < this.recycledBlocks_.length; i++) {
729
- if (this.recycledBlocks_[i].type === blockType) {
730
- index = i;
731
- break;
862
+ /**
863
+ * Empties all of the recycled blocks, properly disposing of them.
864
+ * @private
865
+ */
866
+ emptyRecycledBlocks_() {
867
+ for (let i = 0; i < this.recycledBlocks_.length; i++) {
868
+ this.recycledBlocks_[i].dispose();
732
869
  }
870
+ this.recycledBlocks_ = [];
733
871
  }
734
- return index === -1 ? undefined : this.recycledBlocks_.splice(index, 1)[0];
735
- };
736
872
 
737
- /**
738
- * Adds a gap in the flyout based on block info.
739
- * @param {!toolbox.BlockInfo} blockInfo Information about a block.
740
- * @param {!Array<number>} gaps The list of gaps between items in the flyout.
741
- * @param {number} defaultGap The default gap between one element and the next.
742
- * @private
743
- */
744
- Flyout.prototype.addBlockGap_ = function(blockInfo, gaps, defaultGap) {
745
- let gap;
746
- if (blockInfo['gap']) {
747
- gap = parseInt(blockInfo['gap'], 10);
748
- } else if (blockInfo['blockxml']) {
749
- const xml = typeof blockInfo['blockxml'] === 'string' ?
750
- Xml.textToDom(blockInfo['blockxml']) :
751
- blockInfo['blockxml'];
752
- gap = parseInt(xml.getAttribute('gap'), 10);
873
+ /**
874
+ * Returns whether the given block can be recycled or not.
875
+ * @param {!BlockSvg} _block The block to check for recyclability.
876
+ * @return {boolean} True if the block can be recycled. False otherwise.
877
+ * @protected
878
+ */
879
+ blockIsRecyclable_(_block) {
880
+ // By default, recycling is disabled.
881
+ return false;
753
882
  }
754
- gaps.push(isNaN(gap) ? defaultGap : gap);
755
- };
756
883
 
757
- /**
758
- * Add the necessary gap in the flyout for a separator.
759
- * @param {!toolbox.SeparatorInfo} sepInfo The object holding
760
- * information about a separator.
761
- * @param {!Array<number>} gaps The list gaps between items in the flyout.
762
- * @param {number} defaultGap The default gap between the button and next
763
- * element.
764
- * @private
765
- */
766
- Flyout.prototype.addSeparatorGap_ = function(sepInfo, gaps, defaultGap) {
767
- // Change the gap between two toolbox elements.
768
- // <sep gap="36"></sep>
769
- // The default gap is 24, can be set larger or smaller.
770
- // This overwrites the gap attribute on the previous element.
771
- const newGap = parseInt(sepInfo['gap'], 10);
772
- // Ignore gaps before the first block.
773
- if (!isNaN(newGap) && gaps.length > 0) {
774
- gaps[gaps.length - 1] = newGap;
775
- } else {
776
- gaps.push(defaultGap);
884
+ /**
885
+ * Puts a previously created block into the recycle bin and moves it to the
886
+ * top of the workspace. Used during large workspace swaps to limit the number
887
+ * of new DOM elements we need to create.
888
+ * @param {!BlockSvg} block The block to recycle.
889
+ * @private
890
+ */
891
+ recycleBlock_(block) {
892
+ const xy = block.getRelativeToSurfaceXY();
893
+ block.moveBy(-xy.x, -xy.y);
894
+ this.recycledBlocks_.push(block);
777
895
  }
778
- };
779
896
 
780
- /**
781
- * Delete blocks, mats and buttons from a previous showing of the flyout.
782
- * @private
783
- */
784
- Flyout.prototype.clearOldBlocks_ = function() {
785
- // Delete any blocks from a previous showing.
786
- const oldBlocks = this.workspace_.getTopBlocks(false);
787
- for (let i = 0, block; (block = oldBlocks[i]); i++) {
788
- if (this.blockIsRecyclable_(block)) {
789
- this.recycleBlock_(block);
790
- } else {
791
- block.dispose(false, false);
792
- }
793
- }
794
- // Delete any mats from a previous showing.
795
- for (let j = 0; j < this.mats_.length; j++) {
796
- const rect = this.mats_[j];
797
- if (rect) {
798
- Tooltip.unbindMouseEvents(rect);
799
- dom.removeNode(rect);
800
- }
801
- }
802
- this.mats_.length = 0;
803
- // Delete any buttons from a previous showing.
804
- for (let i = 0, button; (button = this.buttons_[i]); i++) {
805
- button.dispose();
897
+ /**
898
+ * Add listeners to a block that has been added to the flyout.
899
+ * @param {!SVGElement} root The root node of the SVG group the block is in.
900
+ * @param {!BlockSvg} block The block to add listeners for.
901
+ * @param {!SVGElement} rect The invisible rectangle under the block that acts
902
+ * as a mat for that block.
903
+ * @protected
904
+ */
905
+ addBlockListeners_(root, block, rect) {
906
+ this.listeners_.push(browserEvents.conditionalBind(
907
+ root, 'mousedown', null, this.blockMouseDown_(block)));
908
+ this.listeners_.push(browserEvents.conditionalBind(
909
+ rect, 'mousedown', null, this.blockMouseDown_(block)));
910
+ this.listeners_.push(
911
+ browserEvents.bind(root, 'mouseenter', block, block.addSelect));
912
+ this.listeners_.push(
913
+ browserEvents.bind(root, 'mouseleave', block, block.removeSelect));
914
+ this.listeners_.push(
915
+ browserEvents.bind(rect, 'mouseenter', block, block.addSelect));
916
+ this.listeners_.push(
917
+ browserEvents.bind(rect, 'mouseleave', block, block.removeSelect));
806
918
  }
807
- this.buttons_.length = 0;
808
919
 
809
- // Clear potential variables from the previous showing.
810
- this.workspace_.getPotentialVariableMap().clear();
811
- };
812
-
813
- /**
814
- * Empties all of the recycled blocks, properly disposing of them.
815
- * @private
816
- */
817
- Flyout.prototype.emptyRecycledBlocks_ = function() {
818
- for (let i = 0; i < this.recycledBlocks_.length; i++) {
819
- this.recycledBlocks_[i].dispose();
920
+ /**
921
+ * Handle a mouse-down on an SVG block in a non-closing flyout.
922
+ * @param {!BlockSvg} block The flyout block to copy.
923
+ * @return {!Function} Function to call when block is clicked.
924
+ * @private
925
+ */
926
+ blockMouseDown_(block) {
927
+ const flyout = this;
928
+ return function(e) {
929
+ const gesture = flyout.targetWorkspace.getGesture(e);
930
+ if (gesture) {
931
+ gesture.setStartBlock(block);
932
+ gesture.handleFlyoutStart(e, flyout);
933
+ }
934
+ };
820
935
  }
821
- this.recycledBlocks_ = [];
822
- };
823
-
824
- /**
825
- * Returns whether the given block can be recycled or not.
826
- * @param {!BlockSvg} _block The block to check for recyclability.
827
- * @return {boolean} True if the block can be recycled. False otherwise.
828
- * @protected
829
- */
830
- Flyout.prototype.blockIsRecyclable_ = function(_block) {
831
- // By default, recycling is disabled.
832
- return false;
833
- };
834
-
835
- /**
836
- * Puts a previously created block into the recycle bin and moves it to the
837
- * top of the workspace. Used during large workspace swaps to limit the number
838
- * of new DOM elements we need to create.
839
- * @param {!BlockSvg} block The block to recycle.
840
- * @private
841
- */
842
- Flyout.prototype.recycleBlock_ = function(block) {
843
- const xy = block.getRelativeToSurfaceXY();
844
- block.moveBy(-xy.x, -xy.y);
845
- this.recycledBlocks_.push(block);
846
- };
847
-
848
- /**
849
- * Add listeners to a block that has been added to the flyout.
850
- * @param {!SVGElement} root The root node of the SVG group the block is in.
851
- * @param {!BlockSvg} block The block to add listeners for.
852
- * @param {!SVGElement} rect The invisible rectangle under the block that acts
853
- * as a mat for that block.
854
- * @protected
855
- */
856
- Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
857
- this.listeners_.push(browserEvents.conditionalBind(
858
- root, 'mousedown', null, this.blockMouseDown_(block)));
859
- this.listeners_.push(browserEvents.conditionalBind(
860
- rect, 'mousedown', null, this.blockMouseDown_(block)));
861
- this.listeners_.push(
862
- browserEvents.bind(root, 'mouseenter', block, block.addSelect));
863
- this.listeners_.push(
864
- browserEvents.bind(root, 'mouseleave', block, block.removeSelect));
865
- this.listeners_.push(
866
- browserEvents.bind(rect, 'mouseenter', block, block.addSelect));
867
- this.listeners_.push(
868
- browserEvents.bind(rect, 'mouseleave', block, block.removeSelect));
869
- };
870
936
 
871
- /**
872
- * Handle a mouse-down on an SVG block in a non-closing flyout.
873
- * @param {!BlockSvg} block The flyout block to copy.
874
- * @return {!Function} Function to call when block is clicked.
875
- * @private
876
- */
877
- Flyout.prototype.blockMouseDown_ = function(block) {
878
- const flyout = this;
879
- return function(e) {
880
- const gesture = flyout.targetWorkspace.getGesture(e);
937
+ /**
938
+ * Mouse down on the flyout background. Start a vertical scroll drag.
939
+ * @param {!Event} e Mouse down event.
940
+ * @private
941
+ */
942
+ onMouseDown_(e) {
943
+ const gesture = this.targetWorkspace.getGesture(e);
881
944
  if (gesture) {
882
- gesture.setStartBlock(block);
883
- gesture.handleFlyoutStart(e, flyout);
945
+ gesture.handleFlyoutStart(e, this);
884
946
  }
885
- };
886
- };
947
+ }
887
948
 
888
- /**
889
- * Mouse down on the flyout background. Start a vertical scroll drag.
890
- * @param {!Event} e Mouse down event.
891
- * @private
892
- */
893
- Flyout.prototype.onMouseDown_ = function(e) {
894
- const gesture = this.targetWorkspace.getGesture(e);
895
- if (gesture) {
896
- gesture.handleFlyoutStart(e, this);
949
+ /**
950
+ * Does this flyout allow you to create a new instance of the given block?
951
+ * Used for deciding if a block can be "dragged out of" the flyout.
952
+ * @param {!BlockSvg} block The block to copy from the flyout.
953
+ * @return {boolean} True if you can create a new instance of the block, false
954
+ * otherwise.
955
+ * @package
956
+ */
957
+ isBlockCreatable_(block) {
958
+ return block.isEnabled();
897
959
  }
898
- };
899
960
 
900
- /**
901
- * Does this flyout allow you to create a new instance of the given block?
902
- * Used for deciding if a block can be "dragged out of" the flyout.
903
- * @param {!BlockSvg} block The block to copy from the flyout.
904
- * @return {boolean} True if you can create a new instance of the block, false
905
- * otherwise.
906
- * @package
907
- */
908
- Flyout.prototype.isBlockCreatable_ = function(block) {
909
- return block.isEnabled();
910
- };
961
+ /**
962
+ * Create a copy of this block on the workspace.
963
+ * @param {!BlockSvg} originalBlock The block to copy from the flyout.
964
+ * @return {!BlockSvg} The newly created block.
965
+ * @throws {Error} if something went wrong with deserialization.
966
+ * @package
967
+ */
968
+ createBlock(originalBlock) {
969
+ let newBlock = null;
970
+ eventUtils.disable();
971
+ const variablesBeforeCreation = this.targetWorkspace.getAllVariables();
972
+ this.targetWorkspace.setResizesEnabled(false);
973
+ try {
974
+ newBlock = this.placeNewBlock_(originalBlock);
975
+ } finally {
976
+ eventUtils.enable();
977
+ }
911
978
 
912
- /**
913
- * Create a copy of this block on the workspace.
914
- * @param {!BlockSvg} originalBlock The block to copy from the flyout.
915
- * @return {!BlockSvg} The newly created block.
916
- * @throws {Error} if something went wrong with deserialization.
917
- * @package
918
- */
919
- Flyout.prototype.createBlock = function(originalBlock) {
920
- let newBlock = null;
921
- eventUtils.disable();
922
- const variablesBeforeCreation = this.targetWorkspace.getAllVariables();
923
- this.targetWorkspace.setResizesEnabled(false);
924
- try {
925
- newBlock = this.placeNewBlock_(originalBlock);
926
- } finally {
927
- eventUtils.enable();
928
- }
979
+ // Close the flyout.
980
+ this.targetWorkspace.hideChaff();
929
981
 
930
- // Close the flyout.
931
- this.targetWorkspace.hideChaff();
982
+ const newVariables = Variables.getAddedVariables(
983
+ this.targetWorkspace, variablesBeforeCreation);
932
984
 
933
- const newVariables = Variables.getAddedVariables(
934
- this.targetWorkspace, variablesBeforeCreation);
985
+ if (eventUtils.isEnabled()) {
986
+ eventUtils.setGroup(true);
987
+ // Fire a VarCreate event for each (if any) new variable created.
988
+ for (let i = 0; i < newVariables.length; i++) {
989
+ const thisVariable = newVariables[i];
990
+ eventUtils.fire(
991
+ new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable));
992
+ }
935
993
 
936
- if (eventUtils.isEnabled()) {
937
- eventUtils.setGroup(true);
938
- // Fire a VarCreate event for each (if any) new variable created.
939
- for (let i = 0; i < newVariables.length; i++) {
940
- const thisVariable = newVariables[i];
941
- eventUtils.fire(
942
- new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable));
994
+ // Block events come after var events, in case they refer to newly created
995
+ // variables.
996
+ eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
943
997
  }
944
-
945
- // Block events come after var events, in case they refer to newly created
946
- // variables.
947
- eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
948
- }
949
- if (this.autoClose) {
950
- this.hide();
951
- } else {
952
- this.filterForCapacity_();
998
+ if (this.autoClose) {
999
+ this.hide();
1000
+ } else {
1001
+ this.filterForCapacity_();
1002
+ }
1003
+ return newBlock;
953
1004
  }
954
- return newBlock;
955
- };
956
-
957
- /**
958
- * Initialize the given button: move it to the correct location,
959
- * add listeners, etc.
960
- * @param {!FlyoutButton} button The button to initialize and place.
961
- * @param {number} x The x position of the cursor during this layout pass.
962
- * @param {number} y The y position of the cursor during this layout pass.
963
- * @protected
964
- */
965
- Flyout.prototype.initFlyoutButton_ = function(button, x, y) {
966
- const buttonSvg = button.createDom();
967
- button.moveTo(x, y);
968
- button.show();
969
- // Clicking on a flyout button or label is a lot like clicking on the
970
- // flyout background.
971
- this.listeners_.push(browserEvents.conditionalBind(
972
- buttonSvg, 'mousedown', this, this.onMouseDown_));
973
-
974
- this.buttons_.push(button);
975
- };
976
1005
 
977
- /**
978
- * Create and place a rectangle corresponding to the given block.
979
- * @param {!BlockSvg} block The block to associate the rect to.
980
- * @param {number} x The x position of the cursor during this layout pass.
981
- * @param {number} y The y position of the cursor during this layout pass.
982
- * @param {!{height: number, width: number}} blockHW The height and width of the
983
- * block.
984
- * @param {number} index The index into the mats list where this rect should be
985
- * placed.
986
- * @return {!SVGElement} Newly created SVG element for the rectangle behind the
987
- * block.
988
- * @protected
989
- */
990
- Flyout.prototype.createRect_ = function(block, x, y, blockHW, index) {
991
- // Create an invisible rectangle under the block to act as a button. Just
992
- // using the block as a button is poor, since blocks have holes in them.
993
- const rect = dom.createSvgElement(
994
- Svg.RECT, {
995
- 'fill-opacity': 0,
996
- 'x': x,
997
- 'y': y,
998
- 'height': blockHW.height,
999
- 'width': blockHW.width,
1000
- },
1001
- null);
1002
- rect.tooltip = block;
1003
- Tooltip.bindMouseEvents(rect);
1004
- // Add the rectangles under the blocks, so that the blocks' tooltips work.
1005
- this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
1006
-
1007
- block.flyoutRect_ = rect;
1008
- this.mats_[index] = rect;
1009
- return rect;
1010
- };
1006
+ /**
1007
+ * Initialize the given button: move it to the correct location,
1008
+ * add listeners, etc.
1009
+ * @param {!FlyoutButton} button The button to initialize and place.
1010
+ * @param {number} x The x position of the cursor during this layout pass.
1011
+ * @param {number} y The y position of the cursor during this layout pass.
1012
+ * @protected
1013
+ */
1014
+ initFlyoutButton_(button, x, y) {
1015
+ const buttonSvg = button.createDom();
1016
+ button.moveTo(x, y);
1017
+ button.show();
1018
+ // Clicking on a flyout button or label is a lot like clicking on the
1019
+ // flyout background.
1020
+ this.listeners_.push(browserEvents.conditionalBind(
1021
+ buttonSvg, 'mousedown', this, this.onMouseDown_));
1022
+
1023
+ this.buttons_.push(button);
1024
+ }
1011
1025
 
1012
- /**
1013
- * Move a rectangle to sit exactly behind a block, taking into account tabs,
1014
- * hats, and any other protrusions we invent.
1015
- * @param {!SVGElement} rect The rectangle to move directly behind the block.
1016
- * @param {!BlockSvg} block The block the rectangle should be behind.
1017
- * @protected
1018
- */
1019
- Flyout.prototype.moveRectToBlock_ = function(rect, block) {
1020
- const blockHW = block.getHeightWidth();
1021
- rect.setAttribute('width', blockHW.width);
1022
- rect.setAttribute('height', blockHW.height);
1026
+ /**
1027
+ * Create and place a rectangle corresponding to the given block.
1028
+ * @param {!BlockSvg} block The block to associate the rect to.
1029
+ * @param {number} x The x position of the cursor during this layout pass.
1030
+ * @param {number} y The y position of the cursor during this layout pass.
1031
+ * @param {!{height: number, width: number}} blockHW The height and width of
1032
+ * the block.
1033
+ * @param {number} index The index into the mats list where this rect should
1034
+ * be placed.
1035
+ * @return {!SVGElement} Newly created SVG element for the rectangle behind
1036
+ * the block.
1037
+ * @protected
1038
+ */
1039
+ createRect_(block, x, y, blockHW, index) {
1040
+ // Create an invisible rectangle under the block to act as a button. Just
1041
+ // using the block as a button is poor, since blocks have holes in them.
1042
+ const rect = dom.createSvgElement(
1043
+ Svg.RECT, {
1044
+ 'fill-opacity': 0,
1045
+ 'x': x,
1046
+ 'y': y,
1047
+ 'height': blockHW.height,
1048
+ 'width': blockHW.width,
1049
+ },
1050
+ null);
1051
+ rect.tooltip = block;
1052
+ Tooltip.bindMouseEvents(rect);
1053
+ // Add the rectangles under the blocks, so that the blocks' tooltips work.
1054
+ this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
1055
+
1056
+ this.rectMap_.set(block, rect);
1057
+ this.mats_[index] = rect;
1058
+ return rect;
1059
+ }
1023
1060
 
1024
- const blockXY = block.getRelativeToSurfaceXY();
1025
- rect.setAttribute('y', blockXY.y);
1026
- rect.setAttribute('x', this.RTL ? blockXY.x - blockHW.width : blockXY.x);
1027
- };
1061
+ /**
1062
+ * Move a rectangle to sit exactly behind a block, taking into account tabs,
1063
+ * hats, and any other protrusions we invent.
1064
+ * @param {!SVGElement} rect The rectangle to move directly behind the block.
1065
+ * @param {!BlockSvg} block The block the rectangle should be behind.
1066
+ * @protected
1067
+ */
1068
+ moveRectToBlock_(rect, block) {
1069
+ const blockHW = block.getHeightWidth();
1070
+ rect.setAttribute('width', blockHW.width);
1071
+ rect.setAttribute('height', blockHW.height);
1072
+
1073
+ const blockXY = block.getRelativeToSurfaceXY();
1074
+ rect.setAttribute('y', blockXY.y);
1075
+ rect.setAttribute('x', this.RTL ? blockXY.x - blockHW.width : blockXY.x);
1076
+ }
1028
1077
 
1029
- /**
1030
- * Filter the blocks on the flyout to disable the ones that are above the
1031
- * capacity limit. For instance, if the user may only place two more blocks on
1032
- * the workspace, an "a + b" block that has two shadow blocks would be disabled.
1033
- * @private
1034
- */
1035
- Flyout.prototype.filterForCapacity_ = function() {
1036
- const blocks = this.workspace_.getTopBlocks(false);
1037
- for (let i = 0, block; (block = blocks[i]); i++) {
1038
- if (this.permanentlyDisabled_.indexOf(block) === -1) {
1039
- const enable = this.targetWorkspace.isCapacityAvailable(
1040
- common.getBlockTypeCounts(block));
1041
- while (block) {
1042
- block.setEnabled(enable);
1043
- block = block.getNextBlock();
1078
+ /**
1079
+ * Filter the blocks on the flyout to disable the ones that are above the
1080
+ * capacity limit. For instance, if the user may only place two more blocks
1081
+ * on the workspace, an "a + b" block that has two shadow blocks would be
1082
+ * disabled.
1083
+ * @private
1084
+ */
1085
+ filterForCapacity_() {
1086
+ const blocks = this.workspace_.getTopBlocks(false);
1087
+ for (let i = 0, block; (block = blocks[i]); i++) {
1088
+ if (this.permanentlyDisabled_.indexOf(block) === -1) {
1089
+ const enable = this.targetWorkspace.isCapacityAvailable(
1090
+ common.getBlockTypeCounts(block));
1091
+ while (block) {
1092
+ block.setEnabled(enable);
1093
+ block = block.getNextBlock();
1094
+ }
1044
1095
  }
1045
1096
  }
1046
1097
  }
1047
- };
1048
1098
 
1049
- /**
1050
- * Reflow blocks and their mats.
1051
- */
1052
- Flyout.prototype.reflow = function() {
1053
- if (this.reflowWrapper_) {
1054
- this.workspace_.removeChangeListener(this.reflowWrapper_);
1055
- }
1056
- this.reflowInternal_();
1057
- if (this.reflowWrapper_) {
1058
- this.workspace_.addChangeListener(this.reflowWrapper_);
1099
+ /**
1100
+ * Reflow blocks and their mats.
1101
+ */
1102
+ reflow() {
1103
+ if (this.reflowWrapper_) {
1104
+ this.workspace_.removeChangeListener(this.reflowWrapper_);
1105
+ }
1106
+ this.reflowInternal_();
1107
+ if (this.reflowWrapper_) {
1108
+ this.workspace_.addChangeListener(this.reflowWrapper_);
1109
+ }
1059
1110
  }
1060
- };
1061
-
1062
- /**
1063
- * @return {boolean} True if this flyout may be scrolled with a scrollbar or by
1064
- * dragging.
1065
- * @package
1066
- */
1067
- Flyout.prototype.isScrollable = function() {
1068
- return this.workspace_.scrollbar ? this.workspace_.scrollbar.isVisible() :
1069
- false;
1070
- };
1071
1111
 
1072
- /**
1073
- * Copy a block from the flyout to the workspace and position it correctly.
1074
- * @param {!BlockSvg} oldBlock The flyout block to copy.
1075
- * @return {!BlockSvg} The new block in the main workspace.
1076
- * @private
1077
- */
1078
- Flyout.prototype.placeNewBlock_ = function(oldBlock) {
1079
- const targetWorkspace = this.targetWorkspace;
1080
- const svgRootOld = oldBlock.getSvgRoot();
1081
- if (!svgRootOld) {
1082
- throw Error('oldBlock is not rendered.');
1112
+ /**
1113
+ * @return {boolean} True if this flyout may be scrolled with a scrollbar or
1114
+ * by dragging.
1115
+ * @package
1116
+ */
1117
+ isScrollable() {
1118
+ return this.workspace_.scrollbar ? this.workspace_.scrollbar.isVisible() :
1119
+ false;
1083
1120
  }
1084
1121
 
1085
- // Clone the block.
1086
- const json = /** @type {!blocks.State} */ (blocks.save(oldBlock));
1087
- // Normallly this resizes leading to weird jumps. Save it for terminateDrag.
1088
- targetWorkspace.setResizesEnabled(false);
1089
- const block = /** @type {!BlockSvg} */ (blocks.append(json, targetWorkspace));
1122
+ /**
1123
+ * Copy a block from the flyout to the workspace and position it correctly.
1124
+ * @param {!BlockSvg} oldBlock The flyout block to copy.
1125
+ * @return {!BlockSvg} The new block in the main workspace.
1126
+ * @private
1127
+ */
1128
+ placeNewBlock_(oldBlock) {
1129
+ const targetWorkspace = this.targetWorkspace;
1130
+ const svgRootOld = oldBlock.getSvgRoot();
1131
+ if (!svgRootOld) {
1132
+ throw Error('oldBlock is not rendered.');
1133
+ }
1134
+
1135
+ // Clone the block.
1136
+ const json = /** @type {!blocks.State} */ (blocks.save(oldBlock));
1137
+ // Normallly this resizes leading to weird jumps. Save it for terminateDrag.
1138
+ targetWorkspace.setResizesEnabled(false);
1139
+ const block =
1140
+ /** @type {!BlockSvg} */ (blocks.append(json, targetWorkspace));
1090
1141
 
1091
- this.positionNewBlock_(oldBlock, block);
1142
+ this.positionNewBlock_(oldBlock, block);
1092
1143
 
1093
- return block;
1094
- };
1144
+ return block;
1145
+ }
1095
1146
 
1096
- /**
1097
- * Positions a block on the target workspace.
1098
- * @param {!BlockSvg} oldBlock The flyout block being copied.
1099
- * @param {!BlockSvg} block The block to posiiton.
1100
- * @private
1101
- */
1102
- Flyout.prototype.positionNewBlock_ = function(oldBlock, block) {
1103
- const targetWorkspace = this.targetWorkspace;
1104
-
1105
- // The offset in pixels between the main workspace's origin and the upper left
1106
- // corner of the injection div.
1107
- const mainOffsetPixels = targetWorkspace.getOriginOffsetInPixels();
1108
-
1109
- // The offset in pixels between the flyout workspace's origin and the upper
1110
- // left corner of the injection div.
1111
- const flyoutOffsetPixels = this.workspace_.getOriginOffsetInPixels();
1112
-
1113
- // The position of the old block in flyout workspace coordinates.
1114
- const oldBlockPos = oldBlock.getRelativeToSurfaceXY();
1115
- // The position of the old block in pixels relative to the flyout
1116
- // workspace's origin.
1117
- oldBlockPos.scale(this.workspace_.scale);
1118
-
1119
- // The position of the old block in pixels relative to the upper left corner
1120
- // of the injection div.
1121
- const oldBlockOffsetPixels = Coordinate.sum(flyoutOffsetPixels, oldBlockPos);
1122
-
1123
- // The position of the old block in pixels relative to the origin of the
1124
- // main workspace.
1125
- const finalOffset =
1126
- Coordinate.difference(oldBlockOffsetPixels, mainOffsetPixels);
1127
- // The position of the old block in main workspace coordinates.
1128
- finalOffset.scale(1 / targetWorkspace.scale);
1129
-
1130
- block.moveTo(new Coordinate(finalOffset.x, finalOffset.y));
1131
- };
1147
+ /**
1148
+ * Positions a block on the target workspace.
1149
+ * @param {!BlockSvg} oldBlock The flyout block being copied.
1150
+ * @param {!BlockSvg} block The block to posiiton.
1151
+ * @private
1152
+ */
1153
+ positionNewBlock_(oldBlock, block) {
1154
+ const targetWorkspace = this.targetWorkspace;
1155
+
1156
+ // The offset in pixels between the main workspace's origin and the upper
1157
+ // left corner of the injection div.
1158
+ const mainOffsetPixels = targetWorkspace.getOriginOffsetInPixels();
1159
+
1160
+ // The offset in pixels between the flyout workspace's origin and the upper
1161
+ // left corner of the injection div.
1162
+ const flyoutOffsetPixels = this.workspace_.getOriginOffsetInPixels();
1163
+
1164
+ // The position of the old block in flyout workspace coordinates.
1165
+ const oldBlockPos = oldBlock.getRelativeToSurfaceXY();
1166
+ // The position of the old block in pixels relative to the flyout
1167
+ // workspace's origin.
1168
+ oldBlockPos.scale(this.workspace_.scale);
1169
+
1170
+ // The position of the old block in pixels relative to the upper left corner
1171
+ // of the injection div.
1172
+ const oldBlockOffsetPixels =
1173
+ Coordinate.sum(flyoutOffsetPixels, oldBlockPos);
1174
+
1175
+ // The position of the old block in pixels relative to the origin of the
1176
+ // main workspace.
1177
+ const finalOffset =
1178
+ Coordinate.difference(oldBlockOffsetPixels, mainOffsetPixels);
1179
+ // The position of the old block in main workspace coordinates.
1180
+ finalOffset.scale(1 / targetWorkspace.scale);
1181
+
1182
+ block.moveTo(new Coordinate(finalOffset.x, finalOffset.y));
1183
+ }
1184
+ }
1132
1185
 
1133
1186
  /**
1134
1187
  * Returns the bounding rectangle of the drag target area in pixel units