blockly 7.20211209.2 → 8.0.0

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 (262) hide show
  1. package/blockly.d.ts +18963 -18432
  2. package/blockly.min.js +852 -844
  3. package/blockly_compressed.js +669 -664
  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 +41 -27
  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 +146 -141
  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 +19 -9
  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/dart_compressed.js +40 -43
  208. package/dart_compressed.js.map +1 -1
  209. package/generators/dart/colour.js +56 -64
  210. package/generators/dart/lists.js +61 -50
  211. package/generators/dart/math.js +160 -148
  212. package/generators/dart/text.js +83 -61
  213. package/generators/javascript/colour.js +37 -34
  214. package/generators/javascript/lists.js +50 -43
  215. package/generators/javascript/math.js +123 -139
  216. package/generators/javascript/text.js +67 -81
  217. package/generators/lua/colour.js +25 -23
  218. package/generators/lua/lists.js +97 -69
  219. package/generators/lua/logic.js +1 -2
  220. package/generators/lua/math.js +182 -144
  221. package/generators/lua/text.js +116 -99
  222. package/generators/php/colour.js +38 -32
  223. package/generators/php/lists.js +109 -89
  224. package/generators/php/math.js +90 -81
  225. package/generators/php/text.js +63 -61
  226. package/generators/python/colour.js +18 -18
  227. package/generators/python/lists.js +38 -30
  228. package/generators/python/loops.js +12 -8
  229. package/generators/python/math.js +104 -106
  230. package/generators/python/text.js +34 -30
  231. package/javascript_compressed.js +37 -39
  232. package/javascript_compressed.js.map +1 -1
  233. package/lua_compressed.js +39 -42
  234. package/lua_compressed.js.map +1 -1
  235. package/msg/az.js +2 -2
  236. package/msg/be.js +4 -4
  237. package/msg/cs.js +15 -15
  238. package/msg/de.js +1 -1
  239. package/msg/diq.js +1 -1
  240. package/msg/eo.js +1 -1
  241. package/msg/es.js +1 -1
  242. package/msg/fa.js +1 -1
  243. package/msg/fr.js +4 -4
  244. package/msg/he.js +1 -1
  245. package/msg/hr.js +2 -2
  246. package/msg/hy.js +2 -2
  247. package/msg/id.js +12 -12
  248. package/msg/inh.js +14 -14
  249. package/msg/ja.js +7 -7
  250. package/msg/lv.js +29 -29
  251. package/msg/pa.js +3 -3
  252. package/msg/smn.js +436 -0
  253. package/msg/te.js +1 -1
  254. package/msg/yue.js +1 -1
  255. package/msg/zh-hans.js +3 -3
  256. package/msg/zh-hant.js +3 -3
  257. package/package.json +7 -6
  258. package/php_compressed.js +38 -42
  259. package/php_compressed.js.map +1 -1
  260. package/python_compressed.js +26 -25
  261. package/python_compressed.js.map +1 -1
  262. package/blocks/all.js +0 -23
@@ -31,9 +31,8 @@ const blocks = goog.require('Blockly.serialization.blocks');
31
31
  const browserEvents = goog.require('Blockly.browserEvents');
32
32
  const common = goog.require('Blockly.common');
33
33
  const dom = goog.require('Blockly.utils.dom');
34
+ const dropDownDiv = goog.require('Blockly.dropDownDiv');
34
35
  const eventUtils = goog.require('Blockly.Events.utils');
35
- const internalConstants = goog.require('Blockly.internalConstants');
36
- const object = goog.require('Blockly.utils.object');
37
36
  const registry = goog.require('Blockly.registry');
38
37
  const svgMath = goog.require('Blockly.utils.svgMath');
39
38
  const toolbox = goog.require('Blockly.utils.toolbox');
@@ -48,12 +47,12 @@ const {BlocklyOptions} = goog.requireType('Blockly.BlocklyOptions');
48
47
  const {Block} = goog.requireType('Blockly.Block');
49
48
  const {Classic} = goog.require('Blockly.Themes.Classic');
50
49
  const {ComponentManager} = goog.require('Blockly.ComponentManager');
50
+ const {config} = goog.require('Blockly.config');
51
51
  const {ConnectionDB} = goog.require('Blockly.ConnectionDB');
52
52
  const {ContextMenuRegistry} = goog.require('Blockly.ContextMenuRegistry');
53
53
  const {Coordinate} = goog.require('Blockly.utils.Coordinate');
54
54
  /* eslint-disable-next-line no-unused-vars */
55
55
  const {Cursor} = goog.requireType('Blockly.Cursor');
56
- const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
57
56
  /* eslint-disable-next-line no-unused-vars */
58
57
  const {FlyoutButton} = goog.requireType('Blockly.FlyoutButton');
59
58
  const {Gesture} = goog.require('Blockly.Gesture');
@@ -63,6 +62,8 @@ const {IASTNodeLocationSvg} = goog.require('Blockly.IASTNodeLocationSvg');
63
62
  /* eslint-disable-next-line no-unused-vars */
64
63
  const {IBoundedElement} = goog.requireType('Blockly.IBoundedElement');
65
64
  /* eslint-disable-next-line no-unused-vars */
65
+ const {ICopyable} = goog.requireType('Blockly.ICopyable');
66
+ /* eslint-disable-next-line no-unused-vars */
66
67
  const {IDragTarget} = goog.requireType('Blockly.IDragTarget');
67
68
  /* eslint-disable-next-line no-unused-vars */
68
69
  const {IFlyout} = goog.requireType('Blockly.IFlyout');
@@ -78,6 +79,8 @@ const {Metrics} = goog.requireType('Blockly.utils.Metrics');
78
79
  const {Options} = goog.require('Blockly.Options');
79
80
  const {Rect} = goog.require('Blockly.utils.Rect');
80
81
  /* eslint-disable-next-line no-unused-vars */
82
+ const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
83
+ /* eslint-disable-next-line no-unused-vars */
81
84
  const {Renderer} = goog.requireType('Blockly.blockRendering.Renderer');
82
85
  /* eslint-disable-next-line no-unused-vars */
83
86
  const {ScrollbarPair} = goog.requireType('Blockly.ScrollbarPair');
@@ -116,2585 +119,2607 @@ goog.require('Blockly.Msg');
116
119
  /**
117
120
  * Class for a workspace. This is an onscreen area with optional trashcan,
118
121
  * scrollbars, bubbles, and dragging.
119
- * @param {!Options} options Dictionary of options.
120
- * @param {BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
121
- * blocks.
122
- * @param {WorkspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
123
- * the workspace.
124
122
  * @extends {Workspace}
125
123
  * @implements {IASTNodeLocationSvg}
126
- * @constructor
127
124
  * @alias Blockly.WorkspaceSvg
128
125
  */
129
- const WorkspaceSvg = function(
130
- options, opt_blockDragSurface, opt_wsDragSurface) {
131
- WorkspaceSvg.superClass_.constructor.call(this, options);
126
+ class WorkspaceSvg extends Workspace {
127
+ /**
128
+ * @param {!Options} options Dictionary of options.
129
+ * @param {BlockDragSurfaceSvg=} opt_blockDragSurface Drag surface for
130
+ * blocks.
131
+ * @param {WorkspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for
132
+ * the workspace.
133
+ */
134
+ constructor(options, opt_blockDragSurface, opt_wsDragSurface) {
135
+ super(options);
136
+
137
+ /**
138
+ * A wrapper function called when a resize event occurs.
139
+ * You can pass the result to `eventHandling.unbind`.
140
+ * @type {?browserEvents.Data}
141
+ * @private
142
+ */
143
+ this.resizeHandlerWrapper_ = null;
144
+
145
+ /**
146
+ * The render status of an SVG workspace.
147
+ * Returns `false` for headless workspaces and true for instances of
148
+ * `WorkspaceSvg`.
149
+ * @type {boolean}
150
+ */
151
+ this.rendered = true;
152
+
153
+ /**
154
+ * Whether the workspace is visible. False if the workspace has been hidden
155
+ * by calling `setVisible(false)`.
156
+ * @type {boolean}
157
+ * @private
158
+ */
159
+ this.isVisible_ = true;
160
+
161
+ /**
162
+ * Whether this workspace has resizes enabled.
163
+ * Disable during batch operations for a performance improvement.
164
+ * @type {boolean}
165
+ * @private
166
+ */
167
+ this.resizesEnabled_ = true;
168
+
169
+ /**
170
+ * Current horizontal scrolling offset in pixel units, relative to the
171
+ * workspace origin.
172
+ *
173
+ * It is useful to think about a view, and a canvas moving beneath that
174
+ * view. As the canvas moves right, this value becomes more positive, and
175
+ * the view is now "seeing" the left side of the canvas. As the canvas moves
176
+ * left, this value becomes more negative, and the view is now "seeing" the
177
+ * right side of the canvas.
178
+ *
179
+ * The confusing thing about this value is that it does not, and must not
180
+ * include the absoluteLeft offset. This is because it is used to calculate
181
+ * the viewLeft value.
182
+ *
183
+ * The viewLeft is relative to the workspace origin (although in pixel
184
+ * units). The workspace origin is the top-left corner of the workspace (at
185
+ * least when it is enabled). It is shifted from the top-left of the
186
+ * blocklyDiv so as not to be beneath the toolbox.
187
+ *
188
+ * When the workspace is enabled the viewLeft and workspace origin are at
189
+ * the same X location. As the canvas slides towards the right beneath the
190
+ * view this value (scrollX) becomes more positive, and the viewLeft becomes
191
+ * more negative relative to the workspace origin (imagine the workspace
192
+ * origin as a dot on the canvas sliding to the right as the canvas moves).
193
+ *
194
+ * So if the scrollX were to include the absoluteLeft this would in a way
195
+ * "unshift" the workspace origin. This means that the viewLeft would be
196
+ * representing the left edge of the blocklyDiv, rather than the left edge
197
+ * of the workspace.
198
+ *
199
+ * @type {number}
200
+ */
201
+ this.scrollX = 0;
202
+
203
+ /**
204
+ * Current vertical scrolling offset in pixel units, relative to the
205
+ * workspace origin.
206
+ *
207
+ * It is useful to think about a view, and a canvas moving beneath that
208
+ * view. As the canvas moves down, this value becomes more positive, and the
209
+ * view is now "seeing" the upper part of the canvas. As the canvas moves
210
+ * up, this value becomes more negative, and the view is "seeing" the lower
211
+ * part of the canvas.
212
+ *
213
+ * This confusing thing about this value is that it does not, and must not
214
+ * include the absoluteTop offset. This is because it is used to calculate
215
+ * the viewTop value.
216
+ *
217
+ * The viewTop is relative to the workspace origin (although in pixel
218
+ * units). The workspace origin is the top-left corner of the workspace (at
219
+ * least when it is enabled). It is shifted from the top-left of the
220
+ * blocklyDiv so as not to be beneath the toolbox.
221
+ *
222
+ * When the workspace is enabled the viewTop and workspace origin are at the
223
+ * same Y location. As the canvas slides towards the bottom this value
224
+ * (scrollY) becomes more positive, and the viewTop becomes more negative
225
+ * relative to the workspace origin (image in the workspace origin as a dot
226
+ * on the canvas sliding downwards as the canvas moves).
227
+ *
228
+ * So if the scrollY were to include the absoluteTop this would in a way
229
+ * "unshift" the workspace origin. This means that the viewTop would be
230
+ * representing the top edge of the blocklyDiv, rather than the top edge of
231
+ * the workspace.
232
+ *
233
+ * @type {number}
234
+ */
235
+ this.scrollY = 0;
236
+
237
+ /**
238
+ * Horizontal scroll value when scrolling started in pixel units.
239
+ * @type {number}
240
+ */
241
+ this.startScrollX = 0;
242
+
243
+ /**
244
+ * Vertical scroll value when scrolling started in pixel units.
245
+ * @type {number}
246
+ */
247
+ this.startScrollY = 0;
248
+
249
+ /**
250
+ * Distance from mouse to object being dragged.
251
+ * @type {Coordinate}
252
+ * @private
253
+ */
254
+ this.dragDeltaXY_ = null;
255
+
256
+ /**
257
+ * Current scale.
258
+ * @type {number}
259
+ */
260
+ this.scale = 1;
261
+
262
+ /**
263
+ * Cached scale value. Used to detect changes in viewport.
264
+ * @type {number}
265
+ * @private
266
+ */
267
+ this.oldScale_ = 1;
268
+
269
+ /**
270
+ * Cached viewport top value. Used to detect changes in viewport.
271
+ * @type {number}
272
+ * @private
273
+ */
274
+ this.oldTop_ = 0;
275
+
276
+ /**
277
+ * Cached viewport left value. Used to detect changes in viewport.
278
+ * @type {number}
279
+ * @private
280
+ */
281
+ this.oldLeft_ = 0;
282
+
283
+ /**
284
+ * The workspace's trashcan (if any).
285
+ * @type {Trashcan}
286
+ */
287
+ this.trashcan = null;
288
+
289
+ /**
290
+ * This workspace's scrollbars, if they exist.
291
+ * @type {ScrollbarPair}
292
+ */
293
+ this.scrollbar = null;
294
+
295
+ /**
296
+ * Fixed flyout providing blocks which may be dragged into this workspace.
297
+ * @type {IFlyout}
298
+ * @private
299
+ */
300
+ this.flyout_ = null;
301
+
302
+ /**
303
+ * Category-based toolbox providing blocks which may be dragged into this
304
+ * workspace.
305
+ * @type {IToolbox}
306
+ * @private
307
+ */
308
+ this.toolbox_ = null;
309
+
310
+ /**
311
+ * The current gesture in progress on this workspace, if any.
312
+ * @type {TouchGesture}
313
+ * @package
314
+ */
315
+ this.currentGesture_ = null;
316
+
317
+ /**
318
+ * This workspace's surface for dragging blocks, if it exists.
319
+ * @type {BlockDragSurfaceSvg}
320
+ * @private
321
+ */
322
+ this.blockDragSurface_ = null;
323
+
324
+ /**
325
+ * This workspace's drag surface, if it exists.
326
+ * @type {WorkspaceDragSurfaceSvg}
327
+ * @private
328
+ */
329
+ this.workspaceDragSurface_ = null;
330
+
331
+ /**
332
+ * Whether to move workspace to the drag surface when it is dragged.
333
+ * True if it should move, false if it should be translated directly.
334
+ * @type {boolean}
335
+ * @private
336
+ */
337
+ this.useWorkspaceDragSurface_ = false;
338
+
339
+ /**
340
+ * Whether the drag surface is actively in use. When true, calls to
341
+ * translate will translate the drag surface instead of the translating the
342
+ * workspace directly.
343
+ * This is set to true in setupDragSurface and to false in resetDragSurface.
344
+ * @type {boolean}
345
+ * @private
346
+ */
347
+ this.isDragSurfaceActive_ = false;
348
+
349
+ /**
350
+ * The first parent div with 'injectionDiv' in the name, or null if not set.
351
+ * Access this with getInjectionDiv.
352
+ * @type {Element}
353
+ * @private
354
+ */
355
+ this.injectionDiv_ = null;
356
+
357
+ /**
358
+ * Last known position of the page scroll.
359
+ * This is used to determine whether we have recalculated screen coordinate
360
+ * stuff since the page scrolled.
361
+ * @type {Coordinate}
362
+ * @private
363
+ */
364
+ this.lastRecordedPageScroll_ = null;
365
+
366
+ /**
367
+ * Developers may define this function to add custom menu options to the
368
+ * workspace's context menu or edit the workspace-created set of menu
369
+ * options.
370
+ * @param {!Array<!Object>} options List of menu options to add to.
371
+ * @param {!Event} e The right-click event that triggered the context menu.
372
+ */
373
+ this.configureContextMenu;
374
+
375
+ /**
376
+ * In a flyout, the target workspace where blocks should be placed after a
377
+ * drag. Otherwise null.
378
+ * @type {WorkspaceSvg}
379
+ * @package
380
+ */
381
+ this.targetWorkspace = null;
382
+
383
+ /**
384
+ * Inverted screen CTM, for use in mouseToSvg.
385
+ * @type {?SVGMatrix}
386
+ * @private
387
+ */
388
+ this.inverseScreenCTM_ = null;
389
+
390
+ /**
391
+ * Inverted screen CTM is dirty, recalculate it.
392
+ * @type {boolean}
393
+ * @private
394
+ */
395
+ this.inverseScreenCTMDirty_ = true;
396
+
397
+ const MetricsManagerClass = registry.getClassFromOptions(
398
+ registry.Type.METRICS_MANAGER, options, true);
399
+ /**
400
+ * Object in charge of calculating metrics for the workspace.
401
+ * @type {!IMetricsManager}
402
+ * @private
403
+ */
404
+ this.metricsManager_ = new MetricsManagerClass(this);
405
+
406
+ /**
407
+ * Method to get all the metrics that have to do with a workspace.
408
+ * @type {function():!Metrics}
409
+ * @package
410
+ */
411
+ this.getMetrics = options.getMetrics ||
412
+ this.metricsManager_.getMetrics.bind(this.metricsManager_);
413
+
414
+ /**
415
+ * Translates the workspace.
416
+ * @type {function(!{x:number, y:number}):void}
417
+ * @package
418
+ */
419
+ this.setMetrics =
420
+ options.setMetrics || WorkspaceSvg.setTopLevelWorkspaceMetrics_;
421
+
422
+ /**
423
+ * @type {!ComponentManager}
424
+ * @private
425
+ */
426
+ this.componentManager_ = new ComponentManager();
427
+
428
+ this.connectionDBList = ConnectionDB.init(this.connectionChecker);
429
+
430
+ if (opt_blockDragSurface) {
431
+ this.blockDragSurface_ = opt_blockDragSurface;
432
+ }
433
+
434
+ if (opt_wsDragSurface) {
435
+ this.workspaceDragSurface_ = opt_wsDragSurface;
436
+ }
437
+
438
+ this.useWorkspaceDragSurface_ =
439
+ !!this.workspaceDragSurface_ && svgMath.is3dSupported();
440
+
441
+ /**
442
+ * List of currently highlighted blocks. Block highlighting is often used
443
+ * to visually mark blocks currently being executed.
444
+ * @type {!Array<!BlockSvg>}
445
+ * @private
446
+ */
447
+ this.highlightedBlocks_ = [];
448
+
449
+ /**
450
+ * Object in charge of loading, storing, and playing audio for a workspace.
451
+ * @type {!WorkspaceAudio}
452
+ * @private
453
+ */
454
+ this.audioManager_ = new WorkspaceAudio(
455
+ /** @type {WorkspaceSvg} */ (options.parentWorkspace));
456
+
457
+ /**
458
+ * This workspace's grid object or null.
459
+ * @type {Grid}
460
+ * @private
461
+ */
462
+ this.grid_ = this.options.gridPattern ?
463
+ new Grid(this.options.gridPattern, options.gridOptions) :
464
+ null;
465
+
466
+ /**
467
+ * Manager in charge of markers and cursors.
468
+ * @type {!MarkerManager}
469
+ * @private
470
+ */
471
+ this.markerManager_ = new MarkerManager(this);
472
+
473
+ /**
474
+ * Map from function names to callbacks, for deciding what to do when a
475
+ * custom toolbox category is opened.
476
+ * @type {!Object<string, ?function(!WorkspaceSvg):
477
+ * !toolbox.FlyoutDefinition>}
478
+ * @private
479
+ */
480
+ this.toolboxCategoryCallbacks_ = Object.create(null);
481
+
482
+ /**
483
+ * Map from function names to callbacks, for deciding what to do when a
484
+ * button is clicked.
485
+ * @type {!Object<string, ?function(!FlyoutButton)>}
486
+ * @private
487
+ */
488
+ this.flyoutButtonCallbacks_ = Object.create(null);
489
+
490
+ const Variables = goog.module.get('Blockly.Variables');
491
+ if (Variables && Variables.flyoutCategory) {
492
+ this.registerToolboxCategoryCallback(
493
+ Variables.CATEGORY_NAME, Variables.flyoutCategory);
494
+ }
495
+
496
+ const VariablesDynamic = goog.module.get('Blockly.VariablesDynamic');
497
+ if (VariablesDynamic && VariablesDynamic.flyoutCategory) {
498
+ this.registerToolboxCategoryCallback(
499
+ VariablesDynamic.CATEGORY_NAME, VariablesDynamic.flyoutCategory);
500
+ }
501
+
502
+ const Procedures = goog.module.get('Blockly.Procedures');
503
+ if (Procedures && Procedures.flyoutCategory) {
504
+ this.registerToolboxCategoryCallback(
505
+ Procedures.CATEGORY_NAME, Procedures.flyoutCategory);
506
+ this.addChangeListener(Procedures.mutatorOpenListener);
507
+ }
508
+
509
+ /**
510
+ * Object in charge of storing and updating the workspace theme.
511
+ * @type {!ThemeManager}
512
+ * @protected
513
+ */
514
+ this.themeManager_ = this.options.parentWorkspace ?
515
+ this.options.parentWorkspace.getThemeManager() :
516
+ new ThemeManager(this, this.options.theme || Classic);
517
+ this.themeManager_.subscribeWorkspace(this);
518
+
519
+ /**
520
+ * The block renderer used for rendering blocks on this workspace.
521
+ * @type {!Renderer}
522
+ * @private
523
+ */
524
+ this.renderer_ = blockRendering.init(
525
+ this.options.renderer || 'geras', this.getTheme(),
526
+ this.options.rendererOverrides);
527
+
528
+ /**
529
+ * Cached parent SVG.
530
+ * @type {SVGElement}
531
+ * @private
532
+ */
533
+ this.cachedParentSvg_ = null;
534
+
535
+ /**
536
+ * True if keyboard accessibility mode is on, false otherwise.
537
+ * @type {boolean}
538
+ */
539
+ this.keyboardAccessibilityMode = false;
540
+
541
+ /**
542
+ * The list of top-level bounded elements on the workspace.
543
+ * @type {!Array<!IBoundedElement>}
544
+ * @private
545
+ */
546
+ this.topBoundedElements_ = [];
547
+
548
+ /**
549
+ * The recorded drag targets.
550
+ * @type {!Array<
551
+ * {
552
+ * component: !IDragTarget,
553
+ * clientRect: !Rect
554
+ * }>}
555
+ * @private
556
+ */
557
+ this.dragTargetAreas_ = [];
558
+
559
+ /**
560
+ * The cached size of the parent svg element.
561
+ * Used to compute svg metrics.
562
+ * @type {!Size}
563
+ * @private
564
+ */
565
+ this.cachedParentSvgSize_ = new Size(0, 0);
566
+ }
132
567
 
133
- const MetricsManagerClass = registry.getClassFromOptions(
134
- registry.Type.METRICS_MANAGER, options, true);
135
568
  /**
136
- * Object in charge of calculating metrics for the workspace.
137
- * @type {!IMetricsManager}
138
- * @private
569
+ * Get the marker manager for this workspace.
570
+ * @return {!MarkerManager} The marker manager.
139
571
  */
140
- this.metricsManager_ = new MetricsManagerClass(this);
572
+ getMarkerManager() {
573
+ return this.markerManager_;
574
+ }
141
575
 
142
576
  /**
143
- * Method to get all the metrics that have to do with a workspace.
144
- * @type {function():!Metrics}
145
- * @package
577
+ * Gets the metrics manager for this workspace.
578
+ * @return {!IMetricsManager} The metrics manager.
579
+ * @public
146
580
  */
147
- this.getMetrics = options.getMetrics ||
148
- this.metricsManager_.getMetrics.bind(this.metricsManager_);
581
+ getMetricsManager() {
582
+ return this.metricsManager_;
583
+ }
149
584
 
150
585
  /**
151
- * Translates the workspace.
152
- * @type {function(!{x:number, y:number}):void}
586
+ * Sets the metrics manager for the workspace.
587
+ * @param {!IMetricsManager} metricsManager The metrics manager.
153
588
  * @package
154
589
  */
155
- this.setMetrics =
156
- options.setMetrics || WorkspaceSvg.setTopLevelWorkspaceMetrics_;
590
+ setMetricsManager(metricsManager) {
591
+ this.metricsManager_ = metricsManager;
592
+ this.getMetrics =
593
+ this.metricsManager_.getMetrics.bind(this.metricsManager_);
594
+ }
157
595
 
158
596
  /**
159
- * @type {!ComponentManager}
160
- * @private
597
+ * Gets the component manager for this workspace.
598
+ * @return {!ComponentManager} The component manager.
599
+ * @public
161
600
  */
162
- this.componentManager_ = new ComponentManager();
163
-
164
- this.connectionDBList = ConnectionDB.init(this.connectionChecker);
601
+ getComponentManager() {
602
+ return this.componentManager_;
603
+ }
165
604
 
166
- if (opt_blockDragSurface) {
167
- this.blockDragSurface_ = opt_blockDragSurface;
605
+ /**
606
+ * Add the cursor SVG to this workspaces SVG group.
607
+ * @param {SVGElement} cursorSvg The SVG root of the cursor to be added to the
608
+ * workspace SVG group.
609
+ * @package
610
+ */
611
+ setCursorSvg(cursorSvg) {
612
+ this.markerManager_.setCursorSvg(cursorSvg);
168
613
  }
169
614
 
170
- if (opt_wsDragSurface) {
171
- this.workspaceDragSurface_ = opt_wsDragSurface;
615
+ /**
616
+ * Add the marker SVG to this workspaces SVG group.
617
+ * @param {SVGElement} markerSvg The SVG root of the marker to be added to the
618
+ * workspace SVG group.
619
+ * @package
620
+ */
621
+ setMarkerSvg(markerSvg) {
622
+ this.markerManager_.setMarkerSvg(markerSvg);
172
623
  }
173
624
 
174
- this.useWorkspaceDragSurface_ =
175
- !!this.workspaceDragSurface_ && svgMath.is3dSupported();
625
+ /**
626
+ * Get the marker with the given ID.
627
+ * @param {string} id The ID of the marker.
628
+ * @return {?Marker} The marker with the given ID or null if no marker
629
+ * with the given ID exists.
630
+ * @package
631
+ */
632
+ getMarker(id) {
633
+ if (this.markerManager_) {
634
+ return this.markerManager_.getMarker(id);
635
+ }
636
+ return null;
637
+ }
176
638
 
177
639
  /**
178
- * List of currently highlighted blocks. Block highlighting is often used to
179
- * visually mark blocks currently being executed.
180
- * @type {!Array<!BlockSvg>}
181
- * @private
640
+ * The cursor for this workspace.
641
+ * @return {?Cursor} The cursor for the workspace.
182
642
  */
183
- this.highlightedBlocks_ = [];
643
+ getCursor() {
644
+ if (this.markerManager_) {
645
+ return this.markerManager_.getCursor();
646
+ }
647
+ return null;
648
+ }
184
649
 
185
650
  /**
186
- * Object in charge of loading, storing, and playing audio for a workspace.
187
- * @type {!WorkspaceAudio}
188
- * @private
651
+ * Get the block renderer attached to this workspace.
652
+ * @return {!Renderer} The renderer attached to this
653
+ * workspace.
189
654
  */
190
- this.audioManager_ = new WorkspaceAudio(
191
- /** @type {WorkspaceSvg} */ (options.parentWorkspace));
655
+ getRenderer() {
656
+ return this.renderer_;
657
+ }
192
658
 
193
659
  /**
194
- * This workspace's grid object or null.
195
- * @type {Grid}
196
- * @private
660
+ * Get the theme manager for this workspace.
661
+ * @return {!ThemeManager} The theme manager for this workspace.
662
+ * @package
197
663
  */
198
- this.grid_ = this.options.gridPattern ?
199
- new Grid(this.options.gridPattern, options.gridOptions) :
200
- null;
664
+ getThemeManager() {
665
+ return this.themeManager_;
666
+ }
201
667
 
202
668
  /**
203
- * Manager in charge of markers and cursors.
204
- * @type {!MarkerManager}
205
- * @private
669
+ * Get the workspace theme object.
670
+ * @return {!Theme} The workspace theme object.
206
671
  */
207
- this.markerManager_ = new MarkerManager(this);
672
+ getTheme() {
673
+ return this.themeManager_.getTheme();
674
+ }
208
675
 
209
676
  /**
210
- * Map from function names to callbacks, for deciding what to do when a custom
211
- * toolbox category is opened.
212
- * @type {!Object<string, ?function(!Workspace):
213
- * !toolbox.FlyoutDefinition>}
214
- * @private
677
+ * Set the workspace theme object.
678
+ * If no theme is passed, default to the `Classic` theme.
679
+ * @param {Theme} theme The workspace theme object.
215
680
  */
216
- this.toolboxCategoryCallbacks_ = Object.create(null);
681
+ setTheme(theme) {
682
+ if (!theme) {
683
+ theme = /** @type {!Theme} */ (Classic);
684
+ }
685
+ this.themeManager_.setTheme(theme);
686
+ }
217
687
 
218
688
  /**
219
- * Map from function names to callbacks, for deciding what to do when a button
220
- * is clicked.
221
- * @type {!Object<string, ?function(!FlyoutButton)>}
222
- * @private
689
+ * Refresh all blocks on the workspace after a theme update.
690
+ * @package
223
691
  */
224
- this.flyoutButtonCallbacks_ = Object.create(null);
692
+ refreshTheme() {
693
+ if (this.svgGroup_) {
694
+ this.renderer_.refreshDom(this.svgGroup_, this.getTheme());
695
+ }
696
+
697
+ // Update all blocks in workspace that have a style name.
698
+ this.updateBlockStyles_(this.getAllBlocks(false).filter(function(block) {
699
+ return !!block.getStyleName();
700
+ }));
701
+
702
+ // Update current toolbox selection.
703
+ this.refreshToolboxSelection();
704
+ if (this.toolbox_) {
705
+ this.toolbox_.refreshTheme();
706
+ }
225
707
 
226
- const Variables = goog.module.get('Blockly.Variables');
227
- if (Variables && Variables.flyoutCategory) {
228
- this.registerToolboxCategoryCallback(
229
- Variables.CATEGORY_NAME, Variables.flyoutCategory);
708
+ // Re-render if workspace is visible
709
+ if (this.isVisible()) {
710
+ this.setVisible(true);
711
+ }
712
+
713
+ const event = new (eventUtils.get(eventUtils.THEME_CHANGE))(
714
+ this.getTheme().name, this.id);
715
+ eventUtils.fire(event);
230
716
  }
231
717
 
232
- const VariablesDynamic = goog.module.get('Blockly.VariablesDynamic');
233
- if (VariablesDynamic && VariablesDynamic.flyoutCategory) {
234
- this.registerToolboxCategoryCallback(
235
- VariablesDynamic.CATEGORY_NAME, VariablesDynamic.flyoutCategory);
718
+ /**
719
+ * Updates all the blocks with new style.
720
+ * @param {!Array<!Block>} blocks List of blocks to update the style
721
+ * on.
722
+ * @private
723
+ */
724
+ updateBlockStyles_(blocks) {
725
+ for (let i = 0, block; (block = blocks[i]); i++) {
726
+ const blockStyleName = block.getStyleName();
727
+ if (blockStyleName) {
728
+ block.setStyle(blockStyleName);
729
+ if (block.mutator) {
730
+ block.mutator.updateBlockStyle();
731
+ }
732
+ }
733
+ }
236
734
  }
237
735
 
238
- const Procedures = goog.module.get('Blockly.Procedures');
239
- if (Procedures && Procedures.flyoutCategory) {
240
- this.registerToolboxCategoryCallback(
241
- Procedures.CATEGORY_NAME, Procedures.flyoutCategory);
242
- this.addChangeListener(Procedures.mutatorOpenListener);
736
+ /**
737
+ * Getter for the inverted screen CTM.
738
+ * @return {?SVGMatrix} The matrix to use in mouseToSvg
739
+ */
740
+ getInverseScreenCTM() {
741
+ // Defer getting the screen CTM until we actually need it, this should
742
+ // avoid forced reflows from any calls to updateInverseScreenCTM.
743
+ if (this.inverseScreenCTMDirty_) {
744
+ const ctm = this.getParentSvg().getScreenCTM();
745
+ if (ctm) {
746
+ this.inverseScreenCTM_ = ctm.inverse();
747
+ this.inverseScreenCTMDirty_ = false;
748
+ }
749
+ }
750
+
751
+ return this.inverseScreenCTM_;
243
752
  }
244
753
 
245
754
  /**
246
- * Object in charge of storing and updating the workspace theme.
247
- * @type {!ThemeManager}
248
- * @protected
755
+ * Mark the inverse screen CTM as dirty.
249
756
  */
250
- this.themeManager_ = this.options.parentWorkspace ?
251
- this.options.parentWorkspace.getThemeManager() :
252
- new ThemeManager(this, this.options.theme || Classic);
253
- this.themeManager_.subscribeWorkspace(this);
757
+ updateInverseScreenCTM() {
758
+ this.inverseScreenCTMDirty_ = true;
759
+ }
254
760
 
255
761
  /**
256
- * The block renderer used for rendering blocks on this workspace.
257
- * @type {!Renderer}
258
- * @private
762
+ * Getter for isVisible
763
+ * @return {boolean} Whether the workspace is visible.
764
+ * False if the workspace has been hidden by calling `setVisible(false)`.
259
765
  */
260
- this.renderer_ = blockRendering.init(
261
- this.options.renderer || 'geras', this.getTheme(),
262
- this.options.rendererOverrides);
766
+ isVisible() {
767
+ return this.isVisible_;
768
+ }
263
769
 
264
770
  /**
265
- * Cached parent SVG.
266
- * @type {SVGElement}
267
- * @private
771
+ * Return the absolute coordinates of the top-left corner of this element,
772
+ * scales that after canvas SVG element, if it's a descendant.
773
+ * The origin (0,0) is the top-left corner of the Blockly SVG.
774
+ * @param {!SVGElement} element SVG element to find the coordinates of.
775
+ * @return {!Coordinate} Object with .x and .y properties.
776
+ * @package
268
777
  */
269
- this.cachedParentSvg_ = null;
778
+ getSvgXY(element) {
779
+ let x = 0;
780
+ let y = 0;
781
+ let scale = 1;
782
+ if (dom.containsNode(this.getCanvas(), element) ||
783
+ dom.containsNode(this.getBubbleCanvas(), element)) {
784
+ // Before the SVG canvas, scale the coordinates.
785
+ scale = this.scale;
786
+ }
787
+ do {
788
+ // Loop through this block and every parent.
789
+ const xy = svgMath.getRelativeXY(element);
790
+ if (element === this.getCanvas() || element === this.getBubbleCanvas()) {
791
+ // After the SVG canvas, don't scale the coordinates.
792
+ scale = 1;
793
+ }
794
+ x += xy.x * scale;
795
+ y += xy.y * scale;
796
+ element = /** @type {!SVGElement} */ (element.parentNode);
797
+ } while (element && element !== this.getParentSvg());
798
+ return new Coordinate(x, y);
799
+ }
270
800
 
271
801
  /**
272
- * True if keyboard accessibility mode is on, false otherwise.
273
- * @type {boolean}
802
+ * Gets the size of the workspace's parent SVG element.
803
+ * @return {!Size} The cached width and height of the workspace's
804
+ * parent SVG element.
805
+ * @package
274
806
  */
275
- this.keyboardAccessibilityMode = false;
807
+ getCachedParentSvgSize() {
808
+ const size = this.cachedParentSvgSize_;
809
+ return new Size(size.width, size.height);
810
+ }
276
811
 
277
812
  /**
278
- * The list of top-level bounded elements on the workspace.
279
- * @type {!Array<!IBoundedElement>}
280
- * @private
813
+ * Return the position of the workspace origin relative to the injection div
814
+ * origin in pixels.
815
+ * The workspace origin is where a block would render at position (0, 0).
816
+ * It is not the upper left corner of the workspace SVG.
817
+ * @return {!Coordinate} Offset in pixels.
818
+ * @package
281
819
  */
282
- this.topBoundedElements_ = [];
820
+ getOriginOffsetInPixels() {
821
+ return svgMath.getInjectionDivXY(this.getCanvas());
822
+ }
283
823
 
284
824
  /**
285
- * The recorded drag targets.
286
- * @type {!Array<
287
- * {
288
- * component: !IDragTarget,
289
- * clientRect: !Rect
290
- * }>}
291
- * @private
825
+ * Return the injection div that is a parent of this workspace.
826
+ * Walks the DOM the first time it's called, then returns a cached value.
827
+ * Note: We assume this is only called after the workspace has been injected
828
+ * into the DOM.
829
+ * @return {!Element} The first parent div with 'injectionDiv' in the name.
830
+ * @package
292
831
  */
293
- this.dragTargetAreas_ = [];
832
+ getInjectionDiv() {
833
+ // NB: it would be better to pass this in at createDom, but is more likely
834
+ // to break existing uses of Blockly.
835
+ if (!this.injectionDiv_) {
836
+ let element = this.svgGroup_;
837
+ while (element) {
838
+ const classes = element.getAttribute('class') || '';
839
+ if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {
840
+ this.injectionDiv_ = element;
841
+ break;
842
+ }
843
+ element = /** @type {!Element} */ (element.parentNode);
844
+ }
845
+ }
846
+ return /** @type {!Element} */ (this.injectionDiv_);
847
+ }
294
848
 
295
849
  /**
296
- * The cached size of the parent svg element.
297
- * Used to compute svg metrics.
298
- * @type {!Size}
299
- * @private
850
+ * Get the SVG block canvas for the workspace.
851
+ * @return {?SVGElement} The SVG group for the workspace.
852
+ * @package
300
853
  */
301
- this.cachedParentSvgSize_ = new Size(0, 0);
302
- };
303
- object.inherits(WorkspaceSvg, Workspace);
854
+ getBlockCanvas() {
855
+ return this.svgBlockCanvas_;
856
+ }
304
857
 
305
- /**
306
- * A wrapper function called when a resize event occurs.
307
- * You can pass the result to `eventHandling.unbind`.
308
- * @type {?browserEvents.Data}
309
- * @private
310
- */
311
- WorkspaceSvg.prototype.resizeHandlerWrapper_ = null;
858
+ /**
859
+ * Save resize handler data so we can delete it later in dispose.
860
+ * @param {!browserEvents.Data} handler Data that can be passed to
861
+ * eventHandling.unbind.
862
+ */
863
+ setResizeHandlerWrapper(handler) {
864
+ this.resizeHandlerWrapper_ = handler;
865
+ }
312
866
 
313
- /**
314
- * The render status of an SVG workspace.
315
- * Returns `false` for headless workspaces and true for instances of
316
- * `WorkspaceSvg`.
317
- * @type {boolean}
318
- */
319
- WorkspaceSvg.prototype.rendered = true;
867
+ /**
868
+ * Create the workspace DOM elements.
869
+ * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
870
+ * 'blocklyMutatorBackground'.
871
+ * @return {!Element} The workspace's SVG group.
872
+ */
873
+ createDom(opt_backgroundClass) {
874
+ /**
875
+ * <g class="blocklyWorkspace">
876
+ * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
877
+ * [Trashcan and/or flyout may go here]
878
+ * <g class="blocklyBlockCanvas"></g>
879
+ * <g class="blocklyBubbleCanvas"></g>
880
+ * </g>
881
+ * @type {SVGElement}
882
+ */
883
+ this.svgGroup_ =
884
+ dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}, null);
885
+
886
+ // Note that a <g> alone does not receive mouse events--it must have a
887
+ // valid target inside it. If no background class is specified, as in the
888
+ // flyout, the workspace will not receive mouse events.
889
+ if (opt_backgroundClass) {
890
+ /** @type {SVGElement} */
891
+ this.svgBackground_ = dom.createSvgElement(
892
+ Svg.RECT,
893
+ {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
894
+ this.svgGroup_);
895
+
896
+ if (opt_backgroundClass === 'blocklyMainBackground' && this.grid_) {
897
+ this.svgBackground_.style.fill =
898
+ 'url(#' + this.grid_.getPatternId() + ')';
899
+ } else {
900
+ this.themeManager_.subscribe(
901
+ this.svgBackground_, 'workspaceBackgroundColour', 'fill');
902
+ }
903
+ }
904
+ /** @type {SVGElement} */
905
+ this.svgBlockCanvas_ = dom.createSvgElement(
906
+ Svg.G, {'class': 'blocklyBlockCanvas'}, this.svgGroup_);
907
+ /** @type {SVGElement} */
908
+ this.svgBubbleCanvas_ = dom.createSvgElement(
909
+ Svg.G, {'class': 'blocklyBubbleCanvas'}, this.svgGroup_);
910
+
911
+ if (!this.isFlyout) {
912
+ browserEvents.conditionalBind(
913
+ this.svgGroup_, 'mousedown', this, this.onMouseDown_, false, true);
914
+ // This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683,
915
+ // which otherwise prevents zoom/scroll events from being observed in
916
+ // Safari. Once that bug is fixed it should be removed.
917
+ document.body.addEventListener('wheel', function() {});
918
+ browserEvents.conditionalBind(
919
+ this.svgGroup_, 'wheel', this, this.onMouseWheel_);
920
+ }
320
921
 
321
- /**
322
- * Whether the workspace is visible. False if the workspace has been hidden
323
- * by calling `setVisible(false)`.
324
- * @type {boolean}
325
- * @private
326
- */
327
- WorkspaceSvg.prototype.isVisible_ = true;
922
+ // Determine if there needs to be a category tree, or a simple list of
923
+ // blocks. This cannot be changed later, since the UI is very different.
924
+ if (this.options.hasCategories) {
925
+ const ToolboxClass = registry.getClassFromOptions(
926
+ registry.Type.TOOLBOX, this.options, true);
927
+ this.toolbox_ = new ToolboxClass(this);
928
+ }
929
+ if (this.grid_) {
930
+ this.grid_.update(this.scale);
931
+ }
932
+ this.recordDragTargets();
933
+ const CursorClass =
934
+ registry.getClassFromOptions(registry.Type.CURSOR, this.options);
328
935
 
329
- /**
330
- * Is this workspace the surface for a flyout?
331
- * @type {boolean}
332
- */
333
- WorkspaceSvg.prototype.isFlyout = false;
936
+ CursorClass && this.markerManager_.setCursor(new CursorClass());
334
937
 
335
- /**
336
- * Is this workspace the surface for a mutator?
337
- * @type {boolean}
338
- * @package
339
- */
340
- WorkspaceSvg.prototype.isMutator = false;
938
+ this.renderer_.createDom(this.svgGroup_, this.getTheme());
939
+ return this.svgGroup_;
940
+ }
341
941
 
342
- /**
343
- * Whether this workspace has resizes enabled.
344
- * Disable during batch operations for a performance improvement.
345
- * @type {boolean}
346
- * @private
347
- */
348
- WorkspaceSvg.prototype.resizesEnabled_ = true;
942
+ /**
943
+ * Dispose of this workspace.
944
+ * Unlink from all DOM elements to prevent memory leaks.
945
+ * @suppress {checkTypes}
946
+ */
947
+ dispose() {
948
+ // Stop rerendering.
949
+ this.rendered = false;
950
+ if (this.currentGesture_) {
951
+ this.currentGesture_.cancel();
952
+ }
953
+ if (this.svgGroup_) {
954
+ dom.removeNode(this.svgGroup_);
955
+ this.svgGroup_ = null;
956
+ }
957
+ this.svgBlockCanvas_ = null;
958
+ this.svgBubbleCanvas_ = null;
959
+ if (this.toolbox_) {
960
+ this.toolbox_.dispose();
961
+ this.toolbox_ = null;
962
+ }
963
+ if (this.flyout_) {
964
+ this.flyout_.dispose();
965
+ this.flyout_ = null;
966
+ }
967
+ if (this.trashcan) {
968
+ this.trashcan.dispose();
969
+ this.trashcan = null;
970
+ }
971
+ if (this.scrollbar) {
972
+ this.scrollbar.dispose();
973
+ this.scrollbar = null;
974
+ }
975
+ if (this.zoomControls_) {
976
+ this.zoomControls_.dispose();
977
+ this.zoomControls_ = null;
978
+ }
349
979
 
350
- /**
351
- * Current horizontal scrolling offset in pixel units, relative to the
352
- * workspace origin.
353
- *
354
- * It is useful to think about a view, and a canvas moving beneath that
355
- * view. As the canvas moves right, this value becomes more positive, and
356
- * the view is now "seeing" the left side of the canvas. As the canvas moves
357
- * left, this value becomes more negative, and the view is now "seeing" the
358
- * right side of the canvas.
359
- *
360
- * The confusing thing about this value is that it does not, and must not
361
- * include the absoluteLeft offset. This is because it is used to calculate
362
- * the viewLeft value.
363
- *
364
- * The viewLeft is relative to the workspace origin (although in pixel
365
- * units). The workspace origin is the top-left corner of the workspace (at
366
- * least when it is enabled). It is shifted from the top-left of the blocklyDiv
367
- * so as not to be beneath the toolbox.
368
- *
369
- * When the workspace is enabled the viewLeft and workspace origin are at
370
- * the same X location. As the canvas slides towards the right beneath the view
371
- * this value (scrollX) becomes more positive, and the viewLeft becomes more
372
- * negative relative to the workspace origin (imagine the workspace origin
373
- * as a dot on the canvas sliding to the right as the canvas moves).
374
- *
375
- * So if the scrollX were to include the absoluteLeft this would in a way
376
- * "unshift" the workspace origin. This means that the viewLeft would be
377
- * representing the left edge of the blocklyDiv, rather than the left edge
378
- * of the workspace.
379
- *
380
- * @type {number}
381
- */
382
- WorkspaceSvg.prototype.scrollX = 0;
980
+ if (this.audioManager_) {
981
+ this.audioManager_.dispose();
982
+ this.audioManager_ = null;
983
+ }
383
984
 
384
- /**
385
- * Current vertical scrolling offset in pixel units, relative to the
386
- * workspace origin.
387
- *
388
- * It is useful to think about a view, and a canvas moving beneath that
389
- * view. As the canvas moves down, this value becomes more positive, and the
390
- * view is now "seeing" the upper part of the canvas. As the canvas moves
391
- * up, this value becomes more negative, and the view is "seeing" the lower
392
- * part of the canvas.
393
- *
394
- * This confusing thing about this value is that it does not, and must not
395
- * include the absoluteTop offset. This is because it is used to calculate
396
- * the viewTop value.
397
- *
398
- * The viewTop is relative to the workspace origin (although in pixel
399
- * units). The workspace origin is the top-left corner of the workspace (at
400
- * least when it is enabled). It is shifted from the top-left of the
401
- * blocklyDiv so as not to be beneath the toolbox.
402
- *
403
- * When the workspace is enabled the viewTop and workspace origin are at the
404
- * same Y location. As the canvas slides towards the bottom this value
405
- * (scrollY) becomes more positive, and the viewTop becomes more negative
406
- * relative to the workspace origin (image in the workspace origin as a dot
407
- * on the canvas sliding downwards as the canvas moves).
408
- *
409
- * So if the scrollY were to include the absoluteTop this would in a way
410
- * "unshift" the workspace origin. This means that the viewTop would be
411
- * representing the top edge of the blocklyDiv, rather than the top edge of
412
- * the workspace.
413
- *
414
- * @type {number}
415
- */
416
- WorkspaceSvg.prototype.scrollY = 0;
985
+ if (this.grid_) {
986
+ this.grid_.dispose();
987
+ this.grid_ = null;
988
+ }
417
989
 
418
- /**
419
- * Horizontal scroll value when scrolling started in pixel units.
420
- * @type {number}
421
- */
422
- WorkspaceSvg.prototype.startScrollX = 0;
990
+ this.renderer_.dispose();
423
991
 
424
- /**
425
- * Vertical scroll value when scrolling started in pixel units.
426
- * @type {number}
427
- */
428
- WorkspaceSvg.prototype.startScrollY = 0;
992
+ if (this.markerManager_) {
993
+ this.markerManager_.dispose();
994
+ this.markerManager_ = null;
995
+ }
429
996
 
430
- /**
431
- * Distance from mouse to object being dragged.
432
- * @type {Coordinate}
433
- * @private
434
- */
435
- WorkspaceSvg.prototype.dragDeltaXY_ = null;
997
+ super.dispose();
436
998
 
437
- /**
438
- * Current scale.
439
- * @type {number}
440
- */
441
- WorkspaceSvg.prototype.scale = 1;
999
+ // Dispose of theme manager after all blocks and mutators are disposed of.
1000
+ if (this.themeManager_) {
1001
+ this.themeManager_.unsubscribeWorkspace(this);
1002
+ this.themeManager_.unsubscribe(this.svgBackground_);
1003
+ if (!this.options.parentWorkspace) {
1004
+ this.themeManager_.dispose();
1005
+ this.themeManager_ = null;
1006
+ }
1007
+ }
442
1008
 
443
- /**
444
- * Cached scale value. Used to detect changes in viewport.
445
- * @type {number}
446
- * @private
447
- */
448
- WorkspaceSvg.prototype.oldScale_ = 1;
1009
+ this.connectionDBList = null;
449
1010
 
450
- /**
451
- * Cached viewport top value. Used to detect changes in viewport.
452
- * @type {number}
453
- * @private
454
- */
455
- WorkspaceSvg.prototype.oldTop_ = 0;
1011
+ this.toolboxCategoryCallbacks_ = null;
1012
+ this.flyoutButtonCallbacks_ = null;
456
1013
 
457
- /**
458
- * Cached viewport left value. Used to detect changes in viewport.
459
- * @type {number}
460
- * @private
461
- */
462
- WorkspaceSvg.prototype.oldLeft_ = 0;
1014
+ if (!this.options.parentWorkspace) {
1015
+ // Top-most workspace. Dispose of the div that the
1016
+ // SVG is injected into (i.e. injectionDiv).
1017
+ const parentSvg = this.getParentSvg();
1018
+ if (parentSvg && parentSvg.parentNode) {
1019
+ dom.removeNode(parentSvg.parentNode);
1020
+ }
1021
+ }
1022
+ if (this.resizeHandlerWrapper_) {
1023
+ browserEvents.unbind(this.resizeHandlerWrapper_);
1024
+ this.resizeHandlerWrapper_ = null;
1025
+ }
1026
+ }
463
1027
 
464
- /**
465
- * The workspace's trashcan (if any).
466
- * @type {Trashcan}
467
- */
468
- WorkspaceSvg.prototype.trashcan = null;
1028
+ /**
1029
+ * Obtain a newly created block.
1030
+ *
1031
+ * This block's SVG must still be initialized
1032
+ * ([initSvg]{@link BlockSvg#initSvg}) and it must be rendered
1033
+ * ([render]{@link BlockSvg#render}) before the block will be visible.
1034
+ * @param {!string} prototypeName Name of the language object containing
1035
+ * type-specific functions for this block.
1036
+ * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
1037
+ * create a new ID.
1038
+ * @return {!BlockSvg} The created block.
1039
+ * @override
1040
+ */
1041
+ newBlock(prototypeName, opt_id) {
1042
+ return new BlockSvg(this, prototypeName, opt_id);
1043
+ }
469
1044
 
470
- /**
471
- * This workspace's scrollbars, if they exist.
472
- * @type {ScrollbarPair}
473
- */
474
- WorkspaceSvg.prototype.scrollbar = null;
1045
+ /**
1046
+ * Add a trashcan.
1047
+ * @package
1048
+ */
1049
+ addTrashcan() {
1050
+ const {Trashcan} = goog.module.get('Blockly.Trashcan');
1051
+ if (!Trashcan) {
1052
+ throw Error('Missing require for Blockly.Trashcan');
1053
+ }
1054
+ /** @type {Trashcan} */
1055
+ this.trashcan = new Trashcan(this);
1056
+ const svgTrashcan = this.trashcan.createDom();
1057
+ this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
1058
+ }
475
1059
 
476
- /**
477
- * Fixed flyout providing blocks which may be dragged into this workspace.
478
- * @type {IFlyout}
479
- * @private
480
- */
481
- WorkspaceSvg.prototype.flyout_ = null;
1060
+ /**
1061
+ * Add zoom controls.
1062
+ * @package
1063
+ */
1064
+ addZoomControls() {
1065
+ const {ZoomControls} = goog.module.get('Blockly.ZoomControls');
1066
+ if (!ZoomControls) {
1067
+ throw Error('Missing require for Blockly.ZoomControls');
1068
+ }
1069
+ /** @type {ZoomControls} */
1070
+ this.zoomControls_ = new ZoomControls(this);
1071
+ const svgZoomControls = this.zoomControls_.createDom();
1072
+ this.svgGroup_.appendChild(svgZoomControls);
1073
+ }
482
1074
 
483
- /**
484
- * Category-based toolbox providing blocks which may be dragged into this
485
- * workspace.
486
- * @type {IToolbox}
487
- * @private
488
- */
489
- WorkspaceSvg.prototype.toolbox_ = null;
1075
+ /**
1076
+ * Add a flyout element in an element with the given tag name.
1077
+ * @param {string|
1078
+ * !Svg<!SVGSVGElement>|
1079
+ * !Svg<!SVGGElement>} tagName What type of tag the
1080
+ * flyout belongs in.
1081
+ * @return {!Element} The element containing the flyout DOM.
1082
+ * @package
1083
+ */
1084
+ addFlyout(tagName) {
1085
+ const workspaceOptions = new Options(
1086
+ /** @type {!BlocklyOptions} */
1087
+ ({
1088
+ 'parentWorkspace': this,
1089
+ 'rtl': this.RTL,
1090
+ 'oneBasedIndex': this.options.oneBasedIndex,
1091
+ 'horizontalLayout': this.horizontalLayout,
1092
+ 'renderer': this.options.renderer,
1093
+ 'rendererOverrides': this.options.rendererOverrides,
1094
+ 'move': {
1095
+ 'scrollbars': true,
1096
+ },
1097
+ }));
1098
+ workspaceOptions.toolboxPosition = this.options.toolboxPosition;
1099
+ if (this.horizontalLayout) {
1100
+ const HorizontalFlyout = registry.getClassFromOptions(
1101
+ registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, this.options, true);
1102
+ this.flyout_ = new HorizontalFlyout(workspaceOptions);
1103
+ } else {
1104
+ const VerticalFlyout = registry.getClassFromOptions(
1105
+ registry.Type.FLYOUTS_VERTICAL_TOOLBOX, this.options, true);
1106
+ this.flyout_ = new VerticalFlyout(workspaceOptions);
1107
+ }
1108
+ this.flyout_.autoClose = false;
1109
+ this.flyout_.getWorkspace().setVisible(true);
490
1110
 
491
- /**
492
- * The current gesture in progress on this workspace, if any.
493
- * @type {TouchGesture}
494
- * @private
495
- */
496
- WorkspaceSvg.prototype.currentGesture_ = null;
1111
+ // Return the element so that callers can place it in their desired
1112
+ // spot in the DOM. For example, mutator flyouts do not go in the same
1113
+ // place as main workspace flyouts.
1114
+ return this.flyout_.createDom(tagName);
1115
+ }
497
1116
 
498
- /**
499
- * This workspace's surface for dragging blocks, if it exists.
500
- * @type {BlockDragSurfaceSvg}
501
- * @private
502
- */
503
- WorkspaceSvg.prototype.blockDragSurface_ = null;
1117
+ /**
1118
+ * Getter for the flyout associated with this workspace. This flyout may be
1119
+ * owned by either the toolbox or the workspace, depending on toolbox
1120
+ * configuration. It will be null if there is no flyout.
1121
+ * @param {boolean=} opt_own Whether to only return the workspace's own
1122
+ * flyout.
1123
+ * @return {?IFlyout} The flyout on this workspace.
1124
+ * @package
1125
+ */
1126
+ getFlyout(opt_own) {
1127
+ if (this.flyout_ || opt_own) {
1128
+ return this.flyout_;
1129
+ }
1130
+ if (this.toolbox_) {
1131
+ return this.toolbox_.getFlyout();
1132
+ }
1133
+ return null;
1134
+ }
504
1135
 
505
- /**
506
- * This workspace's drag surface, if it exists.
507
- * @type {WorkspaceDragSurfaceSvg}
508
- * @private
509
- */
510
- WorkspaceSvg.prototype.workspaceDragSurface_ = null;
1136
+ /**
1137
+ * Getter for the toolbox associated with this workspace, if one exists.
1138
+ * @return {?IToolbox} The toolbox on this workspace.
1139
+ * @package
1140
+ */
1141
+ getToolbox() {
1142
+ return this.toolbox_;
1143
+ }
511
1144
 
512
- /**
513
- * Whether to move workspace to the drag surface when it is dragged.
514
- * True if it should move, false if it should be translated directly.
515
- * @type {boolean}
516
- * @private
517
- */
518
- WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
1145
+ /**
1146
+ * Update items that use screen coordinate calculations
1147
+ * because something has changed (e.g. scroll position, window size).
1148
+ * @private
1149
+ */
1150
+ updateScreenCalculations_() {
1151
+ this.updateInverseScreenCTM();
1152
+ this.recordDragTargets();
1153
+ }
519
1154
 
520
- /**
521
- * Whether the drag surface is actively in use. When true, calls to
522
- * translate will translate the drag surface instead of the translating the
523
- * workspace directly.
524
- * This is set to true in setupDragSurface and to false in resetDragSurface.
525
- * @type {boolean}
526
- * @private
527
- */
528
- WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
1155
+ /**
1156
+ * If enabled, resize the parts of the workspace that change when the
1157
+ * workspace contents (e.g. block positions) change. This will also scroll
1158
+ * the workspace contents if needed.
1159
+ * @package
1160
+ */
1161
+ resizeContents() {
1162
+ if (!this.resizesEnabled_ || !this.rendered) {
1163
+ return;
1164
+ }
1165
+ if (this.scrollbar) {
1166
+ this.scrollbar.resize();
1167
+ }
1168
+ this.updateInverseScreenCTM();
1169
+ }
529
1170
 
530
- /**
531
- * The first parent div with 'injectionDiv' in the name, or null if not set.
532
- * Access this with getInjectionDiv.
533
- * @type {Element}
534
- * @private
535
- */
536
- WorkspaceSvg.prototype.injectionDiv_ = null;
1171
+ /**
1172
+ * Resize and reposition all of the workspace chrome (toolbox,
1173
+ * trash, scrollbars etc.)
1174
+ * This should be called when something changes that
1175
+ * requires recalculating dimensions and positions of the
1176
+ * trash, zoom, toolbox, etc. (e.g. window resize).
1177
+ */
1178
+ resize() {
1179
+ if (this.toolbox_) {
1180
+ this.toolbox_.position();
1181
+ }
1182
+ if (this.flyout_) {
1183
+ this.flyout_.position();
1184
+ }
537
1185
 
538
- /**
539
- * Last known position of the page scroll.
540
- * This is used to determine whether we have recalculated screen coordinate
541
- * stuff since the page scrolled.
542
- * @type {Coordinate}
543
- * @private
544
- */
545
- WorkspaceSvg.prototype.lastRecordedPageScroll_ = null;
1186
+ const positionables = this.componentManager_.getComponents(
1187
+ ComponentManager.Capability.POSITIONABLE, true);
1188
+ const metrics = this.getMetricsManager().getUiMetrics();
1189
+ const savedPositions = [];
1190
+ for (let i = 0, positionable; (positionable = positionables[i]); i++) {
1191
+ positionable.position(metrics, savedPositions);
1192
+ const boundingRect = positionable.getBoundingRectangle();
1193
+ if (boundingRect) {
1194
+ savedPositions.push(boundingRect);
1195
+ }
1196
+ }
546
1197
 
547
- /**
548
- * Developers may define this function to add custom menu options to the
549
- * workspace's context menu or edit the workspace-created set of menu options.
550
- * @param {!Array<!Object>} options List of menu options to add to.
551
- * @param {!Event} e The right-click event that triggered the context menu.
552
- */
553
- WorkspaceSvg.prototype.configureContextMenu;
1198
+ if (this.scrollbar) {
1199
+ this.scrollbar.resize();
1200
+ }
1201
+ this.updateScreenCalculations_();
1202
+ }
554
1203
 
555
- /**
556
- * In a flyout, the target workspace where blocks should be placed after a drag.
557
- * Otherwise null.
558
- * @type {WorkspaceSvg}
559
- * @package
560
- */
561
- WorkspaceSvg.prototype.targetWorkspace = null;
1204
+ /**
1205
+ * Resizes and repositions workspace chrome if the page has a new
1206
+ * scroll position.
1207
+ * @package
1208
+ */
1209
+ updateScreenCalculationsIfScrolled() {
1210
+ /* eslint-disable indent */
1211
+ const currScroll = svgMath.getDocumentScroll();
1212
+ if (!Coordinate.equals(this.lastRecordedPageScroll_, currScroll)) {
1213
+ this.lastRecordedPageScroll_ = currScroll;
1214
+ this.updateScreenCalculations_();
1215
+ }
1216
+ }
1217
+ /* eslint-enable indent */
562
1218
 
563
- /**
564
- * Inverted screen CTM, for use in mouseToSvg.
565
- * @type {?SVGMatrix}
566
- * @private
567
- */
568
- WorkspaceSvg.prototype.inverseScreenCTM_ = null;
1219
+ /**
1220
+ * Get the SVG element that forms the drawing surface.
1221
+ * @return {!SVGGElement} SVG group element.
1222
+ */
1223
+ getCanvas() {
1224
+ return /** @type {!SVGGElement} */ (this.svgBlockCanvas_);
1225
+ }
569
1226
 
570
- /**
571
- * Inverted screen CTM is dirty, recalculate it.
572
- * @type {boolean}
573
- * @private
574
- */
575
- WorkspaceSvg.prototype.inverseScreenCTMDirty_ = true;
576
-
577
- /**
578
- * Get the marker manager for this workspace.
579
- * @return {!MarkerManager} The marker manager.
580
- */
581
- WorkspaceSvg.prototype.getMarkerManager = function() {
582
- return this.markerManager_;
583
- };
584
-
585
- /**
586
- * Gets the metrics manager for this workspace.
587
- * @return {!IMetricsManager} The metrics manager.
588
- * @public
589
- */
590
- WorkspaceSvg.prototype.getMetricsManager = function() {
591
- return this.metricsManager_;
592
- };
593
-
594
- /**
595
- * Sets the metrics manager for the workspace.
596
- * @param {!IMetricsManager} metricsManager The metrics manager.
597
- * @package
598
- */
599
- WorkspaceSvg.prototype.setMetricsManager = function(metricsManager) {
600
- this.metricsManager_ = metricsManager;
601
- this.getMetrics = this.metricsManager_.getMetrics.bind(this.metricsManager_);
602
- };
603
-
604
- /**
605
- * Gets the component manager for this workspace.
606
- * @return {!ComponentManager} The component manager.
607
- * @public
608
- */
609
- WorkspaceSvg.prototype.getComponentManager = function() {
610
- return this.componentManager_;
611
- };
612
-
613
- /**
614
- * Add the cursor SVG to this workspaces SVG group.
615
- * @param {SVGElement} cursorSvg The SVG root of the cursor to be added to the
616
- * workspace SVG group.
617
- * @package
618
- */
619
- WorkspaceSvg.prototype.setCursorSvg = function(cursorSvg) {
620
- this.markerManager_.setCursorSvg(cursorSvg);
621
- };
622
-
623
- /**
624
- * Add the marker SVG to this workspaces SVG group.
625
- * @param {SVGElement} markerSvg The SVG root of the marker to be added to the
626
- * workspace SVG group.
627
- * @package
628
- */
629
- WorkspaceSvg.prototype.setMarkerSvg = function(markerSvg) {
630
- this.markerManager_.setMarkerSvg(markerSvg);
631
- };
632
-
633
- /**
634
- * Get the marker with the given ID.
635
- * @param {string} id The ID of the marker.
636
- * @return {?Marker} The marker with the given ID or null if no marker
637
- * with the given ID exists.
638
- * @package
639
- */
640
- WorkspaceSvg.prototype.getMarker = function(id) {
641
- if (this.markerManager_) {
642
- return this.markerManager_.getMarker(id);
643
- }
644
- return null;
645
- };
646
-
647
- /**
648
- * The cursor for this workspace.
649
- * @return {?Cursor} The cursor for the workspace.
650
- */
651
- WorkspaceSvg.prototype.getCursor = function() {
652
- if (this.markerManager_) {
653
- return this.markerManager_.getCursor();
654
- }
655
- return null;
656
- };
657
-
658
- /**
659
- * Get the block renderer attached to this workspace.
660
- * @return {!Renderer} The renderer attached to this
661
- * workspace.
662
- */
663
- WorkspaceSvg.prototype.getRenderer = function() {
664
- return this.renderer_;
665
- };
666
-
667
- /**
668
- * Get the theme manager for this workspace.
669
- * @return {!ThemeManager} The theme manager for this workspace.
670
- * @package
671
- */
672
- WorkspaceSvg.prototype.getThemeManager = function() {
673
- return this.themeManager_;
674
- };
675
-
676
- /**
677
- * Get the workspace theme object.
678
- * @return {!Theme} The workspace theme object.
679
- */
680
- WorkspaceSvg.prototype.getTheme = function() {
681
- return this.themeManager_.getTheme();
682
- };
683
-
684
- /**
685
- * Set the workspace theme object.
686
- * If no theme is passed, default to the `Classic` theme.
687
- * @param {Theme} theme The workspace theme object.
688
- */
689
- WorkspaceSvg.prototype.setTheme = function(theme) {
690
- if (!theme) {
691
- theme = /** @type {!Theme} */ (Classic);
692
- }
693
- this.themeManager_.setTheme(theme);
694
- };
695
-
696
- /**
697
- * Refresh all blocks on the workspace after a theme update.
698
- * @package
699
- */
700
- WorkspaceSvg.prototype.refreshTheme = function() {
701
- if (this.svgGroup_) {
702
- this.renderer_.refreshDom(this.svgGroup_, this.getTheme());
703
- }
704
-
705
- // Update all blocks in workspace that have a style name.
706
- this.updateBlockStyles_(this.getAllBlocks(false).filter(function(block) {
707
- return !!block.getStyleName();
708
- }));
709
-
710
- // Update current toolbox selection.
711
- this.refreshToolboxSelection();
712
- if (this.toolbox_) {
713
- this.toolbox_.refreshTheme();
1227
+ /**
1228
+ * Caches the width and height of the workspace's parent SVG element for use
1229
+ * with getSvgMetrics.
1230
+ * @param {?number} width The width of the parent SVG element.
1231
+ * @param {?number} height The height of the parent SVG element
1232
+ * @package
1233
+ */
1234
+ setCachedParentSvgSize(width, height) {
1235
+ const svg = this.getParentSvg();
1236
+ if (width != null) {
1237
+ this.cachedParentSvgSize_.width = width;
1238
+ // This is set to support the public (but deprecated) Blockly.svgSize
1239
+ // method.
1240
+ svg.cachedWidth_ = width;
1241
+ }
1242
+ if (height != null) {
1243
+ this.cachedParentSvgSize_.height = height;
1244
+ // This is set to support the public (but deprecated) Blockly.svgSize
1245
+ // method.
1246
+ svg.cachedHeight_ = height;
1247
+ }
714
1248
  }
715
1249
 
716
- // Re-render if workspace is visible
717
- if (this.isVisible()) {
718
- this.setVisible(true);
1250
+ /**
1251
+ * Get the SVG element that forms the bubble surface.
1252
+ * @return {!SVGGElement} SVG group element.
1253
+ */
1254
+ getBubbleCanvas() {
1255
+ return /** @type {!SVGGElement} */ (this.svgBubbleCanvas_);
719
1256
  }
720
1257
 
721
- const event = new (eventUtils.get(eventUtils.THEME_CHANGE))(
722
- this.getTheme().name, this.id);
723
- eventUtils.fire(event);
724
- };
725
-
726
- /**
727
- * Updates all the blocks with new style.
728
- * @param {!Array<!Block>} blocks List of blocks to update the style
729
- * on.
730
- * @private
731
- */
732
- WorkspaceSvg.prototype.updateBlockStyles_ = function(blocks) {
733
- for (let i = 0, block; (block = blocks[i]); i++) {
734
- const blockStyleName = block.getStyleName();
735
- if (blockStyleName) {
736
- block.setStyle(blockStyleName);
737
- if (block.mutator) {
738
- block.mutator.updateBlockStyle();
1258
+ /**
1259
+ * Get the SVG element that contains this workspace.
1260
+ * Note: We assume this is only called after the workspace has been injected
1261
+ * into the DOM.
1262
+ * @return {!SVGElement} SVG element.
1263
+ */
1264
+ getParentSvg() {
1265
+ if (!this.cachedParentSvg_) {
1266
+ let element = this.svgGroup_;
1267
+ while (element) {
1268
+ if (element.tagName === 'svg') {
1269
+ this.cachedParentSvg_ = element;
1270
+ break;
1271
+ }
1272
+ element = /** @type {!SVGElement} */ (element.parentNode);
739
1273
  }
740
1274
  }
1275
+ return /** @type {!SVGElement} */ (this.cachedParentSvg_);
741
1276
  }
742
- };
743
1277
 
744
- /**
745
- * Getter for the inverted screen CTM.
746
- * @return {?SVGMatrix} The matrix to use in mouseToSvg
747
- */
748
- WorkspaceSvg.prototype.getInverseScreenCTM = function() {
749
- // Defer getting the screen CTM until we actually need it, this should
750
- // avoid forced reflows from any calls to updateInverseScreenCTM.
751
- if (this.inverseScreenCTMDirty_) {
752
- const ctm = this.getParentSvg().getScreenCTM();
753
- if (ctm) {
754
- this.inverseScreenCTM_ = ctm.inverse();
755
- this.inverseScreenCTMDirty_ = false;
1278
+ /**
1279
+ * Fires a viewport event if events are enabled and there is a change in
1280
+ * viewport values.
1281
+ * @package
1282
+ */
1283
+ maybeFireViewportChangeEvent() {
1284
+ if (!eventUtils.isEnabled()) {
1285
+ return;
756
1286
  }
757
- }
758
-
759
- return this.inverseScreenCTM_;
760
- };
761
-
762
- /**
763
- * Mark the inverse screen CTM as dirty.
764
- */
765
- WorkspaceSvg.prototype.updateInverseScreenCTM = function() {
766
- this.inverseScreenCTMDirty_ = true;
767
- };
768
-
769
- /**
770
- * Getter for isVisible
771
- * @return {boolean} Whether the workspace is visible.
772
- * False if the workspace has been hidden by calling `setVisible(false)`.
773
- */
774
- WorkspaceSvg.prototype.isVisible = function() {
775
- return this.isVisible_;
776
- };
777
-
778
- /**
779
- * Return the absolute coordinates of the top-left corner of this element,
780
- * scales that after canvas SVG element, if it's a descendant.
781
- * The origin (0,0) is the top-left corner of the Blockly SVG.
782
- * @param {!SVGElement} element SVG element to find the coordinates of.
783
- * @return {!Coordinate} Object with .x and .y properties.
784
- * @package
785
- */
786
- WorkspaceSvg.prototype.getSvgXY = function(element) {
787
- let x = 0;
788
- let y = 0;
789
- let scale = 1;
790
- if (dom.containsNode(this.getCanvas(), element) ||
791
- dom.containsNode(this.getBubbleCanvas(), element)) {
792
- // Before the SVG canvas, scale the coordinates.
793
- scale = this.scale;
794
- }
795
- do {
796
- // Loop through this block and every parent.
797
- const xy = svgMath.getRelativeXY(element);
798
- if (element === this.getCanvas() || element === this.getBubbleCanvas()) {
799
- // After the SVG canvas, don't scale the coordinates.
800
- scale = 1;
801
- }
802
- x += xy.x * scale;
803
- y += xy.y * scale;
804
- element = /** @type {!SVGElement} */ (element.parentNode);
805
- } while (element && element !== this.getParentSvg());
806
- return new Coordinate(x, y);
807
- };
808
-
809
- /**
810
- * Gets the size of the workspace's parent SVG element.
811
- * @return {!Size} The cached width and height of the workspace's
812
- * parent SVG element.
813
- * @package
814
- */
815
- WorkspaceSvg.prototype.getCachedParentSvgSize = function() {
816
- const size = this.cachedParentSvgSize_;
817
- return new Size(size.width, size.height);
818
- };
819
-
820
- /**
821
- * Return the position of the workspace origin relative to the injection div
822
- * origin in pixels.
823
- * The workspace origin is where a block would render at position (0, 0).
824
- * It is not the upper left corner of the workspace SVG.
825
- * @return {!Coordinate} Offset in pixels.
826
- * @package
827
- */
828
- WorkspaceSvg.prototype.getOriginOffsetInPixels = function() {
829
- return svgMath.getInjectionDivXY(this.getCanvas());
830
- };
831
-
832
- /**
833
- * Return the injection div that is a parent of this workspace.
834
- * Walks the DOM the first time it's called, then returns a cached value.
835
- * Note: We assume this is only called after the workspace has been injected
836
- * into the DOM.
837
- * @return {!Element} The first parent div with 'injectionDiv' in the name.
838
- * @package
839
- */
840
- WorkspaceSvg.prototype.getInjectionDiv = function() {
841
- // NB: it would be better to pass this in at createDom, but is more likely to
842
- // break existing uses of Blockly.
843
- if (!this.injectionDiv_) {
844
- let element = this.svgGroup_;
845
- while (element) {
846
- const classes = element.getAttribute('class') || '';
847
- if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {
848
- this.injectionDiv_ = element;
849
- break;
850
- }
851
- element = /** @type {!Element} */ (element.parentNode);
1287
+ const scale = this.scale;
1288
+ const top = -this.scrollY;
1289
+ const left = -this.scrollX;
1290
+ if (scale === this.oldScale_ && Math.abs(top - this.oldTop_) < 1 &&
1291
+ Math.abs(left - this.oldLeft_) < 1) {
1292
+ // Ignore sub-pixel changes in top and left. Due to #4192 there are a lot
1293
+ // of negligible changes in viewport top/left.
1294
+ return;
852
1295
  }
1296
+ const event = new (eventUtils.get(eventUtils.VIEWPORT_CHANGE))(
1297
+ top, left, scale, this.id, this.oldScale_);
1298
+ this.oldScale_ = scale;
1299
+ this.oldTop_ = top;
1300
+ this.oldLeft_ = left;
1301
+ eventUtils.fire(event);
853
1302
  }
854
- return /** @type {!Element} */ (this.injectionDiv_);
855
- };
856
1303
 
857
- /**
858
- * Get the SVG block canvas for the workspace.
859
- * @return {?SVGElement} The SVG group for the workspace.
860
- * @package
861
- */
862
- WorkspaceSvg.prototype.getBlockCanvas = function() {
863
- return this.svgBlockCanvas_;
864
- };
865
-
866
- /**
867
- * Save resize handler data so we can delete it later in dispose.
868
- * @param {!browserEvents.Data} handler Data that can be passed to
869
- * eventHandling.unbind.
870
- */
871
- WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) {
872
- this.resizeHandlerWrapper_ = handler;
873
- };
874
-
875
- /**
876
- * Create the workspace DOM elements.
877
- * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or
878
- * 'blocklyMutatorBackground'.
879
- * @return {!Element} The workspace's SVG group.
880
- */
881
- WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
882
- /**
883
- * <g class="blocklyWorkspace">
884
- * <rect class="blocklyMainBackground" height="100%" width="100%"></rect>
885
- * [Trashcan and/or flyout may go here]
886
- * <g class="blocklyBlockCanvas"></g>
887
- * <g class="blocklyBubbleCanvas"></g>
888
- * </g>
889
- * @type {SVGElement}
890
- */
891
- this.svgGroup_ =
892
- dom.createSvgElement(Svg.G, {'class': 'blocklyWorkspace'}, null);
893
-
894
- // Note that a <g> alone does not receive mouse events--it must have a
895
- // valid target inside it. If no background class is specified, as in the
896
- // flyout, the workspace will not receive mouse events.
897
- if (opt_backgroundClass) {
898
- /** @type {SVGElement} */
899
- this.svgBackground_ = dom.createSvgElement(
900
- Svg.RECT,
901
- {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
902
- this.svgGroup_);
903
-
904
- if (opt_backgroundClass === 'blocklyMainBackground' && this.grid_) {
905
- this.svgBackground_.style.fill =
906
- 'url(#' + this.grid_.getPatternId() + ')';
1304
+ /**
1305
+ * Translate this workspace to new coordinates.
1306
+ * @param {number} x Horizontal translation, in pixel units relative to the
1307
+ * top left of the Blockly div.
1308
+ * @param {number} y Vertical translation, in pixel units relative to the
1309
+ * top left of the Blockly div.
1310
+ */
1311
+ translate(x, y) {
1312
+ if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
1313
+ this.workspaceDragSurface_.translateSurface(x, y);
907
1314
  } else {
908
- this.themeManager_.subscribe(
909
- this.svgBackground_, 'workspaceBackgroundColour', 'fill');
910
- }
911
- }
912
- /** @type {SVGElement} */
913
- this.svgBlockCanvas_ = dom.createSvgElement(
914
- Svg.G, {'class': 'blocklyBlockCanvas'}, this.svgGroup_);
915
- /** @type {SVGElement} */
916
- this.svgBubbleCanvas_ = dom.createSvgElement(
917
- Svg.G, {'class': 'blocklyBubbleCanvas'}, this.svgGroup_);
918
-
919
- if (!this.isFlyout) {
920
- browserEvents.conditionalBind(
921
- this.svgGroup_, 'mousedown', this, this.onMouseDown_, false, true);
922
- // This no-op works around https://bugs.webkit.org/show_bug.cgi?id=226683,
923
- // which otherwise prevents zoom/scroll events from being observed in
924
- // Safari. Once that bug is fixed it should be removed.
925
- document.body.addEventListener('wheel', function() {});
926
- browserEvents.conditionalBind(
927
- this.svgGroup_, 'wheel', this, this.onMouseWheel_);
928
- }
929
-
930
- // Determine if there needs to be a category tree, or a simple list of
931
- // blocks. This cannot be changed later, since the UI is very different.
932
- if (this.options.hasCategories) {
933
- const ToolboxClass =
934
- registry.getClassFromOptions(registry.Type.TOOLBOX, this.options, true);
935
- this.toolbox_ = new ToolboxClass(this);
936
- }
937
- if (this.grid_) {
938
- this.grid_.update(this.scale);
939
- }
940
- this.recordDragTargets();
941
- const CursorClass =
942
- registry.getClassFromOptions(registry.Type.CURSOR, this.options);
943
-
944
- CursorClass && this.markerManager_.setCursor(new CursorClass());
945
-
946
- this.renderer_.createDom(this.svgGroup_, this.getTheme());
947
- return this.svgGroup_;
948
- };
949
-
950
- /**
951
- * Dispose of this workspace.
952
- * Unlink from all DOM elements to prevent memory leaks.
953
- * @suppress {checkTypes}
954
- */
955
- WorkspaceSvg.prototype.dispose = function() {
956
- // Stop rerendering.
957
- this.rendered = false;
958
- if (this.currentGesture_) {
959
- this.currentGesture_.cancel();
960
- }
961
- if (this.svgGroup_) {
962
- dom.removeNode(this.svgGroup_);
963
- this.svgGroup_ = null;
964
- }
965
- this.svgBlockCanvas_ = null;
966
- this.svgBubbleCanvas_ = null;
967
- if (this.toolbox_) {
968
- this.toolbox_.dispose();
969
- this.toolbox_ = null;
970
- }
971
- if (this.flyout_) {
972
- this.flyout_.dispose();
973
- this.flyout_ = null;
974
- }
975
- if (this.trashcan) {
976
- this.trashcan.dispose();
977
- this.trashcan = null;
978
- }
979
- if (this.scrollbar) {
980
- this.scrollbar.dispose();
981
- this.scrollbar = null;
982
- }
983
- if (this.zoomControls_) {
984
- this.zoomControls_.dispose();
985
- this.zoomControls_ = null;
986
- }
1315
+ const translation = 'translate(' + x + ',' + y + ') ' +
1316
+ 'scale(' + this.scale + ')';
1317
+ this.svgBlockCanvas_.setAttribute('transform', translation);
1318
+ this.svgBubbleCanvas_.setAttribute('transform', translation);
1319
+ }
1320
+ // Now update the block drag surface if we're using one.
1321
+ if (this.blockDragSurface_) {
1322
+ this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
1323
+ }
1324
+ // And update the grid if we're using one.
1325
+ if (this.grid_) {
1326
+ this.grid_.moveTo(x, y);
1327
+ }
987
1328
 
988
- if (this.audioManager_) {
989
- this.audioManager_.dispose();
990
- this.audioManager_ = null;
1329
+ this.maybeFireViewportChangeEvent();
991
1330
  }
992
1331
 
993
- if (this.grid_) {
994
- this.grid_.dispose();
995
- this.grid_ = null;
996
- }
1332
+ /**
1333
+ * Called at the end of a workspace drag to take the contents
1334
+ * out of the drag surface and put them back into the workspace SVG.
1335
+ * Does nothing if the workspace drag surface is not enabled.
1336
+ * @package
1337
+ */
1338
+ resetDragSurface() {
1339
+ // Don't do anything if we aren't using a drag surface.
1340
+ if (!this.useWorkspaceDragSurface_) {
1341
+ return;
1342
+ }
997
1343
 
998
- this.renderer_.dispose();
1344
+ this.isDragSurfaceActive_ = false;
999
1345
 
1000
- if (this.markerManager_) {
1001
- this.markerManager_.dispose();
1002
- this.markerManager_ = null;
1346
+ const trans = this.workspaceDragSurface_.getSurfaceTranslation();
1347
+ this.workspaceDragSurface_.clearAndHide(this.svgGroup_);
1348
+ const translation = 'translate(' + trans.x + ',' + trans.y + ') ' +
1349
+ 'scale(' + this.scale + ')';
1350
+ this.svgBlockCanvas_.setAttribute('transform', translation);
1351
+ this.svgBubbleCanvas_.setAttribute('transform', translation);
1003
1352
  }
1004
1353
 
1005
- WorkspaceSvg.superClass_.dispose.call(this);
1006
-
1007
- // Dispose of theme manager after all blocks and mutators are disposed of.
1008
- if (this.themeManager_) {
1009
- this.themeManager_.unsubscribeWorkspace(this);
1010
- this.themeManager_.unsubscribe(this.svgBackground_);
1011
- if (!this.options.parentWorkspace) {
1012
- this.themeManager_.dispose();
1013
- this.themeManager_ = null;
1354
+ /**
1355
+ * Called at the beginning of a workspace drag to move contents of
1356
+ * the workspace to the drag surface.
1357
+ * Does nothing if the drag surface is not enabled.
1358
+ * @package
1359
+ */
1360
+ setupDragSurface() {
1361
+ // Don't do anything if we aren't using a drag surface.
1362
+ if (!this.useWorkspaceDragSurface_) {
1363
+ return;
1014
1364
  }
1015
- }
1016
-
1017
- this.connectionDBList = null;
1018
-
1019
- this.toolboxCategoryCallbacks_ = null;
1020
- this.flyoutButtonCallbacks_ = null;
1021
1365
 
1022
- if (!this.options.parentWorkspace) {
1023
- // Top-most workspace. Dispose of the div that the
1024
- // SVG is injected into (i.e. injectionDiv).
1025
- const parentSvg = this.getParentSvg();
1026
- if (parentSvg && parentSvg.parentNode) {
1027
- dom.removeNode(parentSvg.parentNode);
1366
+ // This can happen if the user starts a drag, mouses up outside of the
1367
+ // document where the mouseup listener is registered (e.g. outside of an
1368
+ // iframe) and then moves the mouse back in the workspace. On mobile and
1369
+ // ff, we get the mouseup outside the frame. On chrome and safari desktop we
1370
+ // do not.
1371
+ if (this.isDragSurfaceActive_) {
1372
+ return;
1028
1373
  }
1029
- }
1030
- if (this.resizeHandlerWrapper_) {
1031
- browserEvents.unbind(this.resizeHandlerWrapper_);
1032
- this.resizeHandlerWrapper_ = null;
1033
- }
1034
- };
1035
-
1036
- /**
1037
- * Obtain a newly created block.
1038
- *
1039
- * This block's SVG must still be initialized
1040
- * ([initSvg]{@link BlockSvg#initSvg}) and it must be rendered
1041
- * ([render]{@link BlockSvg#render}) before the block will be visible.
1042
- * @param {!string} prototypeName Name of the language object containing
1043
- * type-specific functions for this block.
1044
- * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
1045
- * create a new ID.
1046
- * @return {!BlockSvg} The created block.
1047
- * @override
1048
- */
1049
- WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
1050
- return new BlockSvg(this, prototypeName, opt_id);
1051
- };
1052
-
1053
- /**
1054
- * Add a trashcan.
1055
- * @package
1056
- */
1057
- WorkspaceSvg.prototype.addTrashcan = function() {
1058
- const {Trashcan} = goog.module.get('Blockly.Trashcan');
1059
- if (!Trashcan) {
1060
- throw Error('Missing require for Blockly.Trashcan');
1061
- }
1062
- /** @type {Trashcan} */
1063
- this.trashcan = new Trashcan(this);
1064
- const svgTrashcan = this.trashcan.createDom();
1065
- this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
1066
- };
1067
-
1068
- /**
1069
- * Add zoom controls.
1070
- * @package
1071
- */
1072
- WorkspaceSvg.prototype.addZoomControls = function() {
1073
- const {ZoomControls} = goog.module.get('Blockly.ZoomControls');
1074
- if (!ZoomControls) {
1075
- throw Error('Missing require for Blockly.ZoomControls');
1076
- }
1077
- /** @type {ZoomControls} */
1078
- this.zoomControls_ = new ZoomControls(this);
1079
- const svgZoomControls = this.zoomControls_.createDom();
1080
- this.svgGroup_.appendChild(svgZoomControls);
1081
- };
1082
1374
 
1083
- /**
1084
- * Add a flyout element in an element with the given tag name.
1085
- * @param {string|
1086
- * !Svg<!SVGSVGElement>|
1087
- * !Svg<!SVGGElement>} tagName What type of tag the
1088
- * flyout belongs in.
1089
- * @return {!Element} The element containing the flyout DOM.
1090
- * @package
1091
- */
1092
- WorkspaceSvg.prototype.addFlyout = function(tagName) {
1093
- const workspaceOptions = new Options(
1094
- /** @type {!BlocklyOptions} */
1095
- ({
1096
- 'parentWorkspace': this,
1097
- 'rtl': this.RTL,
1098
- 'oneBasedIndex': this.options.oneBasedIndex,
1099
- 'horizontalLayout': this.horizontalLayout,
1100
- 'renderer': this.options.renderer,
1101
- 'rendererOverrides': this.options.rendererOverrides,
1102
- 'move': {
1103
- 'scrollbars': true,
1104
- },
1105
- }));
1106
- workspaceOptions.toolboxPosition = this.options.toolboxPosition;
1107
- if (this.horizontalLayout) {
1108
- const HorizontalFlyout = registry.getClassFromOptions(
1109
- registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, this.options, true);
1110
- this.flyout_ = new HorizontalFlyout(workspaceOptions);
1111
- } else {
1112
- const VerticalFlyout = registry.getClassFromOptions(
1113
- registry.Type.FLYOUTS_VERTICAL_TOOLBOX, this.options, true);
1114
- this.flyout_ = new VerticalFlyout(workspaceOptions);
1115
- }
1116
- this.flyout_.autoClose = false;
1117
- this.flyout_.getWorkspace().setVisible(true);
1118
-
1119
- // Return the element so that callers can place it in their desired
1120
- // spot in the DOM. For example, mutator flyouts do not go in the same place
1121
- // as main workspace flyouts.
1122
- return this.flyout_.createDom(tagName);
1123
- };
1375
+ this.isDragSurfaceActive_ = true;
1124
1376
 
1125
- /**
1126
- * Getter for the flyout associated with this workspace. This flyout may be
1127
- * owned by either the toolbox or the workspace, depending on toolbox
1128
- * configuration. It will be null if there is no flyout.
1129
- * @param {boolean=} opt_own Whether to only return the workspace's own flyout.
1130
- * @return {?IFlyout} The flyout on this workspace.
1131
- * @package
1132
- */
1133
- WorkspaceSvg.prototype.getFlyout = function(opt_own) {
1134
- if (this.flyout_ || opt_own) {
1135
- return this.flyout_;
1377
+ // Figure out where we want to put the canvas back. The order
1378
+ // in the is important because things are layered.
1379
+ const previousElement =
1380
+ /** @type {Element} */ (this.svgBlockCanvas_.previousSibling);
1381
+ const width = parseInt(this.getParentSvg().getAttribute('width'), 10);
1382
+ const height = parseInt(this.getParentSvg().getAttribute('height'), 10);
1383
+ const coord = svgMath.getRelativeXY(this.getCanvas());
1384
+ this.workspaceDragSurface_.setContentsAndShow(
1385
+ this.getCanvas(), this.getBubbleCanvas(), previousElement, width,
1386
+ height, this.scale);
1387
+ this.workspaceDragSurface_.translateSurface(coord.x, coord.y);
1136
1388
  }
1137
- if (this.toolbox_) {
1138
- return this.toolbox_.getFlyout();
1139
- }
1140
- return null;
1141
- };
1142
-
1143
- /**
1144
- * Getter for the toolbox associated with this workspace, if one exists.
1145
- * @return {?IToolbox} The toolbox on this workspace.
1146
- * @package
1147
- */
1148
- WorkspaceSvg.prototype.getToolbox = function() {
1149
- return this.toolbox_;
1150
- };
1151
1389
 
1152
- /**
1153
- * Update items that use screen coordinate calculations
1154
- * because something has changed (e.g. scroll position, window size).
1155
- * @private
1156
- */
1157
- WorkspaceSvg.prototype.updateScreenCalculations_ = function() {
1158
- this.updateInverseScreenCTM();
1159
- this.recordDragTargets();
1160
- };
1161
-
1162
- /**
1163
- * If enabled, resize the parts of the workspace that change when the workspace
1164
- * contents (e.g. block positions) change. This will also scroll the
1165
- * workspace contents if needed.
1166
- * @package
1167
- */
1168
- WorkspaceSvg.prototype.resizeContents = function() {
1169
- if (!this.resizesEnabled_ || !this.rendered) {
1170
- return;
1171
- }
1172
- if (this.scrollbar) {
1173
- this.scrollbar.resize();
1390
+ /**
1391
+ * Gets the drag surface blocks are moved to when a drag is started.
1392
+ * @return {?BlockDragSurfaceSvg} This workspace's block drag surface,
1393
+ * if one is in use.
1394
+ * @package
1395
+ */
1396
+ getBlockDragSurface() {
1397
+ return this.blockDragSurface_;
1174
1398
  }
1175
- this.updateInverseScreenCTM();
1176
- };
1177
1399
 
1178
- /**
1179
- * Resize and reposition all of the workspace chrome (toolbox,
1180
- * trash, scrollbars etc.)
1181
- * This should be called when something changes that
1182
- * requires recalculating dimensions and positions of the
1183
- * trash, zoom, toolbox, etc. (e.g. window resize).
1184
- */
1185
- WorkspaceSvg.prototype.resize = function() {
1186
- if (this.toolbox_) {
1187
- this.toolbox_.position();
1188
- }
1189
- if (this.flyout_) {
1190
- this.flyout_.position();
1400
+ /**
1401
+ * Returns the horizontal offset of the workspace.
1402
+ * Intended for LTR/RTL compatibility in XML.
1403
+ * @return {number} Width.
1404
+ */
1405
+ getWidth() {
1406
+ const metrics = this.getMetrics();
1407
+ return metrics ? metrics.viewWidth / this.scale : 0;
1191
1408
  }
1192
1409
 
1193
- const positionables = this.componentManager_.getComponents(
1194
- ComponentManager.Capability.POSITIONABLE, true);
1195
- const metrics = this.getMetricsManager().getUiMetrics();
1196
- const savedPositions = [];
1197
- for (let i = 0, positionable; (positionable = positionables[i]); i++) {
1198
- positionable.position(metrics, savedPositions);
1199
- const boundingRect = positionable.getBoundingRectangle();
1200
- if (boundingRect) {
1201
- savedPositions.push(boundingRect);
1410
+ /**
1411
+ * Toggles the visibility of the workspace.
1412
+ * Currently only intended for main workspace.
1413
+ * @param {boolean} isVisible True if workspace should be visible.
1414
+ */
1415
+ setVisible(isVisible) {
1416
+ this.isVisible_ = isVisible;
1417
+ if (!this.svgGroup_) {
1418
+ return;
1202
1419
  }
1203
- }
1204
1420
 
1205
- if (this.scrollbar) {
1206
- this.scrollbar.resize();
1207
- }
1208
- this.updateScreenCalculations_();
1209
- };
1210
-
1211
- /**
1212
- * Resizes and repositions workspace chrome if the page has a new
1213
- * scroll position.
1214
- * @package
1215
- */
1216
- WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled = function() {
1217
- /* eslint-disable indent */
1218
- const currScroll = svgMath.getDocumentScroll();
1219
- if (!Coordinate.equals(this.lastRecordedPageScroll_, currScroll)) {
1220
- this.lastRecordedPageScroll_ = currScroll;
1221
- this.updateScreenCalculations_();
1222
- }
1223
- }; /* eslint-enable indent */
1224
-
1225
- /**
1226
- * Get the SVG element that forms the drawing surface.
1227
- * @return {!SVGGElement} SVG group element.
1228
- */
1229
- WorkspaceSvg.prototype.getCanvas = function() {
1230
- return /** @type {!SVGGElement} */ (this.svgBlockCanvas_);
1231
- };
1421
+ // Tell the scrollbar whether its container is visible so it can
1422
+ // tell when to hide itself.
1423
+ if (this.scrollbar) {
1424
+ this.scrollbar.setContainerVisible(isVisible);
1425
+ }
1232
1426
 
1233
- /**
1234
- * Caches the width and height of the workspace's parent SVG element for use
1235
- * with getSvgMetrics.
1236
- * @param {?number} width The width of the parent SVG element.
1237
- * @param {?number} height The height of the parent SVG element
1238
- * @package
1239
- */
1240
- WorkspaceSvg.prototype.setCachedParentSvgSize = function(width, height) {
1241
- const svg = this.getParentSvg();
1242
- if (width) {
1243
- this.cachedParentSvgSize_.width = width;
1244
- // This is set to support the public (but deprecated) Blockly.svgSize
1245
- // method.
1246
- svg.cachedWidth_ = width;
1247
- }
1248
- if (height) {
1249
- this.cachedParentSvgSize_.height = height;
1250
- // This is set to support the public (but deprecated) Blockly.svgSize
1251
- // method.
1252
- svg.cachedHeight_ = height;
1253
- }
1254
- };
1427
+ // Tell the flyout whether its container is visible so it can
1428
+ // tell when to hide itself.
1429
+ if (this.getFlyout()) {
1430
+ this.getFlyout().setContainerVisible(isVisible);
1431
+ }
1255
1432
 
1256
- /**
1257
- * Get the SVG element that forms the bubble surface.
1258
- * @return {!SVGGElement} SVG group element.
1259
- */
1260
- WorkspaceSvg.prototype.getBubbleCanvas = function() {
1261
- return /** @type {!SVGGElement} */ (this.svgBubbleCanvas_);
1262
- };
1433
+ this.getParentSvg().style.display = isVisible ? 'block' : 'none';
1434
+ if (this.toolbox_) {
1435
+ // Currently does not support toolboxes in mutators.
1436
+ this.toolbox_.setVisible(isVisible);
1437
+ }
1438
+ if (isVisible) {
1439
+ const blocks = this.getAllBlocks(false);
1440
+ // Tell each block on the workspace to mark its fields as dirty.
1441
+ for (let i = blocks.length - 1; i >= 0; i--) {
1442
+ blocks[i].markDirty();
1443
+ }
1263
1444
 
1264
- /**
1265
- * Get the SVG element that contains this workspace.
1266
- * Note: We assume this is only called after the workspace has been injected
1267
- * into the DOM.
1268
- * @return {!SVGElement} SVG element.
1269
- */
1270
- WorkspaceSvg.prototype.getParentSvg = function() {
1271
- if (!this.cachedParentSvg_) {
1272
- let element = this.svgGroup_;
1273
- while (element) {
1274
- if (element.tagName === 'svg') {
1275
- this.cachedParentSvg_ = element;
1276
- break;
1445
+ this.render();
1446
+ if (this.toolbox_) {
1447
+ this.toolbox_.position();
1277
1448
  }
1278
- element = /** @type {!SVGElement} */ (element.parentNode);
1449
+ } else {
1450
+ this.hideChaff(true);
1279
1451
  }
1280
1452
  }
1281
- return /** @type {!SVGElement} */ (this.cachedParentSvg_);
1282
- };
1283
-
1284
- /**
1285
- * Fires a viewport event if events are enabled and there is a change in
1286
- * viewport values.
1287
- * @package
1288
- */
1289
- WorkspaceSvg.prototype.maybeFireViewportChangeEvent = function() {
1290
- if (!eventUtils.isEnabled()) {
1291
- return;
1292
- }
1293
- const scale = this.scale;
1294
- const top = -this.scrollY;
1295
- const left = -this.scrollX;
1296
- if (scale === this.oldScale_ && Math.abs(top - this.oldTop_) < 1 &&
1297
- Math.abs(left - this.oldLeft_) < 1) {
1298
- // Ignore sub-pixel changes in top and left. Due to #4192 there are a lot of
1299
- // negligible changes in viewport top/left.
1300
- return;
1301
- }
1302
- const event = new (eventUtils.get(eventUtils.VIEWPORT_CHANGE))(
1303
- top, left, scale, this.id, this.oldScale_);
1304
- this.oldScale_ = scale;
1305
- this.oldTop_ = top;
1306
- this.oldLeft_ = left;
1307
- eventUtils.fire(event);
1308
- };
1309
-
1310
- /**
1311
- * Translate this workspace to new coordinates.
1312
- * @param {number} x Horizontal translation, in pixel units relative to the
1313
- * top left of the Blockly div.
1314
- * @param {number} y Vertical translation, in pixel units relative to the
1315
- * top left of the Blockly div.
1316
- */
1317
- WorkspaceSvg.prototype.translate = function(x, y) {
1318
- if (this.useWorkspaceDragSurface_ && this.isDragSurfaceActive_) {
1319
- this.workspaceDragSurface_.translateSurface(x, y);
1320
- } else {
1321
- const translation = 'translate(' + x + ',' + y + ') ' +
1322
- 'scale(' + this.scale + ')';
1323
- this.svgBlockCanvas_.setAttribute('transform', translation);
1324
- this.svgBubbleCanvas_.setAttribute('transform', translation);
1325
- }
1326
- // Now update the block drag surface if we're using one.
1327
- if (this.blockDragSurface_) {
1328
- this.blockDragSurface_.translateAndScaleGroup(x, y, this.scale);
1329
- }
1330
- // And update the grid if we're using one.
1331
- if (this.grid_) {
1332
- this.grid_.moveTo(x, y);
1333
- }
1334
-
1335
- this.maybeFireViewportChangeEvent();
1336
- };
1337
-
1338
- /**
1339
- * Called at the end of a workspace drag to take the contents
1340
- * out of the drag surface and put them back into the workspace SVG.
1341
- * Does nothing if the workspace drag surface is not enabled.
1342
- * @package
1343
- */
1344
- WorkspaceSvg.prototype.resetDragSurface = function() {
1345
- // Don't do anything if we aren't using a drag surface.
1346
- if (!this.useWorkspaceDragSurface_) {
1347
- return;
1348
- }
1349
-
1350
- this.isDragSurfaceActive_ = false;
1351
-
1352
- const trans = this.workspaceDragSurface_.getSurfaceTranslation();
1353
- this.workspaceDragSurface_.clearAndHide(this.svgGroup_);
1354
- const translation = 'translate(' + trans.x + ',' + trans.y + ') ' +
1355
- 'scale(' + this.scale + ')';
1356
- this.svgBlockCanvas_.setAttribute('transform', translation);
1357
- this.svgBubbleCanvas_.setAttribute('transform', translation);
1358
- };
1359
-
1360
- /**
1361
- * Called at the beginning of a workspace drag to move contents of
1362
- * the workspace to the drag surface.
1363
- * Does nothing if the drag surface is not enabled.
1364
- * @package
1365
- */
1366
- WorkspaceSvg.prototype.setupDragSurface = function() {
1367
- // Don't do anything if we aren't using a drag surface.
1368
- if (!this.useWorkspaceDragSurface_) {
1369
- return;
1370
- }
1371
-
1372
- // This can happen if the user starts a drag, mouses up outside of the
1373
- // document where the mouseup listener is registered (e.g. outside of an
1374
- // iframe) and then moves the mouse back in the workspace. On mobile and ff,
1375
- // we get the mouseup outside the frame. On chrome and safari desktop we do
1376
- // not.
1377
- if (this.isDragSurfaceActive_) {
1378
- return;
1379
- }
1380
-
1381
- this.isDragSurfaceActive_ = true;
1382
-
1383
- // Figure out where we want to put the canvas back. The order
1384
- // in the is important because things are layered.
1385
- const previousElement =
1386
- /** @type {Element} */ (this.svgBlockCanvas_.previousSibling);
1387
- const width = parseInt(this.getParentSvg().getAttribute('width'), 10);
1388
- const height = parseInt(this.getParentSvg().getAttribute('height'), 10);
1389
- const coord = svgMath.getRelativeXY(this.getCanvas());
1390
- this.workspaceDragSurface_.setContentsAndShow(
1391
- this.getCanvas(), this.getBubbleCanvas(), previousElement, width, height,
1392
- this.scale);
1393
- this.workspaceDragSurface_.translateSurface(coord.x, coord.y);
1394
- };
1395
-
1396
- /**
1397
- * Gets the drag surface blocks are moved to when a drag is started.
1398
- * @return {?BlockDragSurfaceSvg} This workspace's block drag surface,
1399
- * if one is in use.
1400
- * @package
1401
- */
1402
- WorkspaceSvg.prototype.getBlockDragSurface = function() {
1403
- return this.blockDragSurface_;
1404
- };
1405
-
1406
- /**
1407
- * Returns the horizontal offset of the workspace.
1408
- * Intended for LTR/RTL compatibility in XML.
1409
- * @return {number} Width.
1410
- */
1411
- WorkspaceSvg.prototype.getWidth = function() {
1412
- const metrics = this.getMetrics();
1413
- return metrics ? metrics.viewWidth / this.scale : 0;
1414
- };
1415
-
1416
- /**
1417
- * Toggles the visibility of the workspace.
1418
- * Currently only intended for main workspace.
1419
- * @param {boolean} isVisible True if workspace should be visible.
1420
- */
1421
- WorkspaceSvg.prototype.setVisible = function(isVisible) {
1422
- this.isVisible_ = isVisible;
1423
- if (!this.svgGroup_) {
1424
- return;
1425
- }
1426
1453
 
1427
- // Tell the scrollbar whether its container is visible so it can
1428
- // tell when to hide itself.
1429
- if (this.scrollbar) {
1430
- this.scrollbar.setContainerVisible(isVisible);
1431
- }
1432
-
1433
- // Tell the flyout whether its container is visible so it can
1434
- // tell when to hide itself.
1435
- if (this.getFlyout()) {
1436
- this.getFlyout().setContainerVisible(isVisible);
1437
- }
1438
-
1439
- this.getParentSvg().style.display = isVisible ? 'block' : 'none';
1440
- if (this.toolbox_) {
1441
- // Currently does not support toolboxes in mutators.
1442
- this.toolbox_.setVisible(isVisible);
1443
- }
1444
- if (isVisible) {
1454
+ /**
1455
+ * Render all blocks in workspace.
1456
+ */
1457
+ render() {
1458
+ // Generate list of all blocks.
1445
1459
  const blocks = this.getAllBlocks(false);
1446
- // Tell each block on the workspace to mark its fields as dirty.
1460
+ // Render each block.
1447
1461
  for (let i = blocks.length - 1; i >= 0; i--) {
1448
- blocks[i].markDirty();
1462
+ blocks[i].render(false);
1449
1463
  }
1450
1464
 
1451
- this.render();
1452
- if (this.toolbox_) {
1453
- this.toolbox_.position();
1465
+ if (this.currentGesture_) {
1466
+ const imList = this.currentGesture_.getInsertionMarkers();
1467
+ for (let i = 0; i < imList.length; i++) {
1468
+ imList[i].render(false);
1469
+ }
1454
1470
  }
1455
- } else {
1456
- this.hideChaff(true);
1457
- }
1458
- };
1459
1471
 
1460
- /**
1461
- * Render all blocks in workspace.
1462
- */
1463
- WorkspaceSvg.prototype.render = function() {
1464
- // Generate list of all blocks.
1465
- const blocks = this.getAllBlocks(false);
1466
- // Render each block.
1467
- for (let i = blocks.length - 1; i >= 0; i--) {
1468
- blocks[i].render(false);
1472
+ this.markerManager_.updateMarkers();
1469
1473
  }
1470
1474
 
1471
- if (this.currentGesture_) {
1472
- const imList = this.currentGesture_.getInsertionMarkers();
1473
- for (let i = 0; i < imList.length; i++) {
1474
- imList[i].render(false);
1475
+ /**
1476
+ * Highlight or unhighlight a block in the workspace. Block highlighting is
1477
+ * often used to visually mark blocks currently being executed.
1478
+ * @param {?string} id ID of block to highlight/unhighlight,
1479
+ * or null for no block (used to unhighlight all blocks).
1480
+ * @param {boolean=} opt_state If undefined, highlight specified block and
1481
+ * automatically unhighlight all others. If true or false, manually
1482
+ * highlight/unhighlight the specified block.
1483
+ */
1484
+ highlightBlock(id, opt_state) {
1485
+ if (opt_state === undefined) {
1486
+ // Unhighlight all blocks.
1487
+ for (let i = 0, block; (block = this.highlightedBlocks_[i]); i++) {
1488
+ block.setHighlighted(false);
1489
+ }
1490
+ this.highlightedBlocks_.length = 0;
1491
+ }
1492
+ // Highlight/unhighlight the specified block.
1493
+ const block = id ? this.getBlockById(id) : null;
1494
+ if (block) {
1495
+ const state = (opt_state === undefined) || opt_state;
1496
+ // Using Set here would be great, but at the cost of IE10 support.
1497
+ if (!state) {
1498
+ arrayUtils.removeElem(this.highlightedBlocks_, block);
1499
+ } else if (this.highlightedBlocks_.indexOf(block) === -1) {
1500
+ this.highlightedBlocks_.push(block);
1501
+ }
1502
+ block.setHighlighted(state);
1475
1503
  }
1476
1504
  }
1477
1505
 
1478
- this.markerManager_.updateMarkers();
1479
- };
1480
-
1481
- /**
1482
- * Highlight or unhighlight a block in the workspace. Block highlighting is
1483
- * often used to visually mark blocks currently being executed.
1484
- * @param {?string} id ID of block to highlight/unhighlight,
1485
- * or null for no block (used to unhighlight all blocks).
1486
- * @param {boolean=} opt_state If undefined, highlight specified block and
1487
- * automatically unhighlight all others. If true or false, manually
1488
- * highlight/unhighlight the specified block.
1489
- */
1490
- WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) {
1491
- if (opt_state === undefined) {
1492
- // Unhighlight all blocks.
1493
- for (let i = 0, block; (block = this.highlightedBlocks_[i]); i++) {
1494
- block.setHighlighted(false);
1495
- }
1496
- this.highlightedBlocks_.length = 0;
1497
- }
1498
- // Highlight/unhighlight the specified block.
1499
- const block = id ? this.getBlockById(id) : null;
1500
- if (block) {
1501
- const state = (opt_state === undefined) || opt_state;
1502
- // Using Set here would be great, but at the cost of IE10 support.
1503
- if (!state) {
1504
- arrayUtils.removeElem(this.highlightedBlocks_, block);
1505
- } else if (this.highlightedBlocks_.indexOf(block) === -1) {
1506
- this.highlightedBlocks_.push(block);
1507
- }
1508
- block.setHighlighted(state);
1509
- }
1510
- };
1506
+ /**
1507
+ * Pastes the provided block or workspace comment onto the workspace.
1508
+ * Does not check whether there is remaining capacity for the object, that
1509
+ * should be done before calling this method.
1510
+ * @param {!Object|!Element|!DocumentFragment} state The representation of the
1511
+ * thing to paste.
1512
+ * @return {!ICopyable|null} The pasted thing, or null if
1513
+ * the paste was not successful.
1514
+ */
1515
+ paste(state) {
1516
+ if (!this.rendered || !state['type'] && !state.tagName) {
1517
+ return null;
1518
+ }
1519
+ if (this.currentGesture_) {
1520
+ this.currentGesture_.cancel(); // Dragging while pasting? No.
1521
+ }
1511
1522
 
1512
- /**
1513
- * Pastes the provided block or workspace comment onto the workspace.
1514
- * Does not check whether there is remaining capacity for the object, that
1515
- * should be done before calling this method.
1516
- * @param {!Object|!Element|!DocumentFragment} state The representation of the
1517
- * thing to paste.
1518
- */
1519
- WorkspaceSvg.prototype.paste = function(state) {
1520
- if (!this.rendered || !state['type'] && !state.tagName) {
1521
- return;
1522
- }
1523
- if (this.currentGesture_) {
1524
- this.currentGesture_.cancel(); // Dragging while pasting? No.
1525
- }
1523
+ const existingGroup = eventUtils.getGroup();
1524
+ if (!existingGroup) {
1525
+ eventUtils.setGroup(true);
1526
+ }
1526
1527
 
1527
- // Checks if this is JSON. JSON has a type property, while elements don't.
1528
- if (state['type']) {
1529
- this.pasteBlock_(null, /** @type {!blocks.State} */ (state));
1530
- } else {
1531
- const xmlBlock = /** @type {!Element} */ (state);
1532
- if (xmlBlock.tagName.toLowerCase() === 'comment') {
1533
- this.pasteWorkspaceComment_(xmlBlock);
1528
+ let pastedThing;
1529
+ // Checks if this is JSON. JSON has a type property, while elements don't.
1530
+ if (state['type']) {
1531
+ pastedThing =
1532
+ this.pasteBlock_(null, /** @type {!blocks.State} */ (state));
1534
1533
  } else {
1535
- this.pasteBlock_(xmlBlock, null);
1534
+ const xmlBlock = /** @type {!Element} */ (state);
1535
+ if (xmlBlock.tagName.toLowerCase() === 'comment') {
1536
+ pastedThing = this.pasteWorkspaceComment_(xmlBlock);
1537
+ } else {
1538
+ pastedThing = this.pasteBlock_(xmlBlock, null);
1539
+ }
1536
1540
  }
1541
+
1542
+ eventUtils.setGroup(existingGroup);
1543
+ return pastedThing;
1537
1544
  }
1538
- };
1539
1545
 
1540
- /**
1541
- * Paste the provided block onto the workspace.
1542
- * @param {?Element} xmlBlock XML block element.
1543
- * @param {?blocks.State} jsonBlock JSON block
1544
- * representation.
1545
- * @private
1546
- */
1547
- WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock, jsonBlock) {
1548
- eventUtils.disable();
1549
- let block;
1550
- try {
1551
- let blockX = 0;
1552
- let blockY = 0;
1553
- if (xmlBlock) {
1554
- block = Xml.domToBlock(xmlBlock, this);
1555
- blockX = parseInt(xmlBlock.getAttribute('x'), 10);
1556
- if (this.RTL) {
1557
- blockX = -blockX;
1558
- }
1559
- blockY = parseInt(xmlBlock.getAttribute('y'), 10);
1560
- } else if (jsonBlock) {
1561
- block = blocks.append(jsonBlock, this);
1562
- blockX = jsonBlock['x'] || 10;
1563
- if (this.RTL) {
1564
- blockX = this.getWidth() - blockX;
1565
- }
1566
- blockY = jsonBlock['y'] || 10;
1567
- }
1568
-
1569
- // Move the duplicate to original position.
1570
- if (!isNaN(blockX) && !isNaN(blockY)) {
1571
- // Offset block until not clobbering another block and not in connection
1572
- // distance with neighbouring blocks.
1573
- let collide;
1574
- do {
1575
- collide = false;
1576
- const allBlocks = this.getAllBlocks(false);
1577
- for (let i = 0, otherBlock; (otherBlock = allBlocks[i]); i++) {
1578
- const otherXY = otherBlock.getRelativeToSurfaceXY();
1579
- if (Math.abs(blockX - otherXY.x) <= 1 &&
1580
- Math.abs(blockY - otherXY.y) <= 1) {
1581
- collide = true;
1582
- break;
1583
- }
1546
+ /**
1547
+ * Paste the provided block onto the workspace.
1548
+ * @param {?Element} xmlBlock XML block element.
1549
+ * @param {?blocks.State} jsonBlock JSON block
1550
+ * representation.
1551
+ * @return {!BlockSvg} The pasted block.
1552
+ * @private
1553
+ */
1554
+ pasteBlock_(xmlBlock, jsonBlock) {
1555
+ eventUtils.disable();
1556
+ let block;
1557
+ try {
1558
+ let blockX = 0;
1559
+ let blockY = 0;
1560
+ if (xmlBlock) {
1561
+ block = /** @type {!BlockSvg} */ (Xml.domToBlock(xmlBlock, this));
1562
+ blockX = parseInt(xmlBlock.getAttribute('x'), 10);
1563
+ if (this.RTL) {
1564
+ blockX = -blockX;
1565
+ }
1566
+ blockY = parseInt(xmlBlock.getAttribute('y'), 10);
1567
+ } else if (jsonBlock) {
1568
+ block = /** @type {!BlockSvg} */ (blocks.append(jsonBlock, this));
1569
+ blockX = jsonBlock['x'] || 10;
1570
+ if (this.RTL) {
1571
+ blockX = this.getWidth() - blockX;
1584
1572
  }
1585
- if (!collide) {
1586
- // Check for blocks in snap range to any of its connections.
1587
- const connections = block.getConnections_(false);
1588
- for (let i = 0, connection; (connection = connections[i]); i++) {
1589
- const neighbour = connection.closest(
1590
- internalConstants.SNAP_RADIUS, new Coordinate(blockX, blockY));
1591
- if (neighbour.connection) {
1573
+ blockY = jsonBlock['y'] || 10;
1574
+ }
1575
+
1576
+ // Move the duplicate to original position.
1577
+ if (!isNaN(blockX) && !isNaN(blockY)) {
1578
+ // Offset block until not clobbering another block and not in connection
1579
+ // distance with neighbouring blocks.
1580
+ let collide;
1581
+ do {
1582
+ collide = false;
1583
+ const allBlocks = this.getAllBlocks(false);
1584
+ for (let i = 0, otherBlock; (otherBlock = allBlocks[i]); i++) {
1585
+ const otherXY = otherBlock.getRelativeToSurfaceXY();
1586
+ if (Math.abs(blockX - otherXY.x) <= 1 &&
1587
+ Math.abs(blockY - otherXY.y) <= 1) {
1592
1588
  collide = true;
1593
1589
  break;
1594
1590
  }
1595
1591
  }
1596
- }
1597
- if (collide) {
1598
- if (this.RTL) {
1599
- blockX -= internalConstants.SNAP_RADIUS;
1600
- } else {
1601
- blockX += internalConstants.SNAP_RADIUS;
1592
+ if (!collide) {
1593
+ // Check for blocks in snap range to any of its connections.
1594
+ const connections = block.getConnections_(false);
1595
+ for (let i = 0, connection; (connection = connections[i]); i++) {
1596
+ const neighbour =
1597
+ /** @type {!RenderedConnection} */ (connection)
1598
+ .closest(
1599
+ config.snapRadius, new Coordinate(blockX, blockY));
1600
+ if (neighbour.connection) {
1601
+ collide = true;
1602
+ break;
1603
+ }
1604
+ }
1602
1605
  }
1603
- blockY += internalConstants.SNAP_RADIUS * 2;
1604
- }
1605
- } while (collide);
1606
- block.moveTo(new Coordinate(blockX, blockY));
1606
+ if (collide) {
1607
+ if (this.RTL) {
1608
+ blockX -= config.snapRadius;
1609
+ } else {
1610
+ blockX += config.snapRadius;
1611
+ }
1612
+ blockY += config.snapRadius * 2;
1613
+ }
1614
+ } while (collide);
1615
+ block.moveTo(new Coordinate(blockX, blockY));
1616
+ }
1617
+ } finally {
1618
+ eventUtils.enable();
1607
1619
  }
1608
- } finally {
1609
- eventUtils.enable();
1610
- }
1611
- if (eventUtils.isEnabled() && !block.isShadow()) {
1612
- eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
1620
+ if (eventUtils.isEnabled() && !block.isShadow()) {
1621
+ eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
1622
+ }
1623
+ block.select();
1624
+ return block;
1613
1625
  }
1614
- block.select();
1615
- };
1616
1626
 
1617
- /**
1618
- * Paste the provided comment onto the workspace.
1619
- * @param {!Element} xmlComment XML workspace comment element.
1620
- * @private
1621
- * @suppress {checkTypes} Suppress checks while workspace comments are not
1622
- * bundled in.
1623
- */
1624
- WorkspaceSvg.prototype.pasteWorkspaceComment_ = function(xmlComment) {
1625
- eventUtils.disable();
1626
- let comment;
1627
- try {
1628
- comment = goog.module.get('Blockly.WorkspaceCommentSvg')
1629
- .fromXml(xmlComment, this);
1630
- // Move the duplicate to original position.
1631
- let commentX = parseInt(xmlComment.getAttribute('x'), 10);
1632
- let commentY = parseInt(xmlComment.getAttribute('y'), 10);
1633
- if (!isNaN(commentX) && !isNaN(commentY)) {
1634
- if (this.RTL) {
1635
- commentX = -commentX;
1627
+ /**
1628
+ * Paste the provided comment onto the workspace.
1629
+ * @param {!Element} xmlComment XML workspace comment element.
1630
+ * @return {!WorkspaceCommentSvg} The pasted workspace comment.
1631
+ * @private
1632
+ * @suppress {checkTypes} Suppress checks while workspace comments are not
1633
+ * bundled in.
1634
+ */
1635
+ pasteWorkspaceComment_(xmlComment) {
1636
+ eventUtils.disable();
1637
+ let comment;
1638
+ try {
1639
+ comment = goog.module.get('Blockly.WorkspaceCommentSvg')
1640
+ .fromXml(xmlComment, this);
1641
+ // Move the duplicate to original position.
1642
+ let commentX = parseInt(xmlComment.getAttribute('x'), 10);
1643
+ let commentY = parseInt(xmlComment.getAttribute('y'), 10);
1644
+ if (!isNaN(commentX) && !isNaN(commentY)) {
1645
+ if (this.RTL) {
1646
+ commentX = -commentX;
1647
+ }
1648
+ // Offset workspace comment.
1649
+ // TODO (#1719): Properly offset comment such that it's not interfering
1650
+ // with any blocks.
1651
+ commentX += 50;
1652
+ commentY += 50;
1653
+ comment.moveBy(commentX, commentY);
1636
1654
  }
1637
- // Offset workspace comment.
1638
- // TODO (#1719): Properly offset comment such that it's not interfering
1639
- // with any blocks.
1640
- commentX += 50;
1641
- commentY += 50;
1642
- comment.moveBy(commentX, commentY);
1655
+ } finally {
1656
+ eventUtils.enable();
1643
1657
  }
1644
- } finally {
1645
- eventUtils.enable();
1658
+ if (eventUtils.isEnabled()) {
1659
+ goog.module.get('Blockly.WorkspaceComment').fireCreateEvent(comment);
1660
+ }
1661
+ comment.select();
1662
+ return comment;
1646
1663
  }
1647
- if (eventUtils.isEnabled()) {
1648
- goog.module.get('Blockly.WorkspaceComment').fireCreateEvent(comment);
1664
+
1665
+ /**
1666
+ * Refresh the toolbox unless there's a drag in progress.
1667
+ * @package
1668
+ */
1669
+ refreshToolboxSelection() {
1670
+ const ws = this.isFlyout ? this.targetWorkspace : this;
1671
+ if (ws && !ws.currentGesture_ && ws.toolbox_ && ws.toolbox_.getFlyout()) {
1672
+ ws.toolbox_.refreshSelection();
1673
+ }
1649
1674
  }
1650
- comment.select();
1651
- };
1652
1675
 
1653
- /**
1654
- * Refresh the toolbox unless there's a drag in progress.
1655
- * @package
1656
- */
1657
- WorkspaceSvg.prototype.refreshToolboxSelection = function() {
1658
- const ws = this.isFlyout ? this.targetWorkspace : this;
1659
- if (ws && !ws.currentGesture_ && ws.toolbox_ && ws.toolbox_.getFlyout()) {
1660
- ws.toolbox_.refreshSelection();
1676
+ /**
1677
+ * Rename a variable by updating its name in the variable map. Update the
1678
+ * flyout to show the renamed variable immediately.
1679
+ * @param {string} id ID of the variable to rename.
1680
+ * @param {string} newName New variable name.
1681
+ */
1682
+ renameVariableById(id, newName) {
1683
+ super.renameVariableById(id, newName);
1684
+ this.refreshToolboxSelection();
1661
1685
  }
1662
- };
1663
1686
 
1664
- /**
1665
- * Rename a variable by updating its name in the variable map. Update the
1666
- * flyout to show the renamed variable immediately.
1667
- * @param {string} id ID of the variable to rename.
1668
- * @param {string} newName New variable name.
1669
- */
1670
- WorkspaceSvg.prototype.renameVariableById = function(id, newName) {
1671
- WorkspaceSvg.superClass_.renameVariableById.call(this, id, newName);
1672
- this.refreshToolboxSelection();
1673
- };
1687
+ /**
1688
+ * Delete a variable by the passed in ID. Update the flyout to show
1689
+ * immediately that the variable is deleted.
1690
+ * @param {string} id ID of variable to delete.
1691
+ */
1692
+ deleteVariableById(id) {
1693
+ super.deleteVariableById(id);
1694
+ this.refreshToolboxSelection();
1695
+ }
1674
1696
 
1675
- /**
1676
- * Delete a variable by the passed in ID. Update the flyout to show
1677
- * immediately that the variable is deleted.
1678
- * @param {string} id ID of variable to delete.
1679
- */
1680
- WorkspaceSvg.prototype.deleteVariableById = function(id) {
1681
- WorkspaceSvg.superClass_.deleteVariableById.call(this, id);
1682
- this.refreshToolboxSelection();
1683
- };
1697
+ /**
1698
+ * Create a new variable with the given name. Update the flyout to show the
1699
+ * new variable immediately.
1700
+ * @param {string} name The new variable's name.
1701
+ * @param {?string=} opt_type The type of the variable like 'int' or 'string'.
1702
+ * Does not need to be unique. Field_variable can filter variables based
1703
+ * on their type. This will default to '' which is a specific type.
1704
+ * @param {?string=} opt_id The unique ID of the variable. This will default
1705
+ * to a UUID.
1706
+ * @return {!VariableModel} The newly created variable.
1707
+ */
1708
+ createVariable(name, opt_type, opt_id) {
1709
+ const newVar = super.createVariable(name, opt_type, opt_id);
1710
+ this.refreshToolboxSelection();
1711
+ return newVar;
1712
+ }
1684
1713
 
1685
- /**
1686
- * Create a new variable with the given name. Update the flyout to show the
1687
- * new variable immediately.
1688
- * @param {string} name The new variable's name.
1689
- * @param {?string=} opt_type The type of the variable like 'int' or 'string'.
1690
- * Does not need to be unique. Field_variable can filter variables based on
1691
- * their type. This will default to '' which is a specific type.
1692
- * @param {?string=} opt_id The unique ID of the variable. This will default to
1693
- * a UUID.
1694
- * @return {!VariableModel} The newly created variable.
1695
- */
1696
- WorkspaceSvg.prototype.createVariable = function(name, opt_type, opt_id) {
1697
- const newVar = WorkspaceSvg.superClass_.createVariable.call(
1698
- this, name, opt_type, opt_id);
1699
- this.refreshToolboxSelection();
1700
- return newVar;
1701
- };
1714
+ /**
1715
+ * Make a list of all the delete areas for this workspace.
1716
+ * @deprecated Use workspace.recordDragTargets. (2021 June)
1717
+ */
1718
+ recordDeleteAreas() {
1719
+ utils.deprecation.warn(
1720
+ 'WorkspaceSvg.prototype.recordDeleteAreas', 'June 2021', 'June 2022',
1721
+ 'WorkspaceSvg.prototype.recordDragTargets');
1722
+ this.recordDragTargets();
1723
+ }
1702
1724
 
1703
- /**
1704
- * Make a list of all the delete areas for this workspace.
1705
- * @deprecated Use workspace.recordDragTargets. (2021 June)
1706
- */
1707
- WorkspaceSvg.prototype.recordDeleteAreas = function() {
1708
- utils.deprecation.warn(
1709
- 'WorkspaceSvg.prototype.recordDeleteAreas', 'June 2021', 'June 2022',
1710
- 'WorkspaceSvg.prototype.recordDragTargets');
1711
- this.recordDragTargets();
1712
- };
1725
+ /**
1726
+ * Make a list of all the delete areas for this workspace.
1727
+ */
1728
+ recordDragTargets() {
1729
+ const dragTargets = this.componentManager_.getComponents(
1730
+ ComponentManager.Capability.DRAG_TARGET, true);
1731
+
1732
+ this.dragTargetAreas_ = [];
1733
+ for (let i = 0, targetArea; (targetArea = dragTargets[i]); i++) {
1734
+ const rect = targetArea.getClientRect();
1735
+ if (rect) {
1736
+ this.dragTargetAreas_.push({
1737
+ component: targetArea,
1738
+ clientRect: rect,
1739
+ });
1740
+ }
1741
+ }
1742
+ }
1713
1743
 
1714
- /**
1715
- * Make a list of all the delete areas for this workspace.
1716
- */
1717
- WorkspaceSvg.prototype.recordDragTargets = function() {
1718
- const dragTargets = this.componentManager_.getComponents(
1719
- ComponentManager.Capability.DRAG_TARGET, true);
1744
+ /**
1745
+ * Returns the drag target the mouse event is over.
1746
+ * @param {!Event} e Mouse move event.
1747
+ * @return {?IDragTarget} Null if not over a drag target, or the drag
1748
+ * target the event is over.
1749
+ */
1750
+ getDragTarget(e) {
1751
+ for (let i = 0, targetArea; (targetArea = this.dragTargetAreas_[i]); i++) {
1752
+ if (targetArea.clientRect.contains(e.clientX, e.clientY)) {
1753
+ return targetArea.component;
1754
+ }
1755
+ }
1756
+ return null;
1757
+ }
1720
1758
 
1721
- this.dragTargetAreas_ = [];
1722
- for (let i = 0, targetArea; (targetArea = dragTargets[i]); i++) {
1723
- const rect = targetArea.getClientRect();
1724
- if (rect) {
1725
- this.dragTargetAreas_.push({
1726
- component: targetArea,
1727
- clientRect: rect,
1728
- });
1759
+ /**
1760
+ * Handle a mouse-down on SVG drawing surface.
1761
+ * @param {!Event} e Mouse down event.
1762
+ * @private
1763
+ */
1764
+ onMouseDown_(e) {
1765
+ const gesture = this.getGesture(e);
1766
+ if (gesture) {
1767
+ gesture.handleWsStart(e, this);
1729
1768
  }
1730
1769
  }
1731
- };
1732
1770
 
1771
+ /**
1772
+ * Start tracking a drag of an object on this workspace.
1773
+ * @param {!Event} e Mouse down event.
1774
+ * @param {!Coordinate} xy Starting location of object.
1775
+ */
1776
+ startDrag(e, xy) {
1777
+ // Record the starting offset between the bubble's location and the mouse.
1778
+ const point = browserEvents.mouseToSvg(
1779
+ e, this.getParentSvg(), this.getInverseScreenCTM());
1780
+ // Fix scale of mouse event.
1781
+ point.x /= this.scale;
1782
+ point.y /= this.scale;
1783
+ this.dragDeltaXY_ = Coordinate.difference(xy, point);
1784
+ }
1733
1785
 
1734
- /**
1735
- * Returns the drag target the mouse event is over.
1736
- * @param {!Event} e Mouse move event.
1737
- * @return {?IDragTarget} Null if not over a drag target, or the drag
1738
- * target the event is over.
1739
- */
1740
- WorkspaceSvg.prototype.getDragTarget = function(e) {
1741
- for (let i = 0, targetArea; (targetArea = this.dragTargetAreas_[i]); i++) {
1742
- if (targetArea.clientRect.contains(e.clientX, e.clientY)) {
1743
- return targetArea.component;
1744
- }
1786
+ /**
1787
+ * Track a drag of an object on this workspace.
1788
+ * @param {!Event} e Mouse move event.
1789
+ * @return {!Coordinate} New location of object.
1790
+ */
1791
+ moveDrag(e) {
1792
+ const point = browserEvents.mouseToSvg(
1793
+ e, this.getParentSvg(), this.getInverseScreenCTM());
1794
+ // Fix scale of mouse event.
1795
+ point.x /= this.scale;
1796
+ point.y /= this.scale;
1797
+ return Coordinate.sum(
1798
+ /** @type {!Coordinate} */ (this.dragDeltaXY_), point);
1745
1799
  }
1746
- return null;
1747
- };
1748
1800
 
1749
- /**
1750
- * Handle a mouse-down on SVG drawing surface.
1751
- * @param {!Event} e Mouse down event.
1752
- * @private
1753
- */
1754
- WorkspaceSvg.prototype.onMouseDown_ = function(e) {
1755
- const gesture = this.getGesture(e);
1756
- if (gesture) {
1757
- gesture.handleWsStart(e, this);
1801
+ /**
1802
+ * Is the user currently dragging a block or scrolling the flyout/workspace?
1803
+ * @return {boolean} True if currently dragging or scrolling.
1804
+ */
1805
+ isDragging() {
1806
+ return this.currentGesture_ !== null && this.currentGesture_.isDragging();
1758
1807
  }
1759
- };
1760
1808
 
1761
- /**
1762
- * Start tracking a drag of an object on this workspace.
1763
- * @param {!Event} e Mouse down event.
1764
- * @param {!Coordinate} xy Starting location of object.
1765
- */
1766
- WorkspaceSvg.prototype.startDrag = function(e, xy) {
1767
- // Record the starting offset between the bubble's location and the mouse.
1768
- const point = browserEvents.mouseToSvg(
1769
- e, this.getParentSvg(), this.getInverseScreenCTM());
1770
- // Fix scale of mouse event.
1771
- point.x /= this.scale;
1772
- point.y /= this.scale;
1773
- this.dragDeltaXY_ = Coordinate.difference(xy, point);
1774
- };
1809
+ /**
1810
+ * Is this workspace draggable?
1811
+ * @return {boolean} True if this workspace may be dragged.
1812
+ */
1813
+ isDraggable() {
1814
+ return this.options.moveOptions && this.options.moveOptions.drag;
1815
+ }
1775
1816
 
1776
- /**
1777
- * Track a drag of an object on this workspace.
1778
- * @param {!Event} e Mouse move event.
1779
- * @return {!Coordinate} New location of object.
1780
- */
1781
- WorkspaceSvg.prototype.moveDrag = function(e) {
1782
- const point = browserEvents.mouseToSvg(
1783
- e, this.getParentSvg(), this.getInverseScreenCTM());
1784
- // Fix scale of mouse event.
1785
- point.x /= this.scale;
1786
- point.y /= this.scale;
1787
- return Coordinate.sum(
1788
- /** @type {!Coordinate} */ (this.dragDeltaXY_), point);
1789
- };
1817
+ /**
1818
+ * Is this workspace movable?
1819
+ *
1820
+ * This means the user can reposition the X Y coordinates of the workspace
1821
+ * through input. This can be through scrollbars, scroll wheel, dragging, or
1822
+ * through zooming with the scroll wheel or pinch (since the zoom is centered
1823
+ * on the mouse position). This does not include zooming with the zoom
1824
+ * controls since the X Y coordinates are decided programmatically.
1825
+ * @return {boolean} True if the workspace is movable, false otherwise.
1826
+ */
1827
+ isMovable() {
1828
+ return (this.options.moveOptions &&
1829
+ !!this.options.moveOptions.scrollbars) ||
1830
+ (this.options.moveOptions && this.options.moveOptions.wheel) ||
1831
+ (this.options.moveOptions && this.options.moveOptions.drag) ||
1832
+ (this.options.zoomOptions && this.options.zoomOptions.wheel) ||
1833
+ (this.options.zoomOptions && this.options.zoomOptions.pinch);
1834
+ }
1790
1835
 
1791
- /**
1792
- * Is the user currently dragging a block or scrolling the flyout/workspace?
1793
- * @return {boolean} True if currently dragging or scrolling.
1794
- */
1795
- WorkspaceSvg.prototype.isDragging = function() {
1796
- return this.currentGesture_ !== null && this.currentGesture_.isDragging();
1797
- };
1836
+ /**
1837
+ * Is this workspace movable horizontally?
1838
+ * @return {boolean} True if the workspace is movable horizontally, false
1839
+ * otherwise.
1840
+ */
1841
+ isMovableHorizontally() {
1842
+ const hasScrollbars = !!this.scrollbar;
1843
+ return this.isMovable() &&
1844
+ (!hasScrollbars ||
1845
+ (hasScrollbars && this.scrollbar.canScrollHorizontally()));
1846
+ }
1798
1847
 
1799
- /**
1800
- * Is this workspace draggable?
1801
- * @return {boolean} True if this workspace may be dragged.
1802
- */
1803
- WorkspaceSvg.prototype.isDraggable = function() {
1804
- return this.options.moveOptions && this.options.moveOptions.drag;
1805
- };
1848
+ /**
1849
+ * Is this workspace movable vertically?
1850
+ * @return {boolean} True if the workspace is movable vertically, false
1851
+ * otherwise.
1852
+ */
1853
+ isMovableVertically() {
1854
+ const hasScrollbars = !!this.scrollbar;
1855
+ return this.isMovable() &&
1856
+ (!hasScrollbars ||
1857
+ (hasScrollbars && this.scrollbar.canScrollVertically()));
1858
+ }
1806
1859
 
1807
- /**
1808
- * Is this workspace movable?
1809
- *
1810
- * This means the user can reposition the X Y coordinates of the workspace
1811
- * through input. This can be through scrollbars, scroll wheel, dragging, or
1812
- * through zooming with the scroll wheel or pinch (since the zoom is centered on
1813
- * the mouse position). This does not include zooming with the zoom controls
1814
- * since the X Y coordinates are decided programmatically.
1815
- * @return {boolean} True if the workspace is movable, false otherwise.
1816
- */
1817
- WorkspaceSvg.prototype.isMovable = function() {
1818
- return (this.options.moveOptions && !!this.options.moveOptions.scrollbars) ||
1819
- (this.options.moveOptions && this.options.moveOptions.wheel) ||
1820
- (this.options.moveOptions && this.options.moveOptions.drag) ||
1821
- (this.options.zoomOptions && this.options.zoomOptions.wheel) ||
1822
- (this.options.zoomOptions && this.options.zoomOptions.pinch);
1823
- };
1860
+ /**
1861
+ * Handle a mouse-wheel on SVG drawing surface.
1862
+ * @param {!Event} e Mouse wheel event.
1863
+ * @private
1864
+ */
1865
+ onMouseWheel_(e) {
1866
+ // Don't scroll or zoom anything if drag is in progress.
1867
+ if (Gesture.inProgress()) {
1868
+ e.preventDefault();
1869
+ e.stopPropagation();
1870
+ return;
1871
+ }
1872
+ const canWheelZoom =
1873
+ this.options.zoomOptions && this.options.zoomOptions.wheel;
1874
+ const canWheelMove =
1875
+ this.options.moveOptions && this.options.moveOptions.wheel;
1876
+ if (!canWheelZoom && !canWheelMove) {
1877
+ return;
1878
+ }
1824
1879
 
1825
- /**
1826
- * Is this workspace movable horizontally?
1827
- * @return {boolean} True if the workspace is movable horizontally, false
1828
- * otherwise.
1829
- */
1830
- WorkspaceSvg.prototype.isMovableHorizontally = function() {
1831
- const hasScrollbars = !!this.scrollbar;
1832
- return this.isMovable() &&
1833
- (!hasScrollbars ||
1834
- (hasScrollbars && this.scrollbar.canScrollHorizontally()));
1835
- };
1880
+ const scrollDelta = browserEvents.getScrollDeltaPixels(e);
1836
1881
 
1837
- /**
1838
- * Is this workspace movable vertically?
1839
- * @return {boolean} True if the workspace is movable vertically, false
1840
- * otherwise.
1841
- */
1842
- WorkspaceSvg.prototype.isMovableVertically = function() {
1843
- const hasScrollbars = !!this.scrollbar;
1844
- return this.isMovable() &&
1845
- (!hasScrollbars ||
1846
- (hasScrollbars && this.scrollbar.canScrollVertically()));
1847
- };
1882
+ // Zoom should also be enabled by the command key on Mac devices,
1883
+ // but not super on Unix.
1884
+ let commandKey;
1885
+ if (userAgent.MAC) {
1886
+ commandKey = e.metaKey;
1887
+ }
1848
1888
 
1849
- /**
1850
- * Handle a mouse-wheel on SVG drawing surface.
1851
- * @param {!Event} e Mouse wheel event.
1852
- * @private
1853
- */
1854
- WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
1855
- // Don't scroll or zoom anything if drag is in progress.
1856
- if (Gesture.inProgress()) {
1889
+ if (canWheelZoom && (e.ctrlKey || commandKey || !canWheelMove)) {
1890
+ // Zoom.
1891
+ // The vertical scroll distance that corresponds to a click of a zoom
1892
+ // button.
1893
+ const PIXELS_PER_ZOOM_STEP = 50;
1894
+ const delta = -scrollDelta.y / PIXELS_PER_ZOOM_STEP;
1895
+ const position = browserEvents.mouseToSvg(
1896
+ e, this.getParentSvg(), this.getInverseScreenCTM());
1897
+ this.zoom(position.x, position.y, delta);
1898
+ } else {
1899
+ // Scroll.
1900
+ let x = this.scrollX - scrollDelta.x;
1901
+ let y = this.scrollY - scrollDelta.y;
1902
+
1903
+ if (e.shiftKey && !scrollDelta.x) {
1904
+ // Scroll horizontally (based on vertical scroll delta).
1905
+ // This is needed as for some browser/system combinations which do not
1906
+ // set deltaX.
1907
+ x = this.scrollX - scrollDelta.y;
1908
+ y = this.scrollY; // Don't scroll vertically.
1909
+ }
1910
+ this.scroll(x, y);
1911
+ }
1857
1912
  e.preventDefault();
1858
- e.stopPropagation();
1859
- return;
1860
- }
1861
- const canWheelZoom =
1862
- this.options.zoomOptions && this.options.zoomOptions.wheel;
1863
- const canWheelMove =
1864
- this.options.moveOptions && this.options.moveOptions.wheel;
1865
- if (!canWheelZoom && !canWheelMove) {
1866
- return;
1867
1913
  }
1868
1914
 
1869
- const scrollDelta = browserEvents.getScrollDeltaPixels(e);
1915
+ /**
1916
+ * Calculate the bounding box for the blocks on the workspace.
1917
+ * Coordinate system: workspace coordinates.
1918
+ *
1919
+ * @return {!Rect} Contains the position and size of the
1920
+ * bounding box containing the blocks on the workspace.
1921
+ */
1922
+ getBlocksBoundingBox() {
1923
+ const topElements = this.getTopBoundedElements();
1924
+ // There are no blocks, return empty rectangle.
1925
+ if (!topElements.length) {
1926
+ return new Rect(0, 0, 0, 0);
1927
+ }
1870
1928
 
1871
- // Zoom should also be enabled by the command key on Mac devices,
1872
- // but not super on Unix.
1873
- let commandKey;
1874
- if (userAgent.MAC) {
1875
- commandKey = e.metaKey;
1876
- }
1929
+ // Initialize boundary using the first block.
1930
+ const boundary = topElements[0].getBoundingRectangle();
1877
1931
 
1878
- if (canWheelZoom && (e.ctrlKey || commandKey || !canWheelMove)) {
1879
- // Zoom.
1880
- // The vertical scroll distance that corresponds to a click of a zoom
1881
- // button.
1882
- const PIXELS_PER_ZOOM_STEP = 50;
1883
- const delta = -scrollDelta.y / PIXELS_PER_ZOOM_STEP;
1884
- const position = browserEvents.mouseToSvg(
1885
- e, this.getParentSvg(), this.getInverseScreenCTM());
1886
- this.zoom(position.x, position.y, delta);
1887
- } else {
1888
- // Scroll.
1889
- let x = this.scrollX - scrollDelta.x;
1890
- let y = this.scrollY - scrollDelta.y;
1891
-
1892
- if (e.shiftKey && !scrollDelta.x) {
1893
- // Scroll horizontally (based on vertical scroll delta).
1894
- // This is needed as for some browser/system combinations which do not
1895
- // set deltaX.
1896
- x = this.scrollX - scrollDelta.y;
1897
- y = this.scrollY; // Don't scroll vertically.
1932
+ // Start at 1 since the 0th block was used for initialization.
1933
+ for (let i = 1; i < topElements.length; i++) {
1934
+ const topElement = topElements[i];
1935
+ if (topElement.isInsertionMarker && topElement.isInsertionMarker()) {
1936
+ continue;
1937
+ }
1938
+ const blockBoundary = topElement.getBoundingRectangle();
1939
+ if (blockBoundary.top < boundary.top) {
1940
+ boundary.top = blockBoundary.top;
1941
+ }
1942
+ if (blockBoundary.bottom > boundary.bottom) {
1943
+ boundary.bottom = blockBoundary.bottom;
1944
+ }
1945
+ if (blockBoundary.left < boundary.left) {
1946
+ boundary.left = blockBoundary.left;
1947
+ }
1948
+ if (blockBoundary.right > boundary.right) {
1949
+ boundary.right = blockBoundary.right;
1950
+ }
1898
1951
  }
1899
- this.scroll(x, y);
1952
+ return boundary;
1900
1953
  }
1901
- e.preventDefault();
1902
- };
1903
1954
 
1904
- /**
1905
- * Calculate the bounding box for the blocks on the workspace.
1906
- * Coordinate system: workspace coordinates.
1907
- *
1908
- * @return {!Rect} Contains the position and size of the
1909
- * bounding box containing the blocks on the workspace.
1910
- */
1911
- WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
1912
- const topElements = this.getTopBoundedElements();
1913
- // There are no blocks, return empty rectangle.
1914
- if (!topElements.length) {
1915
- return new Rect(0, 0, 0, 0);
1955
+ /**
1956
+ * Clean up the workspace by ordering all the blocks in a column.
1957
+ */
1958
+ cleanUp() {
1959
+ this.setResizesEnabled(false);
1960
+ eventUtils.setGroup(true);
1961
+ const topBlocks = this.getTopBlocks(true);
1962
+ let cursorY = 0;
1963
+ for (let i = 0, block; (block = topBlocks[i]); i++) {
1964
+ if (!block.isMovable()) {
1965
+ continue;
1966
+ }
1967
+ const xy = block.getRelativeToSurfaceXY();
1968
+ block.moveBy(-xy.x, cursorY - xy.y);
1969
+ block.snapToGrid();
1970
+ cursorY = block.getRelativeToSurfaceXY().y +
1971
+ block.getHeightWidth().height +
1972
+ this.renderer_.getConstants().MIN_BLOCK_HEIGHT;
1973
+ }
1974
+ eventUtils.setGroup(false);
1975
+ this.setResizesEnabled(true);
1916
1976
  }
1917
1977
 
1918
- // Initialize boundary using the first block.
1919
- const boundary = topElements[0].getBoundingRectangle();
1920
-
1921
- // Start at 1 since the 0th block was used for initialization.
1922
- for (let i = 1; i < topElements.length; i++) {
1923
- const topElement = topElements[i];
1924
- if (topElement.isInsertionMarker && topElement.isInsertionMarker()) {
1925
- continue;
1926
- }
1927
- const blockBoundary = topElement.getBoundingRectangle();
1928
- if (blockBoundary.top < boundary.top) {
1929
- boundary.top = blockBoundary.top;
1930
- }
1931
- if (blockBoundary.bottom > boundary.bottom) {
1932
- boundary.bottom = blockBoundary.bottom;
1933
- }
1934
- if (blockBoundary.left < boundary.left) {
1935
- boundary.left = blockBoundary.left;
1978
+ /**
1979
+ * Show the context menu for the workspace.
1980
+ * @param {!Event} e Mouse event.
1981
+ * @package
1982
+ */
1983
+ showContextMenu(e) {
1984
+ if (this.options.readOnly || this.isFlyout) {
1985
+ return;
1936
1986
  }
1937
- if (blockBoundary.right > boundary.right) {
1938
- boundary.right = blockBoundary.right;
1987
+ const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
1988
+ ContextMenuRegistry.ScopeType.WORKSPACE, {workspace: this});
1989
+
1990
+ // Allow the developer to add or modify menuOptions.
1991
+ if (this.configureContextMenu) {
1992
+ this.configureContextMenu(menuOptions, e);
1939
1993
  }
1994
+
1995
+ ContextMenu.show(e, menuOptions, this.RTL);
1940
1996
  }
1941
- return boundary;
1942
- };
1943
1997
 
1944
- /**
1945
- * Clean up the workspace by ordering all the blocks in a column.
1946
- */
1947
- WorkspaceSvg.prototype.cleanUp = function() {
1948
- this.setResizesEnabled(false);
1949
- eventUtils.setGroup(true);
1950
- const topBlocks = this.getTopBlocks(true);
1951
- let cursorY = 0;
1952
- for (let i = 0, block; (block = topBlocks[i]); i++) {
1953
- if (!block.isMovable()) {
1954
- continue;
1998
+ /**
1999
+ * Modify the block tree on the existing toolbox.
2000
+ * @param {?toolbox.ToolboxDefinition} toolboxDef
2001
+ * DOM tree of toolbox contents, string of toolbox contents, or JSON
2002
+ * representing toolbox definition.
2003
+ */
2004
+ updateToolbox(toolboxDef) {
2005
+ const parsedToolboxDef = toolbox.convertToolboxDefToJson(toolboxDef);
2006
+
2007
+ if (!parsedToolboxDef) {
2008
+ if (this.options.languageTree) {
2009
+ throw Error('Can\'t nullify an existing toolbox.');
2010
+ }
2011
+ return; // No change (null to null).
2012
+ }
2013
+ if (!this.options.languageTree) {
2014
+ throw Error('Existing toolbox is null. Can\'t create new toolbox.');
1955
2015
  }
1956
- const xy = block.getRelativeToSurfaceXY();
1957
- block.moveBy(-xy.x, cursorY - xy.y);
1958
- block.snapToGrid();
1959
- cursorY = block.getRelativeToSurfaceXY().y + block.getHeightWidth().height +
1960
- this.renderer_.getConstants().MIN_BLOCK_HEIGHT;
1961
- }
1962
- eventUtils.setGroup(false);
1963
- this.setResizesEnabled(true);
1964
- };
1965
2016
 
1966
- /**
1967
- * Show the context menu for the workspace.
1968
- * @param {!Event} e Mouse event.
1969
- * @package
1970
- */
1971
- WorkspaceSvg.prototype.showContextMenu = function(e) {
1972
- if (this.options.readOnly || this.isFlyout) {
1973
- return;
2017
+ if (toolbox.hasCategories(parsedToolboxDef)) {
2018
+ if (!this.toolbox_) {
2019
+ throw Error('Existing toolbox has no categories. Can\'t change mode.');
2020
+ }
2021
+ this.options.languageTree = parsedToolboxDef;
2022
+ this.toolbox_.render(parsedToolboxDef);
2023
+ } else {
2024
+ if (!this.flyout_) {
2025
+ throw Error('Existing toolbox has categories. Can\'t change mode.');
2026
+ }
2027
+ this.options.languageTree = parsedToolboxDef;
2028
+ this.flyout_.show(parsedToolboxDef);
2029
+ }
1974
2030
  }
1975
- const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
1976
- ContextMenuRegistry.ScopeType.WORKSPACE, {workspace: this});
1977
2031
 
1978
- // Allow the developer to add or modify menuOptions.
1979
- if (this.configureContextMenu) {
1980
- this.configureContextMenu(menuOptions, e);
2032
+ /**
2033
+ * Mark this workspace as the currently focused main workspace.
2034
+ */
2035
+ markFocused() {
2036
+ if (this.options.parentWorkspace) {
2037
+ this.options.parentWorkspace.markFocused();
2038
+ } else {
2039
+ common.setMainWorkspace(this);
2040
+ // We call e.preventDefault in many event handlers which means we
2041
+ // need to explicitly grab focus (e.g from a textarea) because
2042
+ // the browser will not do it for us. How to do this is browser
2043
+ // dependent.
2044
+ this.setBrowserFocus();
2045
+ }
1981
2046
  }
1982
2047
 
1983
- ContextMenu.show(e, menuOptions, this.RTL);
1984
- };
1985
-
1986
- /**
1987
- * Modify the block tree on the existing toolbox.
1988
- * @param {?toolbox.ToolboxDefinition} toolboxDef
1989
- * DOM tree of toolbox contents, string of toolbox contents, or JSON
1990
- * representing toolbox definition.
1991
- */
1992
- WorkspaceSvg.prototype.updateToolbox = function(toolboxDef) {
1993
- const parsedToolboxDef = toolbox.convertToolboxDefToJson(toolboxDef);
1994
-
1995
- if (!parsedToolboxDef) {
1996
- if (this.options.languageTree) {
1997
- throw Error('Can\'t nullify an existing toolbox.');
2048
+ /**
2049
+ * Set the workspace to have focus in the browser.
2050
+ * @private
2051
+ */
2052
+ setBrowserFocus() {
2053
+ // Blur whatever was focused since explicitly grabbing focus below does not
2054
+ // work in Edge.
2055
+ // In IE, SVGs can't be blurred or focused. Check to make sure the current
2056
+ // focus can be blurred before doing so.
2057
+ // See https://github.com/google/blockly/issues/4440
2058
+ if (document.activeElement && document.activeElement.blur) {
2059
+ document.activeElement.blur();
2060
+ }
2061
+ try {
2062
+ // Focus the workspace SVG - this is for Chrome and Firefox.
2063
+ this.getParentSvg().focus({preventScroll: true});
2064
+ } catch (e) {
2065
+ // IE and Edge do not support focus on SVG elements. When that fails
2066
+ // above, get the injectionDiv (the workspace's parent) and focus that
2067
+ // instead. This doesn't work in Chrome.
2068
+ try {
2069
+ // In IE11, use setActive (which is IE only) so the page doesn't scroll
2070
+ // to the workspace gaining focus.
2071
+ this.getParentSvg().parentNode.setActive();
2072
+ } catch (e) {
2073
+ // setActive support was discontinued in Edge so when that fails, call
2074
+ // focus instead.
2075
+ this.getParentSvg().parentNode.focus({preventScroll: true});
2076
+ }
1998
2077
  }
1999
- return; // No change (null to null).
2000
- }
2001
- if (!this.options.languageTree) {
2002
- throw Error('Existing toolbox is null. Can\'t create new toolbox.');
2003
2078
  }
2004
2079
 
2005
- if (toolbox.hasCategories(parsedToolboxDef)) {
2006
- if (!this.toolbox_) {
2007
- throw Error('Existing toolbox has no categories. Can\'t change mode.');
2080
+ /**
2081
+ * Zooms the workspace in or out relative to/centered on the given (x, y)
2082
+ * coordinate.
2083
+ * @param {number} x X coordinate of center, in pixel units relative to the
2084
+ * top-left corner of the parentSVG.
2085
+ * @param {number} y Y coordinate of center, in pixel units relative to the
2086
+ * top-left corner of the parentSVG.
2087
+ * @param {number} amount Amount of zooming. The formula for the new scale
2088
+ * is newScale = currentScale * (scaleSpeed^amount). scaleSpeed is set in
2089
+ * the workspace options. Negative amount values zoom out, and positive
2090
+ * amount values zoom in.
2091
+ */
2092
+ zoom(x, y, amount) {
2093
+ // Scale factor.
2094
+ const speed = this.options.zoomOptions.scaleSpeed;
2095
+ let scaleChange = Math.pow(speed, amount);
2096
+ const newScale = this.scale * scaleChange;
2097
+ if (this.scale === newScale) {
2098
+ return; // No change in zoom.
2008
2099
  }
2009
- this.options.languageTree = parsedToolboxDef;
2010
- this.toolbox_.render(parsedToolboxDef);
2011
- } else {
2012
- if (!this.flyout_) {
2013
- throw Error('Existing toolbox has categories. Can\'t change mode.');
2100
+
2101
+ // Clamp scale within valid range.
2102
+ if (newScale > this.options.zoomOptions.maxScale) {
2103
+ scaleChange = this.options.zoomOptions.maxScale / this.scale;
2104
+ } else if (newScale < this.options.zoomOptions.minScale) {
2105
+ scaleChange = this.options.zoomOptions.minScale / this.scale;
2014
2106
  }
2015
- this.options.languageTree = parsedToolboxDef;
2016
- this.flyout_.show(parsedToolboxDef);
2017
- }
2018
- };
2019
2107
 
2020
- /**
2021
- * Mark this workspace as the currently focused main workspace.
2022
- */
2023
- WorkspaceSvg.prototype.markFocused = function() {
2024
- if (this.options.parentWorkspace) {
2025
- this.options.parentWorkspace.markFocused();
2026
- } else {
2027
- common.setMainWorkspace(this);
2028
- // We call e.preventDefault in many event handlers which means we
2029
- // need to explicitly grab focus (e.g from a textarea) because
2030
- // the browser will not do it for us. How to do this is browser dependent.
2031
- this.setBrowserFocus();
2108
+ // Transform the x/y coordinates from the parentSVG's space into the
2109
+ // canvas' space, so that they are in workspace units relative to the top
2110
+ // left of the visible portion of the workspace.
2111
+ let matrix = this.getCanvas().getCTM();
2112
+ let center = this.getParentSvg().createSVGPoint();
2113
+ center.x = x;
2114
+ center.y = y;
2115
+ center = center.matrixTransform(matrix.inverse());
2116
+ x = center.x;
2117
+ y = center.y;
2118
+
2119
+ // Find the new scrollX/scrollY so that the center remains in the same
2120
+ // position (relative to the center) after we zoom.
2121
+ // newScale and matrix.a should be identical (within a rounding error).
2122
+ matrix = matrix.translate(x * (1 - scaleChange), y * (1 - scaleChange))
2123
+ .scale(scaleChange);
2124
+ // scrollX and scrollY are in pixels.
2125
+ // The scrollX and scrollY still need to have absoluteLeft and absoluteTop
2126
+ // subtracted from them, but we'll leave that for setScale so that they're
2127
+ // correctly updated for the new flyout size if we have a simple toolbox.
2128
+ this.scrollX = matrix.e;
2129
+ this.scrollY = matrix.f;
2130
+ this.setScale(newScale);
2032
2131
  }
2033
- };
2034
2132
 
2035
- /**
2036
- * Set the workspace to have focus in the browser.
2037
- * @private
2038
- */
2039
- WorkspaceSvg.prototype.setBrowserFocus = function() {
2040
- // Blur whatever was focused since explicitly grabbing focus below does not
2041
- // work in Edge.
2042
- // In IE, SVGs can't be blurred or focused. Check to make sure the current
2043
- // focus can be blurred before doing so.
2044
- // See https://github.com/google/blockly/issues/4440
2045
- if (document.activeElement && document.activeElement.blur) {
2046
- document.activeElement.blur();
2047
- }
2048
- try {
2049
- // Focus the workspace SVG - this is for Chrome and Firefox.
2050
- this.getParentSvg().focus({preventScroll: true});
2051
- } catch (e) {
2052
- // IE and Edge do not support focus on SVG elements. When that fails
2053
- // above, get the injectionDiv (the workspace's parent) and focus that
2054
- // instead. This doesn't work in Chrome.
2055
- try {
2056
- // In IE11, use setActive (which is IE only) so the page doesn't scroll
2057
- // to the workspace gaining focus.
2058
- this.getParentSvg().parentNode.setActive();
2059
- } catch (e) {
2060
- // setActive support was discontinued in Edge so when that fails, call
2061
- // focus instead.
2062
- this.getParentSvg().parentNode.focus({preventScroll: true});
2133
+ /**
2134
+ * Zooming the blocks centered in the center of view with zooming in or out.
2135
+ * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
2136
+ */
2137
+ zoomCenter(type) {
2138
+ const metrics = this.getMetrics();
2139
+ let x;
2140
+ let y;
2141
+ if (this.flyout_) {
2142
+ // If you want blocks in the center of the view (visible portion of the
2143
+ // workspace) to stay centered when the size of the view decreases (i.e.
2144
+ // when the size of the flyout increases) you need the center of the
2145
+ // *blockly div* to stay in the same pixel-position.
2146
+ // Note: This only works because of how scrollCenter positions blocks.
2147
+ x = metrics.svgWidth ? metrics.svgWidth / 2 : 0;
2148
+ y = metrics.svgHeight ? metrics.svgHeight / 2 : 0;
2149
+ } else {
2150
+ x = (metrics.viewWidth / 2) + metrics.absoluteLeft;
2151
+ y = (metrics.viewHeight / 2) + metrics.absoluteTop;
2063
2152
  }
2153
+ this.zoom(x, y, type);
2064
2154
  }
2065
- };
2066
2155
 
2067
- /**
2068
- * Zooms the workspace in or out relative to/centered on the given (x, y)
2069
- * coordinate.
2070
- * @param {number} x X coordinate of center, in pixel units relative to the
2071
- * top-left corner of the parentSVG.
2072
- * @param {number} y Y coordinate of center, in pixel units relative to the
2073
- * top-left corner of the parentSVG.
2074
- * @param {number} amount Amount of zooming. The formula for the new scale
2075
- * is newScale = currentScale * (scaleSpeed^amount). scaleSpeed is set in
2076
- * the workspace options. Negative amount values zoom out, and positive
2077
- * amount values zoom in.
2078
- */
2079
- WorkspaceSvg.prototype.zoom = function(x, y, amount) {
2080
- // Scale factor.
2081
- const speed = this.options.zoomOptions.scaleSpeed;
2082
- let scaleChange = Math.pow(speed, amount);
2083
- const newScale = this.scale * scaleChange;
2084
- if (this.scale === newScale) {
2085
- return; // No change in zoom.
2086
- }
2087
-
2088
- // Clamp scale within valid range.
2089
- if (newScale > this.options.zoomOptions.maxScale) {
2090
- scaleChange = this.options.zoomOptions.maxScale / this.scale;
2091
- } else if (newScale < this.options.zoomOptions.minScale) {
2092
- scaleChange = this.options.zoomOptions.minScale / this.scale;
2093
- }
2094
-
2095
- // Transform the x/y coordinates from the parentSVG's space into the
2096
- // canvas' space, so that they are in workspace units relative to the top
2097
- // left of the visible portion of the workspace.
2098
- let matrix = this.getCanvas().getCTM();
2099
- let center = this.getParentSvg().createSVGPoint();
2100
- center.x = x;
2101
- center.y = y;
2102
- center = center.matrixTransform(matrix.inverse());
2103
- x = center.x;
2104
- y = center.y;
2105
-
2106
- // Find the new scrollX/scrollY so that the center remains in the same
2107
- // position (relative to the center) after we zoom.
2108
- // newScale and matrix.a should be identical (within a rounding error).
2109
- matrix = matrix.translate(x * (1 - scaleChange), y * (1 - scaleChange))
2110
- .scale(scaleChange);
2111
- // scrollX and scrollY are in pixels.
2112
- // The scrollX and scrollY still need to have absoluteLeft and absoluteTop
2113
- // subtracted from them, but we'll leave that for setScale so that they're
2114
- // correctly updated for the new flyout size if we have a simple toolbox.
2115
- this.scrollX = matrix.e;
2116
- this.scrollY = matrix.f;
2117
- this.setScale(newScale);
2118
- };
2156
+ /**
2157
+ * Zoom the blocks to fit in the workspace if possible.
2158
+ */
2159
+ zoomToFit() {
2160
+ if (!this.isMovable()) {
2161
+ console.warn(
2162
+ 'Tried to move a non-movable workspace. This could result' +
2163
+ ' in blocks becoming inaccessible.');
2164
+ return;
2165
+ }
2119
2166
 
2120
- /**
2121
- * Zooming the blocks centered in the center of view with zooming in or out.
2122
- * @param {number} type Type of zooming (-1 zooming out and 1 zooming in).
2123
- */
2124
- WorkspaceSvg.prototype.zoomCenter = function(type) {
2125
- const metrics = this.getMetrics();
2126
- let x;
2127
- let y;
2128
- if (this.flyout_) {
2129
- // If you want blocks in the center of the view (visible portion of the
2130
- // workspace) to stay centered when the size of the view decreases (i.e.
2131
- // when the size of the flyout increases) you need the center of the
2132
- // *blockly div* to stay in the same pixel-position.
2133
- // Note: This only works because of how scrollCenter positions blocks.
2134
- x = metrics.svgWidth ? metrics.svgWidth / 2 : 0;
2135
- y = metrics.svgHeight ? metrics.svgHeight / 2 : 0;
2136
- } else {
2137
- x = (metrics.viewWidth / 2) + metrics.absoluteLeft;
2138
- y = (metrics.viewHeight / 2) + metrics.absoluteTop;
2139
- }
2140
- this.zoom(x, y, type);
2141
- };
2167
+ const metrics = this.getMetrics();
2168
+ let workspaceWidth = metrics.viewWidth;
2169
+ let workspaceHeight = metrics.viewHeight;
2170
+ const blocksBox = this.getBlocksBoundingBox();
2171
+ let blocksWidth = blocksBox.right - blocksBox.left;
2172
+ let blocksHeight = blocksBox.bottom - blocksBox.top;
2173
+ if (!blocksWidth) {
2174
+ return; // Prevents zooming to infinity.
2175
+ }
2176
+ if (this.flyout_) {
2177
+ // We have to add the flyout size to both the workspace size and the
2178
+ // block size because the blocks we want to resize include the blocks in
2179
+ // the flyout, and the area we want to fit them includes the portion of
2180
+ // the workspace that is behind the flyout.
2181
+ if (this.horizontalLayout) {
2182
+ workspaceHeight += this.flyout_.getHeight();
2183
+ // Convert from pixels to workspace coordinates.
2184
+ blocksHeight += this.flyout_.getHeight() / this.scale;
2185
+ } else {
2186
+ workspaceWidth += this.flyout_.getWidth();
2187
+ // Convert from pixels to workspace coordinates.
2188
+ blocksWidth += this.flyout_.getWidth() / this.scale;
2189
+ }
2190
+ }
2142
2191
 
2143
- /**
2144
- * Zoom the blocks to fit in the workspace if possible.
2145
- */
2146
- WorkspaceSvg.prototype.zoomToFit = function() {
2147
- if (!this.isMovable()) {
2148
- console.warn(
2149
- 'Tried to move a non-movable workspace. This could result' +
2150
- ' in blocks becoming inaccessible.');
2151
- return;
2152
- }
2153
-
2154
- const metrics = this.getMetrics();
2155
- let workspaceWidth = metrics.viewWidth;
2156
- let workspaceHeight = metrics.viewHeight;
2157
- const blocksBox = this.getBlocksBoundingBox();
2158
- let blocksWidth = blocksBox.right - blocksBox.left;
2159
- let blocksHeight = blocksBox.bottom - blocksBox.top;
2160
- if (!blocksWidth) {
2161
- return; // Prevents zooming to infinity.
2162
- }
2163
- if (this.flyout_) {
2164
- // We have to add the flyout size to both the workspace size and the
2165
- // block size because the blocks we want to resize include the blocks in
2166
- // the flyout, and the area we want to fit them includes the portion of
2167
- // the workspace that is behind the flyout.
2168
- if (this.horizontalLayout) {
2169
- workspaceHeight += this.flyout_.getHeight();
2170
- // Convert from pixels to workspace coordinates.
2171
- blocksHeight += this.flyout_.getHeight() / this.scale;
2172
- } else {
2173
- workspaceWidth += this.flyout_.getWidth();
2174
- // Convert from pixels to workspace coordinates.
2175
- blocksWidth += this.flyout_.getWidth() / this.scale;
2192
+ // Scale Units: (pixels / workspaceUnit)
2193
+ const ratioX = workspaceWidth / blocksWidth;
2194
+ const ratioY = workspaceHeight / blocksHeight;
2195
+ eventUtils.disable();
2196
+ try {
2197
+ this.setScale(Math.min(ratioX, ratioY));
2198
+ this.scrollCenter();
2199
+ } finally {
2200
+ eventUtils.enable();
2176
2201
  }
2202
+ this.maybeFireViewportChangeEvent();
2177
2203
  }
2178
2204
 
2179
- // Scale Units: (pixels / workspaceUnit)
2180
- const ratioX = workspaceWidth / blocksWidth;
2181
- const ratioY = workspaceHeight / blocksHeight;
2182
- eventUtils.disable();
2183
- try {
2184
- this.setScale(Math.min(ratioX, ratioY));
2185
- this.scrollCenter();
2186
- } finally {
2187
- eventUtils.enable();
2205
+ /**
2206
+ * Add a transition class to the block and bubble canvas, to animate any
2207
+ * transform changes.
2208
+ * @package
2209
+ */
2210
+ beginCanvasTransition() {
2211
+ dom.addClass(
2212
+ /** @type {!SVGElement} */ (this.svgBlockCanvas_),
2213
+ 'blocklyCanvasTransitioning');
2214
+ dom.addClass(
2215
+ /** @type {!SVGElement} */ (this.svgBubbleCanvas_),
2216
+ 'blocklyCanvasTransitioning');
2188
2217
  }
2189
- this.maybeFireViewportChangeEvent();
2190
- };
2191
2218
 
2192
- /**
2193
- * Add a transition class to the block and bubble canvas, to animate any
2194
- * transform changes.
2195
- * @package
2196
- */
2197
- WorkspaceSvg.prototype.beginCanvasTransition = function() {
2198
- dom.addClass(
2199
- /** @type {!SVGElement} */ (this.svgBlockCanvas_),
2200
- 'blocklyCanvasTransitioning');
2201
- dom.addClass(
2202
- /** @type {!SVGElement} */ (this.svgBubbleCanvas_),
2203
- 'blocklyCanvasTransitioning');
2204
- };
2219
+ /**
2220
+ * Remove transition class from the block and bubble canvas.
2221
+ * @package
2222
+ */
2223
+ endCanvasTransition() {
2224
+ dom.removeClass(
2225
+ /** @type {!SVGElement} */ (this.svgBlockCanvas_),
2226
+ 'blocklyCanvasTransitioning');
2227
+ dom.removeClass(
2228
+ /** @type {!SVGElement} */ (this.svgBubbleCanvas_),
2229
+ 'blocklyCanvasTransitioning');
2230
+ }
2205
2231
 
2206
- /**
2207
- * Remove transition class from the block and bubble canvas.
2208
- * @package
2209
- */
2210
- WorkspaceSvg.prototype.endCanvasTransition = function() {
2211
- dom.removeClass(
2212
- /** @type {!SVGElement} */ (this.svgBlockCanvas_),
2213
- 'blocklyCanvasTransitioning');
2214
- dom.removeClass(
2215
- /** @type {!SVGElement} */ (this.svgBubbleCanvas_),
2216
- 'blocklyCanvasTransitioning');
2217
- };
2232
+ /**
2233
+ * Center the workspace.
2234
+ */
2235
+ scrollCenter() {
2236
+ if (!this.isMovable()) {
2237
+ console.warn(
2238
+ 'Tried to move a non-movable workspace. This could result' +
2239
+ ' in blocks becoming inaccessible.');
2240
+ return;
2241
+ }
2218
2242
 
2219
- /**
2220
- * Center the workspace.
2221
- */
2222
- WorkspaceSvg.prototype.scrollCenter = function() {
2223
- if (!this.isMovable()) {
2224
- console.warn(
2225
- 'Tried to move a non-movable workspace. This could result' +
2226
- ' in blocks becoming inaccessible.');
2227
- return;
2228
- }
2229
-
2230
- const metrics = this.getMetrics();
2231
- let x = (metrics.scrollWidth - metrics.viewWidth) / 2;
2232
- let y = (metrics.scrollHeight - metrics.viewHeight) / 2;
2233
-
2234
- // Convert from workspace directions to canvas directions.
2235
- x = -x - metrics.scrollLeft;
2236
- y = -y - metrics.scrollTop;
2237
- this.scroll(x, y);
2238
- };
2243
+ const metrics = this.getMetrics();
2244
+ let x = (metrics.scrollWidth - metrics.viewWidth) / 2;
2245
+ let y = (metrics.scrollHeight - metrics.viewHeight) / 2;
2239
2246
 
2240
- /**
2241
- * Scroll the workspace to center on the given block. If the block has other
2242
- * blocks stacked below it, the workspace will be centered on the stack.
2243
- * @param {?string} id ID of block center on.
2244
- * @public
2245
- */
2246
- WorkspaceSvg.prototype.centerOnBlock = function(id) {
2247
- if (!this.isMovable()) {
2248
- console.warn(
2249
- 'Tried to move a non-movable workspace. This could result' +
2250
- ' in blocks becoming inaccessible.');
2251
- return;
2247
+ // Convert from workspace directions to canvas directions.
2248
+ x = -x - metrics.scrollLeft;
2249
+ y = -y - metrics.scrollTop;
2250
+ this.scroll(x, y);
2252
2251
  }
2253
2252
 
2254
- const block = id ? this.getBlockById(id) : null;
2255
- if (!block) {
2256
- return;
2257
- }
2253
+ /**
2254
+ * Scroll the workspace to center on the given block. If the block has other
2255
+ * blocks stacked below it, the workspace will be centered on the stack.
2256
+ * @param {?string} id ID of block center on.
2257
+ * @public
2258
+ */
2259
+ centerOnBlock(id) {
2260
+ if (!this.isMovable()) {
2261
+ console.warn(
2262
+ 'Tried to move a non-movable workspace. This could result' +
2263
+ ' in blocks becoming inaccessible.');
2264
+ return;
2265
+ }
2258
2266
 
2259
- // XY is in workspace coordinates.
2260
- const xy = block.getRelativeToSurfaceXY();
2261
- // Height/width is in workspace units.
2262
- const heightWidth = block.getHeightWidth();
2267
+ const block = id ? this.getBlockById(id) : null;
2268
+ if (!block) {
2269
+ return;
2270
+ }
2263
2271
 
2264
- // Find the enter of the block in workspace units.
2265
- const blockCenterY = xy.y + heightWidth.height / 2;
2272
+ // XY is in workspace coordinates.
2273
+ const xy = block.getRelativeToSurfaceXY();
2274
+ // Height/width is in workspace units.
2275
+ const heightWidth = block.getHeightWidth();
2266
2276
 
2267
- // In RTL the block's position is the top right of the block, not top left.
2268
- const multiplier = this.RTL ? -1 : 1;
2269
- const blockCenterX = xy.x + (multiplier * heightWidth.width / 2);
2277
+ // Find the enter of the block in workspace units.
2278
+ const blockCenterY = xy.y + heightWidth.height / 2;
2270
2279
 
2271
- // Workspace scale, used to convert from workspace coordinates to pixels.
2272
- const scale = this.scale;
2280
+ // In RTL the block's position is the top right of the block, not top left.
2281
+ const multiplier = this.RTL ? -1 : 1;
2282
+ const blockCenterX = xy.x + (multiplier * heightWidth.width / 2);
2273
2283
 
2274
- // Center of block in pixels, relative to workspace origin (center 0,0).
2275
- // Scrolling to here would put the block in the top-left corner of the
2276
- // visible workspace.
2277
- const pixelX = blockCenterX * scale;
2278
- const pixelY = blockCenterY * scale;
2284
+ // Workspace scale, used to convert from workspace coordinates to pixels.
2285
+ const scale = this.scale;
2279
2286
 
2280
- const metrics = this.getMetrics();
2287
+ // Center of block in pixels, relative to workspace origin (center 0,0).
2288
+ // Scrolling to here would put the block in the top-left corner of the
2289
+ // visible workspace.
2290
+ const pixelX = blockCenterX * scale;
2291
+ const pixelY = blockCenterY * scale;
2281
2292
 
2282
- // viewHeight and viewWidth are in pixels.
2283
- const halfViewWidth = metrics.viewWidth / 2;
2284
- const halfViewHeight = metrics.viewHeight / 2;
2293
+ const metrics = this.getMetrics();
2285
2294
 
2286
- // Put the block in the center of the visible workspace instead.
2287
- const scrollToCenterX = pixelX - halfViewWidth;
2288
- const scrollToCenterY = pixelY - halfViewHeight;
2295
+ // viewHeight and viewWidth are in pixels.
2296
+ const halfViewWidth = metrics.viewWidth / 2;
2297
+ const halfViewHeight = metrics.viewHeight / 2;
2289
2298
 
2290
- // Convert from workspace directions to canvas directions.
2291
- const x = -scrollToCenterX;
2292
- const y = -scrollToCenterY;
2299
+ // Put the block in the center of the visible workspace instead.
2300
+ const scrollToCenterX = pixelX - halfViewWidth;
2301
+ const scrollToCenterY = pixelY - halfViewHeight;
2293
2302
 
2294
- this.scroll(x, y);
2295
- };
2303
+ // Convert from workspace directions to canvas directions.
2304
+ const x = -scrollToCenterX;
2305
+ const y = -scrollToCenterY;
2296
2306
 
2297
- /**
2298
- * Set the workspace's zoom factor.
2299
- * @param {number} newScale Zoom factor. Units: (pixels / workspaceUnit).
2300
- */
2301
- WorkspaceSvg.prototype.setScale = function(newScale) {
2302
- if (this.options.zoomOptions.maxScale &&
2303
- newScale > this.options.zoomOptions.maxScale) {
2304
- newScale = this.options.zoomOptions.maxScale;
2305
- } else if (
2306
- this.options.zoomOptions.minScale &&
2307
- newScale < this.options.zoomOptions.minScale) {
2308
- newScale = this.options.zoomOptions.minScale;
2309
- }
2310
- this.scale = newScale;
2311
-
2312
- this.hideChaff(false);
2313
- // Get the flyout, if any, whether our own or owned by the toolbox.
2314
- const flyout = this.getFlyout(false);
2315
- if (flyout && flyout.isVisible()) {
2316
- flyout.reflow();
2317
- this.recordDragTargets();
2318
- }
2319
- if (this.grid_) {
2320
- this.grid_.update(this.scale);
2307
+ this.scroll(x, y);
2321
2308
  }
2322
2309
 
2323
- // We call scroll instead of scrollbar.resize() so that we can center the
2324
- // zoom correctly without scrollbars, but scroll does not resize the
2325
- // scrollbars so we have to call resizeView/resizeContent as well.
2326
- const metrics = this.getMetrics();
2327
-
2328
- this.scrollX -= metrics.absoluteLeft;
2329
- this.scrollY -= metrics.absoluteTop;
2330
- // The scroll values and the view values are additive inverses of
2331
- // each other, so when we subtract from one we have to add to the other.
2332
- metrics.viewLeft += metrics.absoluteLeft;
2333
- metrics.viewTop += metrics.absoluteTop;
2310
+ /**
2311
+ * Set the workspace's zoom factor.
2312
+ * @param {number} newScale Zoom factor. Units: (pixels / workspaceUnit).
2313
+ */
2314
+ setScale(newScale) {
2315
+ if (this.options.zoomOptions.maxScale &&
2316
+ newScale > this.options.zoomOptions.maxScale) {
2317
+ newScale = this.options.zoomOptions.maxScale;
2318
+ } else if (
2319
+ this.options.zoomOptions.minScale &&
2320
+ newScale < this.options.zoomOptions.minScale) {
2321
+ newScale = this.options.zoomOptions.minScale;
2322
+ }
2323
+ this.scale = newScale;
2324
+
2325
+ this.hideChaff(false);
2326
+ // Get the flyout, if any, whether our own or owned by the toolbox.
2327
+ const flyout = this.getFlyout(false);
2328
+ if (flyout && flyout.isVisible()) {
2329
+ flyout.reflow();
2330
+ this.recordDragTargets();
2331
+ }
2332
+ if (this.grid_) {
2333
+ this.grid_.update(this.scale);
2334
+ }
2334
2335
 
2335
- this.scroll(this.scrollX, this.scrollY);
2336
- if (this.scrollbar) {
2337
- if (this.flyout_) {
2338
- this.scrollbar.resizeView(metrics);
2339
- } else {
2340
- this.scrollbar.resizeContent(metrics);
2336
+ // We call scroll instead of scrollbar.resize() so that we can center the
2337
+ // zoom correctly without scrollbars, but scroll does not resize the
2338
+ // scrollbars so we have to call resizeView/resizeContent as well.
2339
+ const metrics = this.getMetrics();
2340
+
2341
+ this.scrollX -= metrics.absoluteLeft;
2342
+ this.scrollY -= metrics.absoluteTop;
2343
+ // The scroll values and the view values are additive inverses of
2344
+ // each other, so when we subtract from one we have to add to the other.
2345
+ metrics.viewLeft += metrics.absoluteLeft;
2346
+ metrics.viewTop += metrics.absoluteTop;
2347
+
2348
+ this.scroll(this.scrollX, this.scrollY);
2349
+ if (this.scrollbar) {
2350
+ if (this.flyout_) {
2351
+ this.scrollbar.resizeView(metrics);
2352
+ } else {
2353
+ this.scrollbar.resizeContent(metrics);
2354
+ }
2341
2355
  }
2342
2356
  }
2343
- };
2344
2357
 
2358
+ /**
2359
+ * Get the workspace's zoom factor. If the workspace has a parent, we call
2360
+ * into the parent to get the workspace scale.
2361
+ * @return {number} The workspace zoom factor. Units: (pixels /
2362
+ * workspaceUnit).
2363
+ */
2364
+ getScale() {
2365
+ if (this.options.parentWorkspace) {
2366
+ return this.options.parentWorkspace.getScale();
2367
+ }
2368
+ return this.scale;
2369
+ }
2345
2370
 
2346
- /**
2347
- * Get the workspace's zoom factor. If the workspace has a parent, we call into
2348
- * the parent to get the workspace scale.
2349
- * @return {number} The workspace zoom factor. Units: (pixels / workspaceUnit).
2350
- */
2351
- WorkspaceSvg.prototype.getScale = function() {
2352
- if (this.options.parentWorkspace) {
2353
- return this.options.parentWorkspace.getScale();
2371
+ /**
2372
+ * Scroll the workspace to a specified offset (in pixels), keeping in the
2373
+ * workspace bounds. See comment on workspaceSvg.scrollX for more detail on
2374
+ * the meaning of these values.
2375
+ * @param {number} x Target X to scroll to.
2376
+ * @param {number} y Target Y to scroll to.
2377
+ * @package
2378
+ */
2379
+ scroll(x, y) {
2380
+ this.hideChaff(/* opt_onlyClosePopups= */ true);
2381
+
2382
+ // Keep scrolling within the bounds of the content.
2383
+ const metrics = this.getMetrics();
2384
+ // Canvas coordinates (aka scroll coordinates) have inverse directionality
2385
+ // to workspace coordinates so we have to inverse them.
2386
+ x = Math.min(x, -metrics.scrollLeft);
2387
+ y = Math.min(y, -metrics.scrollTop);
2388
+ const maxXDisplacement =
2389
+ Math.max(0, metrics.scrollWidth - metrics.viewWidth);
2390
+ const maxXScroll = metrics.scrollLeft + maxXDisplacement;
2391
+ const maxYDisplacement =
2392
+ Math.max(0, metrics.scrollHeight - metrics.viewHeight);
2393
+ const maxYScroll = metrics.scrollTop + maxYDisplacement;
2394
+ x = Math.max(x, -maxXScroll);
2395
+ y = Math.max(y, -maxYScroll);
2396
+ this.scrollX = x;
2397
+ this.scrollY = y;
2398
+
2399
+ if (this.scrollbar) {
2400
+ // The content position (displacement from the content's top-left to the
2401
+ // origin) plus the scroll position (displacement from the view's top-left
2402
+ // to the origin) gives us the distance from the view's top-left to the
2403
+ // content's top-left. Then we negate this so we get the displacement from
2404
+ // the content's top-left to the view's top-left, matching the
2405
+ // directionality of the scrollbars.
2406
+ this.scrollbar.set(
2407
+ -(x + metrics.scrollLeft), -(y + metrics.scrollTop), false);
2408
+ }
2409
+ // We have to shift the translation so that when the canvas is at 0, 0 the
2410
+ // workspace origin is not underneath the toolbox.
2411
+ x += metrics.absoluteLeft;
2412
+ y += metrics.absoluteTop;
2413
+ this.translate(x, y);
2354
2414
  }
2355
- return this.scale;
2356
- };
2357
2415
 
2358
- /**
2359
- * Scroll the workspace to a specified offset (in pixels), keeping in the
2360
- * workspace bounds. See comment on workspaceSvg.scrollX for more detail on
2361
- * the meaning of these values.
2362
- * @param {number} x Target X to scroll to.
2363
- * @param {number} y Target Y to scroll to.
2364
- * @package
2365
- */
2366
- WorkspaceSvg.prototype.scroll = function(x, y) {
2367
- this.hideChaff(/* opt_onlyClosePopups= */ true);
2368
-
2369
- // Keep scrolling within the bounds of the content.
2370
- const metrics = this.getMetrics();
2371
- // Canvas coordinates (aka scroll coordinates) have inverse directionality
2372
- // to workspace coordinates so we have to inverse them.
2373
- x = Math.min(x, -metrics.scrollLeft);
2374
- y = Math.min(y, -metrics.scrollTop);
2375
- const maxXDisplacement = Math.max(0, metrics.scrollWidth - metrics.viewWidth);
2376
- const maxXScroll = metrics.scrollLeft + maxXDisplacement;
2377
- const maxYDisplacement =
2378
- Math.max(0, metrics.scrollHeight - metrics.viewHeight);
2379
- const maxYScroll = metrics.scrollTop + maxYDisplacement;
2380
- x = Math.max(x, -maxXScroll);
2381
- y = Math.max(y, -maxYScroll);
2382
- this.scrollX = x;
2383
- this.scrollY = y;
2384
-
2385
- if (this.scrollbar) {
2386
- // The content position (displacement from the content's top-left to the
2387
- // origin) plus the scroll position (displacement from the view's top-left
2388
- // to the origin) gives us the distance from the view's top-left to the
2389
- // content's top-left. Then we negate this so we get the displacement from
2390
- // the content's top-left to the view's top-left, matching the
2391
- // directionality of the scrollbars.
2392
- this.scrollbar.set(
2393
- -(x + metrics.scrollLeft), -(y + metrics.scrollTop), false);
2394
- }
2395
- // We have to shift the translation so that when the canvas is at 0, 0 the
2396
- // workspace origin is not underneath the toolbox.
2397
- x += metrics.absoluteLeft;
2398
- y += metrics.absoluteTop;
2399
- this.translate(x, y);
2400
- };
2416
+ /**
2417
+ * Find the block on this workspace with the specified ID.
2418
+ * @param {string} id ID of block to find.
2419
+ * @return {?BlockSvg} The sought after block, or null if not found.
2420
+ * @override
2421
+ */
2422
+ getBlockById(id) {
2423
+ return /** @type {BlockSvg} */ (super.getBlockById(id));
2424
+ }
2401
2425
 
2402
- /**
2403
- * Sets the X/Y translations of a top level workspace.
2404
- * @param {!Object} xyRatio Contains an x and/or y property which is a float
2405
- * between 0 and 1 specifying the degree of scrolling.
2406
- * @private
2407
- * @this {WorkspaceSvg}
2408
- */
2409
- WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
2410
- const metrics = this.getMetrics();
2411
-
2412
- if (typeof xyRatio.x === 'number') {
2413
- this.scrollX =
2414
- -(metrics.scrollLeft +
2415
- (metrics.scrollWidth - metrics.viewWidth) * xyRatio.x);
2416
- }
2417
- if (typeof xyRatio.y === 'number') {
2418
- this.scrollY =
2419
- -(metrics.scrollTop +
2420
- (metrics.scrollHeight - metrics.viewHeight) * xyRatio.y);
2421
- }
2422
- // We have to shift the translation so that when the canvas is at 0, 0 the
2423
- // workspace origin is not underneath the toolbox.
2424
- const x = this.scrollX + metrics.absoluteLeft;
2425
- const y = this.scrollY + metrics.absoluteTop;
2426
- // We could call scroll here, but that has extra checks we don't need to do.
2427
- this.translate(x, y);
2428
- };
2426
+ /**
2427
+ * Find all blocks in workspace. Blocks are optionally sorted
2428
+ * by position; top to bottom (with slight LTR or RTL bias).
2429
+ * @param {boolean} ordered Sort the list if true.
2430
+ * @return {!Array<!BlockSvg>} Array of blocks.
2431
+ */
2432
+ getAllBlocks(ordered) {
2433
+ return /** @type {!Array<!BlockSvg>} */ (super.getAllBlocks(ordered));
2434
+ }
2429
2435
 
2430
- /**
2431
- * Find the block on this workspace with the specified ID.
2432
- * @param {string} id ID of block to find.
2433
- * @return {?BlockSvg} The sought after block, or null if not found.
2434
- * @override
2435
- */
2436
- WorkspaceSvg.prototype.getBlockById = function(id) {
2437
- return /** @type {BlockSvg} */ (
2438
- WorkspaceSvg.superClass_.getBlockById.call(this, id));
2439
- };
2436
+ /**
2437
+ * Finds the top-level blocks and returns them. Blocks are optionally sorted
2438
+ * by position; top to bottom (with slight LTR or RTL bias).
2439
+ * @param {boolean} ordered Sort the list if true.
2440
+ * @return {!Array<!BlockSvg>} The top-level block objects.
2441
+ * @override
2442
+ */
2443
+ getTopBlocks(ordered) {
2444
+ return super.getTopBlocks(ordered);
2445
+ }
2440
2446
 
2441
- /**
2442
- * Finds the top-level blocks and returns them. Blocks are optionally sorted
2443
- * by position; top to bottom (with slight LTR or RTL bias).
2444
- * @param {boolean} ordered Sort the list if true.
2445
- * @return {!Array<!BlockSvg>} The top-level block objects.
2446
- * @override
2447
- */
2448
- WorkspaceSvg.prototype.getTopBlocks = function(ordered) {
2449
- return WorkspaceSvg.superClass_.getTopBlocks.call(this, ordered);
2450
- };
2447
+ /**
2448
+ * Adds a block to the list of top blocks.
2449
+ * @param {!Block} block Block to add.
2450
+ */
2451
+ addTopBlock(block) {
2452
+ this.addTopBoundedElement(/** @type {!BlockSvg} */ (block));
2453
+ super.addTopBlock(block);
2454
+ }
2451
2455
 
2452
- /**
2453
- * Adds a block to the list of top blocks.
2454
- * @param {!Block} block Block to add.
2455
- */
2456
- WorkspaceSvg.prototype.addTopBlock = function(block) {
2457
- this.addTopBoundedElement(/** @type {!BlockSvg} */ (block));
2458
- WorkspaceSvg.superClass_.addTopBlock.call(this, block);
2459
- };
2456
+ /**
2457
+ * Removes a block from the list of top blocks.
2458
+ * @param {!Block} block Block to remove.
2459
+ */
2460
+ removeTopBlock(block) {
2461
+ this.removeTopBoundedElement(/** @type {!BlockSvg} */ (block));
2462
+ super.removeTopBlock(block);
2463
+ }
2460
2464
 
2461
- /**
2462
- * Removes a block from the list of top blocks.
2463
- * @param {!Block} block Block to remove.
2464
- */
2465
- WorkspaceSvg.prototype.removeTopBlock = function(block) {
2466
- this.removeTopBoundedElement(/** @type {!BlockSvg} */ (block));
2467
- WorkspaceSvg.superClass_.removeTopBlock.call(this, block);
2468
- };
2465
+ /**
2466
+ * Adds a comment to the list of top comments.
2467
+ * @param {!WorkspaceComment} comment comment to add.
2468
+ */
2469
+ addTopComment(comment) {
2470
+ this.addTopBoundedElement(
2471
+ /** @type {!WorkspaceCommentSvg} */ (comment));
2472
+ super.addTopComment(comment);
2473
+ }
2469
2474
 
2470
- /**
2471
- * Adds a comment to the list of top comments.
2472
- * @param {!WorkspaceComment} comment comment to add.
2473
- */
2474
- WorkspaceSvg.prototype.addTopComment = function(comment) {
2475
- this.addTopBoundedElement(
2476
- /** @type {!WorkspaceCommentSvg} */ (comment));
2477
- WorkspaceSvg.superClass_.addTopComment.call(this, comment);
2478
- };
2475
+ /**
2476
+ * Removes a comment from the list of top comments.
2477
+ * @param {!WorkspaceComment} comment comment to remove.
2478
+ */
2479
+ removeTopComment(comment) {
2480
+ this.removeTopBoundedElement(
2481
+ /** @type {!WorkspaceCommentSvg} */ (comment));
2482
+ super.removeTopComment(comment);
2483
+ }
2479
2484
 
2480
- /**
2481
- * Removes a comment from the list of top comments.
2482
- * @param {!WorkspaceComment} comment comment to remove.
2483
- */
2484
- WorkspaceSvg.prototype.removeTopComment = function(comment) {
2485
- this.removeTopBoundedElement(
2486
- /** @type {!WorkspaceCommentSvg} */ (comment));
2487
- WorkspaceSvg.superClass_.removeTopComment.call(this, comment);
2488
- };
2485
+ /**
2486
+ * Adds a bounded element to the list of top bounded elements.
2487
+ * @param {!IBoundedElement} element Bounded element to add.
2488
+ */
2489
+ addTopBoundedElement(element) {
2490
+ this.topBoundedElements_.push(element);
2491
+ }
2489
2492
 
2490
- /**
2491
- * Adds a bounded element to the list of top bounded elements.
2492
- * @param {!IBoundedElement} element Bounded element to add.
2493
- */
2494
- WorkspaceSvg.prototype.addTopBoundedElement = function(element) {
2495
- this.topBoundedElements_.push(element);
2496
- };
2493
+ /**
2494
+ * Removes a bounded element from the list of top bounded elements.
2495
+ * @param {!IBoundedElement} element Bounded element to remove.
2496
+ */
2497
+ removeTopBoundedElement(element) {
2498
+ arrayUtils.removeElem(this.topBoundedElements_, element);
2499
+ }
2497
2500
 
2498
- /**
2499
- * Removes a bounded element from the list of top bounded elements.
2500
- * @param {!IBoundedElement} element Bounded element to remove.
2501
- */
2502
- WorkspaceSvg.prototype.removeTopBoundedElement = function(element) {
2503
- arrayUtils.removeElem(this.topBoundedElements_, element);
2504
- };
2501
+ /**
2502
+ * Finds the top-level bounded elements and returns them.
2503
+ * @return {!Array<!IBoundedElement>} The top-level bounded elements.
2504
+ */
2505
+ getTopBoundedElements() {
2506
+ return [].concat(this.topBoundedElements_);
2507
+ }
2505
2508
 
2506
- /**
2507
- * Finds the top-level bounded elements and returns them.
2508
- * @return {!Array<!IBoundedElement>} The top-level bounded elements.
2509
- */
2510
- WorkspaceSvg.prototype.getTopBoundedElements = function() {
2511
- return [].concat(this.topBoundedElements_);
2512
- };
2509
+ /**
2510
+ * Update whether this workspace has resizes enabled.
2511
+ * If enabled, workspace will resize when appropriate.
2512
+ * If disabled, workspace will not resize until re-enabled.
2513
+ * Use to avoid resizing during a batch operation, for performance.
2514
+ * @param {boolean} enabled Whether resizes should be enabled.
2515
+ */
2516
+ setResizesEnabled(enabled) {
2517
+ const reenabled = (!this.resizesEnabled_ && enabled);
2518
+ this.resizesEnabled_ = enabled;
2519
+ if (reenabled) {
2520
+ // Newly enabled. Trigger a resize.
2521
+ this.resizeContents();
2522
+ }
2523
+ }
2513
2524
 
2514
- /**
2515
- * Update whether this workspace has resizes enabled.
2516
- * If enabled, workspace will resize when appropriate.
2517
- * If disabled, workspace will not resize until re-enabled.
2518
- * Use to avoid resizing during a batch operation, for performance.
2519
- * @param {boolean} enabled Whether resizes should be enabled.
2520
- */
2521
- WorkspaceSvg.prototype.setResizesEnabled = function(enabled) {
2522
- const reenabled = (!this.resizesEnabled_ && enabled);
2523
- this.resizesEnabled_ = enabled;
2524
- if (reenabled) {
2525
- // Newly enabled. Trigger a resize.
2526
- this.resizeContents();
2525
+ /**
2526
+ * Dispose of all blocks in workspace, with an optimization to prevent
2527
+ * resizes.
2528
+ */
2529
+ clear() {
2530
+ this.setResizesEnabled(false);
2531
+ super.clear();
2532
+ this.topBoundedElements_ = [];
2533
+ this.setResizesEnabled(true);
2527
2534
  }
2528
- };
2529
2535
 
2530
- /**
2531
- * Dispose of all blocks in workspace, with an optimization to prevent resizes.
2532
- */
2533
- WorkspaceSvg.prototype.clear = function() {
2534
- this.setResizesEnabled(false);
2535
- WorkspaceSvg.superClass_.clear.call(this);
2536
- this.topBoundedElements_ = [];
2537
- this.setResizesEnabled(true);
2538
- };
2536
+ /**
2537
+ * Register a callback function associated with a given key, for clicks on
2538
+ * buttons and labels in the flyout.
2539
+ * For instance, a button specified by the XML
2540
+ * <button text="create variable" callbackKey="CREATE_VARIABLE"></button>
2541
+ * should be matched by a call to
2542
+ * registerButtonCallback("CREATE_VARIABLE", yourCallbackFunction).
2543
+ * @param {string} key The name to use to look up this function.
2544
+ * @param {function(!FlyoutButton)} func The function to call when the
2545
+ * given button is clicked.
2546
+ */
2547
+ registerButtonCallback(key, func) {
2548
+ if (typeof func !== 'function') {
2549
+ throw TypeError('Button callbacks must be functions.');
2550
+ }
2551
+ this.flyoutButtonCallbacks_[key] = func;
2552
+ }
2539
2553
 
2540
- /**
2541
- * Register a callback function associated with a given key, for clicks on
2542
- * buttons and labels in the flyout.
2543
- * For instance, a button specified by the XML
2544
- * <button text="create variable" callbackKey="CREATE_VARIABLE"></button>
2545
- * should be matched by a call to
2546
- * registerButtonCallback("CREATE_VARIABLE", yourCallbackFunction).
2547
- * @param {string} key The name to use to look up this function.
2548
- * @param {function(!FlyoutButton)} func The function to call when the
2549
- * given button is clicked.
2550
- */
2551
- WorkspaceSvg.prototype.registerButtonCallback = function(key, func) {
2552
- if (typeof func !== 'function') {
2553
- throw TypeError('Button callbacks must be functions.');
2554
+ /**
2555
+ * Get the callback function associated with a given key, for clicks on
2556
+ * buttons and labels in the flyout.
2557
+ * @param {string} key The name to use to look up the function.
2558
+ * @return {?function(!FlyoutButton)} The function corresponding to the
2559
+ * given key for this workspace; null if no callback is registered.
2560
+ */
2561
+ getButtonCallback(key) {
2562
+ const result = this.flyoutButtonCallbacks_[key];
2563
+ return result ? result : null;
2554
2564
  }
2555
- this.flyoutButtonCallbacks_[key] = func;
2556
- };
2557
2565
 
2558
- /**
2559
- * Get the callback function associated with a given key, for clicks on buttons
2560
- * and labels in the flyout.
2561
- * @param {string} key The name to use to look up the function.
2562
- * @return {?function(!FlyoutButton)} The function corresponding to the
2563
- * given key for this workspace; null if no callback is registered.
2564
- */
2565
- WorkspaceSvg.prototype.getButtonCallback = function(key) {
2566
- const result = this.flyoutButtonCallbacks_[key];
2567
- return result ? result : null;
2568
- };
2566
+ /**
2567
+ * Remove a callback for a click on a button in the flyout.
2568
+ * @param {string} key The name associated with the callback function.
2569
+ */
2570
+ removeButtonCallback(key) {
2571
+ this.flyoutButtonCallbacks_[key] = null;
2572
+ }
2569
2573
 
2570
- /**
2571
- * Remove a callback for a click on a button in the flyout.
2572
- * @param {string} key The name associated with the callback function.
2573
- */
2574
- WorkspaceSvg.prototype.removeButtonCallback = function(key) {
2575
- this.flyoutButtonCallbacks_[key] = null;
2576
- };
2574
+ /**
2575
+ * Register a callback function associated with a given key, for populating
2576
+ * custom toolbox categories in this workspace. See the variable and
2577
+ * procedure categories as an example.
2578
+ * @param {string} key The name to use to look up this function.
2579
+ * @param {function(!WorkspaceSvg): !toolbox.FlyoutDefinition} func The
2580
+ * function to call when the given toolbox category is opened.
2581
+ */
2582
+ registerToolboxCategoryCallback(key, func) {
2583
+ if (typeof func !== 'function') {
2584
+ throw TypeError('Toolbox category callbacks must be functions.');
2585
+ }
2586
+ this.toolboxCategoryCallbacks_[key] = func;
2587
+ }
2577
2588
 
2578
- /**
2579
- * Register a callback function associated with a given key, for populating
2580
- * custom toolbox categories in this workspace. See the variable and procedure
2581
- * categories as an example.
2582
- * @param {string} key The name to use to look up this function.
2583
- * @param {function(!Workspace): !toolbox.FlyoutDefinition} func The function to
2584
- * call when the given toolbox category is opened.
2585
- */
2586
- WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key, func) {
2587
- if (typeof func !== 'function') {
2588
- throw TypeError('Toolbox category callbacks must be functions.');
2589
+ /**
2590
+ * Get the callback function associated with a given key, for populating
2591
+ * custom toolbox categories in this workspace.
2592
+ * @param {string} key The name to use to look up the function.
2593
+ * @return {?function(!WorkspaceSvg): !toolbox.FlyoutDefinition} The function
2594
+ * corresponding to the given key for this workspace, or null if no
2595
+ * function is registered.
2596
+ */
2597
+ getToolboxCategoryCallback(key) {
2598
+ return this.toolboxCategoryCallbacks_[key] || null;
2589
2599
  }
2590
- this.toolboxCategoryCallbacks_[key] = func;
2591
- };
2592
2600
 
2593
- /**
2594
- * Get the callback function associated with a given key, for populating
2595
- * custom toolbox categories in this workspace.
2596
- * @param {string} key The name to use to look up the function.
2597
- * @return {?function(!Workspace): !toolbox.FlyoutDefinition} The function
2598
- * corresponding to the given key for this workspace, or null if no function
2599
- * is registered.
2600
- */
2601
- WorkspaceSvg.prototype.getToolboxCategoryCallback = function(key) {
2602
- return this.toolboxCategoryCallbacks_[key] || null;
2603
- };
2601
+ /**
2602
+ * Remove a callback for a click on a custom category's name in the toolbox.
2603
+ * @param {string} key The name associated with the callback function.
2604
+ */
2605
+ removeToolboxCategoryCallback(key) {
2606
+ this.toolboxCategoryCallbacks_[key] = null;
2607
+ }
2604
2608
 
2605
- /**
2606
- * Remove a callback for a click on a custom category's name in the toolbox.
2607
- * @param {string} key The name associated with the callback function.
2608
- */
2609
- WorkspaceSvg.prototype.removeToolboxCategoryCallback = function(key) {
2610
- this.toolboxCategoryCallbacks_[key] = null;
2611
- };
2609
+ /**
2610
+ * Look up the gesture that is tracking this touch stream on this workspace.
2611
+ * May create a new gesture.
2612
+ * @param {!Event} e Mouse event or touch event.
2613
+ * @return {?TouchGesture} The gesture that is tracking this touch
2614
+ * stream, or null if no valid gesture exists.
2615
+ * @package
2616
+ */
2617
+ getGesture(e) {
2618
+ const isStart =
2619
+ (e.type === 'mousedown' || e.type === 'touchstart' ||
2620
+ e.type === 'pointerdown');
2621
+
2622
+ const gesture = this.currentGesture_;
2623
+ if (gesture) {
2624
+ if (isStart && gesture.hasStarted()) {
2625
+ console.warn('Tried to start the same gesture twice.');
2626
+ // That's funny. We must have missed a mouse up.
2627
+ // Cancel it, rather than try to retrieve all of the state we need.
2628
+ gesture.cancel();
2629
+ return null;
2630
+ }
2631
+ return gesture;
2632
+ }
2612
2633
 
2613
- /**
2614
- * Look up the gesture that is tracking this touch stream on this workspace.
2615
- * May create a new gesture.
2616
- * @param {!Event} e Mouse event or touch event.
2617
- * @return {?TouchGesture} The gesture that is tracking this touch
2618
- * stream, or null if no valid gesture exists.
2619
- * @package
2620
- */
2621
- WorkspaceSvg.prototype.getGesture = function(e) {
2622
- const isStart =
2623
- (e.type === 'mousedown' || e.type === 'touchstart' ||
2624
- e.type === 'pointerdown');
2625
-
2626
- const gesture = this.currentGesture_;
2627
- if (gesture) {
2628
- if (isStart && gesture.hasStarted()) {
2629
- console.warn('Tried to start the same gesture twice.');
2630
- // That's funny. We must have missed a mouse up.
2631
- // Cancel it, rather than try to retrieve all of the state we need.
2632
- gesture.cancel();
2633
- return null;
2634
+ // No gesture existed on this workspace, but this looks like the start of a
2635
+ // new gesture.
2636
+ if (isStart) {
2637
+ this.currentGesture_ = new TouchGesture(e, this);
2638
+ return this.currentGesture_;
2634
2639
  }
2635
- return gesture;
2640
+ // No gesture existed and this event couldn't be the start of a new gesture.
2641
+ return null;
2636
2642
  }
2637
2643
 
2638
- // No gesture existed on this workspace, but this looks like the start of a
2639
- // new gesture.
2640
- if (isStart) {
2641
- this.currentGesture_ = new TouchGesture(e, this);
2642
- return this.currentGesture_;
2644
+ /**
2645
+ * Clear the reference to the current gesture.
2646
+ * @package
2647
+ */
2648
+ clearGesture() {
2649
+ this.currentGesture_ = null;
2643
2650
  }
2644
- // No gesture existed and this event couldn't be the start of a new gesture.
2645
- return null;
2646
- };
2647
2651
 
2648
- /**
2649
- * Clear the reference to the current gesture.
2650
- * @package
2651
- */
2652
- WorkspaceSvg.prototype.clearGesture = function() {
2653
- this.currentGesture_ = null;
2654
- };
2652
+ /**
2653
+ * Cancel the current gesture, if one exists.
2654
+ * @package
2655
+ */
2656
+ cancelCurrentGesture() {
2657
+ if (this.currentGesture_) {
2658
+ this.currentGesture_.cancel();
2659
+ }
2660
+ }
2655
2661
 
2656
- /**
2657
- * Cancel the current gesture, if one exists.
2658
- * @package
2659
- */
2660
- WorkspaceSvg.prototype.cancelCurrentGesture = function() {
2661
- if (this.currentGesture_) {
2662
- this.currentGesture_.cancel();
2662
+ /**
2663
+ * Get the audio manager for this workspace.
2664
+ * @return {!WorkspaceAudio} The audio manager for this workspace.
2665
+ */
2666
+ getAudioManager() {
2667
+ return this.audioManager_;
2663
2668
  }
2664
- };
2665
2669
 
2666
- /**
2667
- * Get the audio manager for this workspace.
2668
- * @return {!WorkspaceAudio} The audio manager for this workspace.
2669
- */
2670
- WorkspaceSvg.prototype.getAudioManager = function() {
2671
- return this.audioManager_;
2672
- };
2670
+ /**
2671
+ * Get the grid object for this workspace, or null if there is none.
2672
+ * @return {?Grid} The grid object for this workspace.
2673
+ * @package
2674
+ */
2675
+ getGrid() {
2676
+ return this.grid_;
2677
+ }
2673
2678
 
2674
- /**
2675
- * Get the grid object for this workspace, or null if there is none.
2676
- * @return {?Grid} The grid object for this workspace.
2677
- * @package
2678
- */
2679
- WorkspaceSvg.prototype.getGrid = function() {
2680
- return this.grid_;
2681
- };
2679
+ /**
2680
+ * Close tooltips, context menus, dropdown selections, etc.
2681
+ * @param {boolean=} opt_onlyClosePopups Whether only popups should be closed.
2682
+ */
2683
+ hideChaff(opt_onlyClosePopups) {
2684
+ Tooltip.hide();
2685
+ WidgetDiv.hide();
2686
+ dropDownDiv.hideWithoutAnimation();
2682
2687
 
2683
- /**
2684
- * Close tooltips, context menus, dropdown selections, etc.
2685
- * @param {boolean=} opt_onlyClosePopups Whether only popups should be closed.
2686
- */
2687
- WorkspaceSvg.prototype.hideChaff = function(opt_onlyClosePopups) {
2688
- Tooltip.hide();
2689
- WidgetDiv.hide();
2690
- DropDownDiv.hideWithoutAnimation();
2691
-
2692
- const onlyClosePopups = !!opt_onlyClosePopups;
2693
- const autoHideables = this.getComponentManager().getComponents(
2694
- ComponentManager.Capability.AUTOHIDEABLE, true);
2695
- autoHideables.forEach(
2696
- (autoHideable) => autoHideable.autoHide(onlyClosePopups));
2697
- };
2688
+ const onlyClosePopups = !!opt_onlyClosePopups;
2689
+ const autoHideables = this.getComponentManager().getComponents(
2690
+ ComponentManager.Capability.AUTOHIDEABLE, true);
2691
+ autoHideables.forEach(
2692
+ (autoHideable) => autoHideable.autoHide(onlyClosePopups));
2693
+ }
2694
+
2695
+ /**
2696
+ * Sets the X/Y translations of a top level workspace.
2697
+ * @param {!Object} xyRatio Contains an x and/or y property which is a float
2698
+ * between 0 and 1 specifying the degree of scrolling.
2699
+ * @private
2700
+ * @this {WorkspaceSvg}
2701
+ */
2702
+ static setTopLevelWorkspaceMetrics_(xyRatio) {
2703
+ const metrics = this.getMetrics();
2704
+
2705
+ if (typeof xyRatio.x === 'number') {
2706
+ this.scrollX =
2707
+ -(metrics.scrollLeft +
2708
+ (metrics.scrollWidth - metrics.viewWidth) * xyRatio.x);
2709
+ }
2710
+ if (typeof xyRatio.y === 'number') {
2711
+ this.scrollY =
2712
+ -(metrics.scrollTop +
2713
+ (metrics.scrollHeight - metrics.viewHeight) * xyRatio.y);
2714
+ }
2715
+ // We have to shift the translation so that when the canvas is at 0, 0 the
2716
+ // workspace origin is not underneath the toolbox.
2717
+ const x = this.scrollX + metrics.absoluteLeft;
2718
+ const y = this.scrollY + metrics.absoluteTop;
2719
+ // We could call scroll here, but that has extra checks we don't need to do.
2720
+ this.translate(x, y);
2721
+ }
2722
+ }
2698
2723
 
2699
2724
  /**
2700
2725
  * Size the workspace when the contents change. This also updates