blockly 7.20211209.4 → 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
@@ -21,713 +21,732 @@ goog.module('Blockly.FieldDropdown');
21
21
 
22
22
  const aria = goog.require('Blockly.utils.aria');
23
23
  const dom = goog.require('Blockly.utils.dom');
24
+ const dropDownDiv = goog.require('Blockly.dropDownDiv');
24
25
  const fieldRegistry = goog.require('Blockly.fieldRegistry');
25
- const object = goog.require('Blockly.utils.object');
26
26
  const parsing = goog.require('Blockly.utils.parsing');
27
27
  const userAgent = goog.require('Blockly.utils.userAgent');
28
28
  const utilsString = goog.require('Blockly.utils.string');
29
29
  const {Coordinate} = goog.require('Blockly.utils.Coordinate');
30
- const {DropDownDiv} = goog.require('Blockly.DropDownDiv');
31
30
  const {Field} = goog.require('Blockly.Field');
32
31
  const {MenuItem} = goog.require('Blockly.MenuItem');
33
32
  const {Menu} = goog.require('Blockly.Menu');
33
+ /* eslint-disable-next-line no-unused-vars */
34
+ const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
34
35
  const {Svg} = goog.require('Blockly.utils.Svg');
35
36
 
36
37
 
37
38
  /**
38
39
  * Class for an editable dropdown field.
39
- * @param {(!Array<!Array>|!Function)} menuGenerator A non-empty array of
40
- * options for a dropdown list, or a function which generates these options.
41
- * @param {Function=} opt_validator A function that is called to validate
42
- * changes to the field's value. Takes in a language-neutral dropdown
43
- * option & returns a validated language-neutral dropdown option, or null to
44
- * abort the change.
45
- * @param {Object=} opt_config A map of options used to configure the field.
46
- * See the [field creation documentation]{@link
47
- * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}
48
- * for a list of properties this parameter supports.
49
40
  * @extends {Field}
50
- * @constructor
51
- * @throws {TypeError} If `menuGenerator` options are incorrectly structured.
52
41
  * @alias Blockly.FieldDropdown
53
42
  */
54
- const FieldDropdown = function(menuGenerator, opt_validator, opt_config) {
55
- if (typeof menuGenerator !== 'function') {
56
- validateOptions(menuGenerator);
57
- }
58
-
43
+ class FieldDropdown extends Field {
59
44
  /**
60
- * An array of options for a dropdown list,
61
- * or a function which generates these options.
62
- * @type {(!Array<!Array>|
63
- * !function(this:FieldDropdown): !Array<!Array>)}
64
- * @protected
45
+ * @param {(!Array<!Array>|!Function|!Sentinel)} menuGenerator
46
+ * A non-empty array of options for a dropdown list, or a function which
47
+ * generates these options.
48
+ * Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
49
+ * subclasses that want to handle configuration and setting the field
50
+ * value after their own constructors have run).
51
+ * @param {Function=} opt_validator A function that is called to validate
52
+ * changes to the field's value. Takes in a language-neutral dropdown
53
+ * option & returns a validated language-neutral dropdown option, or null
54
+ * to abort the change.
55
+ * @param {Object=} opt_config A map of options used to configure the field.
56
+ * See the [field creation documentation]{@link
57
+ * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}
58
+ * for a list of properties this parameter supports.
59
+ * @throws {TypeError} If `menuGenerator` options are incorrectly structured.
65
60
  */
66
- this.menuGenerator_ = menuGenerator;
61
+ constructor(menuGenerator, opt_validator, opt_config) {
62
+ super(Field.SKIP_SETUP);
63
+
64
+ /**
65
+ * A reference to the currently selected menu item.
66
+ * @type {?MenuItem}
67
+ * @private
68
+ */
69
+ this.selectedMenuItem_ = null;
70
+
71
+ /**
72
+ * The dropdown menu.
73
+ * @type {?Menu}
74
+ * @protected
75
+ */
76
+ this.menu_ = null;
77
+
78
+ /**
79
+ * SVG image element if currently selected option is an image, or null.
80
+ * @type {?SVGImageElement}
81
+ * @private
82
+ */
83
+ this.imageElement_ = null;
84
+
85
+ /**
86
+ * Tspan based arrow element.
87
+ * @type {?SVGTSpanElement}
88
+ * @private
89
+ */
90
+ this.arrow_ = null;
91
+
92
+ /**
93
+ * SVG based arrow element.
94
+ * @type {?SVGElement}
95
+ * @private
96
+ */
97
+ this.svgArrow_ = null;
98
+
99
+ /**
100
+ * Serializable fields are saved by the serializer, non-serializable fields
101
+ * are not. Editable fields should also be serializable.
102
+ * @type {boolean}
103
+ */
104
+ this.SERIALIZABLE = true;
105
+
106
+ /**
107
+ * Mouse cursor style when over the hotspot that initiates the editor.
108
+ * @type {string}
109
+ */
110
+ this.CURSOR = 'default';
111
+
112
+
113
+ // If we pass SKIP_SETUP, don't do *anything* with the menu generator.
114
+ if (menuGenerator === Field.SKIP_SETUP) return;
115
+
116
+ if (Array.isArray(menuGenerator)) {
117
+ validateOptions(menuGenerator);
118
+ }
119
+
120
+ /**
121
+ * An array of options for a dropdown list,
122
+ * or a function which generates these options.
123
+ * @type {(!Array<!Array>|!function(this:FieldDropdown): !Array<!Array>)}
124
+ * @protected
125
+ */
126
+ this.menuGenerator_ =
127
+ /**
128
+ * @type {(!Array<!Array>|
129
+ * !function(this:FieldDropdown):!Array<!Array>)}
130
+ */
131
+ (menuGenerator);
132
+
133
+ /**
134
+ * A cache of the most recently generated options.
135
+ * @type {Array<!Array<string>>}
136
+ * @private
137
+ */
138
+ this.generatedOptions_ = null;
139
+
140
+ /**
141
+ * The prefix field label, of common words set after options are trimmed.
142
+ * @type {?string}
143
+ * @package
144
+ */
145
+ this.prefixField = null;
146
+
147
+ /**
148
+ * The suffix field label, of common words set after options are trimmed.
149
+ * @type {?string}
150
+ * @package
151
+ */
152
+ this.suffixField = null;
153
+
154
+ this.trimOptions_();
155
+
156
+ /**
157
+ * The currently selected option. The field is initialized with the
158
+ * first option selected.
159
+ * @type {!Array<string|!ImageProperties>}
160
+ * @private
161
+ */
162
+ this.selectedOption_ = this.getOptions(false)[0];
163
+
164
+ if (opt_config) this.configure_(opt_config);
165
+ this.setValue(this.selectedOption_[1]);
166
+ if (opt_validator) this.setValidator(opt_validator);
167
+ }
67
168
 
68
169
  /**
69
- * A cache of the most recently generated options.
70
- * @type {Array<!Array<string>>}
71
- * @private
170
+ * Sets the field's value based on the given XML element. Should only be
171
+ * called by Blockly.Xml.
172
+ * @param {!Element} fieldElement The element containing info about the
173
+ * field's state.
174
+ * @package
72
175
  */
73
- this.generatedOptions_ = null;
176
+ fromXml(fieldElement) {
177
+ if (this.isOptionListDynamic()) {
178
+ this.getOptions(false);
179
+ }
180
+ this.setValue(fieldElement.textContent);
181
+ }
74
182
 
75
183
  /**
76
- * The prefix field label, of common words set after options are trimmed.
77
- * @type {?string}
184
+ * Sets the field's value based on the given state.
185
+ * @param {*} state The state to apply to the dropdown field.
186
+ * @override
78
187
  * @package
79
188
  */
80
- this.prefixField = null;
189
+ loadState(state) {
190
+ if (this.loadLegacyState(FieldDropdown, state)) {
191
+ return;
192
+ }
193
+ if (this.isOptionListDynamic()) {
194
+ this.getOptions(false);
195
+ }
196
+ this.setValue(state);
197
+ }
81
198
 
82
199
  /**
83
- * The suffix field label, of common words set after options are trimmed.
84
- * @type {?string}
200
+ * Create the block UI for this dropdown.
85
201
  * @package
86
202
  */
87
- this.suffixField = null;
203
+ initView() {
204
+ if (this.shouldAddBorderRect_()) {
205
+ this.createBorderRect_();
206
+ } else {
207
+ this.clickTarget_ = this.sourceBlock_.getSvgRoot();
208
+ }
209
+ this.createTextElement_();
88
210
 
89
- this.trimOptions_();
211
+ this.imageElement_ = dom.createSvgElement(Svg.IMAGE, {}, this.fieldGroup_);
90
212
 
91
- /**
92
- * The currently selected option. The field is initialized with the
93
- * first option selected.
94
- * @type {!Object}
95
- * @private
96
- */
97
- this.selectedOption_ = this.getOptions(false)[0];
213
+ if (this.getConstants().FIELD_DROPDOWN_SVG_ARROW) {
214
+ this.createSVGArrow_();
215
+ } else {
216
+ this.createTextArrow_();
217
+ }
98
218
 
99
- // Call parent's constructor.
100
- FieldDropdown.superClass_.constructor.call(
101
- this, this.selectedOption_[1], opt_validator, opt_config);
219
+ if (this.borderRect_) {
220
+ dom.addClass(this.borderRect_, 'blocklyDropdownRect');
221
+ }
222
+ }
102
223
 
103
224
  /**
104
- * A reference to the currently selected menu item.
105
- * @type {?MenuItem}
106
- * @private
225
+ * Whether or not the dropdown should add a border rect.
226
+ * @return {boolean} True if the dropdown field should add a border rect.
227
+ * @protected
107
228
  */
108
- this.selectedMenuItem_ = null;
229
+ shouldAddBorderRect_() {
230
+ return !this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
231
+ (this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
232
+ !this.sourceBlock_.isShadow());
233
+ }
109
234
 
110
235
  /**
111
- * The dropdown menu.
112
- * @type {?Menu}
236
+ * Create a tspan based arrow.
113
237
  * @protected
114
238
  */
115
- this.menu_ = null;
239
+ createTextArrow_() {
240
+ this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
241
+ this.arrow_.appendChild(document.createTextNode(
242
+ this.sourceBlock_.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
243
+ ' ' + FieldDropdown.ARROW_CHAR));
244
+ if (this.sourceBlock_.RTL) {
245
+ this.textElement_.insertBefore(this.arrow_, this.textContent_);
246
+ } else {
247
+ this.textElement_.appendChild(this.arrow_);
248
+ }
249
+ }
116
250
 
117
251
  /**
118
- * SVG image element if currently selected option is an image, or null.
119
- * @type {?SVGImageElement}
120
- * @private
252
+ * Create an SVG based arrow.
253
+ * @protected
121
254
  */
122
- this.imageElement_ = null;
255
+ createSVGArrow_() {
256
+ this.svgArrow_ = dom.createSvgElement(
257
+ Svg.IMAGE, {
258
+ 'height': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
259
+ 'width': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
260
+ },
261
+ this.fieldGroup_);
262
+ this.svgArrow_.setAttributeNS(
263
+ dom.XLINK_NS, 'xlink:href',
264
+ this.getConstants().FIELD_DROPDOWN_SVG_ARROW_DATAURI);
265
+ }
123
266
 
124
267
  /**
125
- * Tspan based arrow element.
126
- * @type {?SVGTSpanElement}
127
- * @private
268
+ * Create a dropdown menu under the text.
269
+ * @param {Event=} opt_e Optional mouse event that triggered the field to
270
+ * open, or undefined if triggered programmatically.
271
+ * @protected
128
272
  */
129
- this.arrow_ = null;
273
+ showEditor_(opt_e) {
274
+ this.dropdownCreate_();
275
+ if (opt_e && typeof opt_e.clientX === 'number') {
276
+ this.menu_.openingCoords = new Coordinate(opt_e.clientX, opt_e.clientY);
277
+ } else {
278
+ this.menu_.openingCoords = null;
279
+ }
130
280
 
131
- /**
132
- * SVG based arrow element.
133
- * @type {?SVGElement}
134
- * @private
135
- */
136
- this.svgArrow_ = null;
137
- };
138
- object.inherits(FieldDropdown, Field);
281
+ // Remove any pre-existing elements in the dropdown.
282
+ dropDownDiv.clearContent();
283
+ // Element gets created in render.
284
+ this.menu_.render(dropDownDiv.getContentDiv());
285
+ const menuElement = /** @type {!Element} */ (this.menu_.getElement());
286
+ dom.addClass(menuElement, 'blocklyDropdownMenu');
287
+
288
+ if (this.getConstants().FIELD_DROPDOWN_COLOURED_DIV) {
289
+ const primaryColour = (this.sourceBlock_.isShadow()) ?
290
+ this.sourceBlock_.getParent().getColour() :
291
+ this.sourceBlock_.getColour();
292
+ const borderColour = (this.sourceBlock_.isShadow()) ?
293
+ this.sourceBlock_.getParent().style.colourTertiary :
294
+ this.sourceBlock_.style.colourTertiary;
295
+ dropDownDiv.setColour(primaryColour, borderColour);
296
+ }
139
297
 
140
- /**
141
- * Dropdown image properties.
142
- * @typedef {{
143
- * src:string,
144
- * alt:string,
145
- * width:number,
146
- * height:number
147
- * }}
148
- */
149
- FieldDropdown.ImageProperties;
298
+ dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
150
299
 
151
- /**
152
- * Construct a FieldDropdown from a JSON arg object.
153
- * @param {!Object} options A JSON object with options (options).
154
- * @return {!FieldDropdown} The new field instance.
155
- * @package
156
- * @nocollapse
157
- */
158
- FieldDropdown.fromJson = function(options) {
159
- // `this` might be a subclass of FieldDropdown if that class doesn't override
160
- // the static fromJson method.
161
- return new this(options['options'], undefined, options);
162
- };
300
+ // Focusing needs to be handled after the menu is rendered and positioned.
301
+ // Otherwise it will cause a page scroll to get the misplaced menu in
302
+ // view. See issue #1329.
303
+ this.menu_.focus();
163
304
 
164
- /**
165
- * Sets the field's value based on the given XML element. Should only be
166
- * called by Blockly.Xml.
167
- * @param {!Element} fieldElement The element containing info about the
168
- * field's state.
169
- * @package
170
- */
171
- FieldDropdown.prototype.fromXml = function(fieldElement) {
172
- if (this.isOptionListDynamic()) {
173
- this.getOptions(false);
174
- }
175
- this.setValue(fieldElement.textContent);
176
- };
305
+ if (this.selectedMenuItem_) {
306
+ this.menu_.setHighlighted(this.selectedMenuItem_);
307
+ }
177
308
 
178
- /**
179
- * Sets the field's value based on the given state.
180
- * @param {*} state The state to apply to the dropdown field.
181
- * @override
182
- * @package
183
- */
184
- FieldDropdown.prototype.loadState = function(state) {
185
- if (this.loadLegacyState(FieldDropdown, state)) {
186
- return;
309
+ this.applyColour();
187
310
  }
188
- if (this.isOptionListDynamic()) {
189
- this.getOptions(false);
190
- }
191
- this.setValue(state);
192
- };
193
-
194
- /**
195
- * Serializable fields are saved by the XML renderer, non-serializable fields
196
- * are not. Editable fields should also be serializable.
197
- * @type {boolean}
198
- */
199
- FieldDropdown.prototype.SERIALIZABLE = true;
200
-
201
- /**
202
- * Horizontal distance that a checkmark overhangs the dropdown.
203
- */
204
- FieldDropdown.CHECKMARK_OVERHANG = 25;
205
-
206
- /**
207
- * Maximum height of the dropdown menu, as a percentage of the viewport height.
208
- */
209
- FieldDropdown.MAX_MENU_HEIGHT_VH = 0.45;
210
311
 
211
- /**
212
- * The y offset from the top of the field to the top of the image, if an image
213
- * is selected.
214
- * @type {number}
215
- * @const
216
- */
217
- const IMAGE_Y_OFFSET = 5;
218
-
219
- /**
220
- * The total vertical padding above and below an image.
221
- * @type {number}
222
- * @const
223
- */
224
- const IMAGE_Y_PADDING = IMAGE_Y_OFFSET * 2;
225
-
226
- /**
227
- * Android can't (in 2014) display "▾", so use "▼" instead.
228
- */
229
- FieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '\u25BC' : '\u25BE';
230
-
231
- /**
232
- * Mouse cursor style when over the hotspot that initiates the editor.
233
- */
234
- FieldDropdown.prototype.CURSOR = 'default';
235
-
236
- /**
237
- * Create the block UI for this dropdown.
238
- * @package
239
- */
240
- FieldDropdown.prototype.initView = function() {
241
- if (this.shouldAddBorderRect_()) {
242
- this.createBorderRect_();
243
- } else {
244
- this.clickTarget_ = this.sourceBlock_.getSvgRoot();
312
+ /**
313
+ * Create the dropdown editor.
314
+ * @private
315
+ */
316
+ dropdownCreate_() {
317
+ const menu = new Menu();
318
+ menu.setRole(aria.Role.LISTBOX);
319
+ this.menu_ = menu;
320
+
321
+ const options = this.getOptions(false);
322
+ this.selectedMenuItem_ = null;
323
+ for (let i = 0; i < options.length; i++) {
324
+ let content = options[i][0]; // Human-readable text or image.
325
+ const value = options[i][1]; // Language-neutral value.
326
+ if (typeof content === 'object') {
327
+ // An image, not text.
328
+ const image = new Image(content['width'], content['height']);
329
+ image.src = content['src'];
330
+ image.alt = content['alt'] || '';
331
+ content = image;
332
+ }
333
+ const menuItem = new MenuItem(content, value);
334
+ menuItem.setRole(aria.Role.OPTION);
335
+ menuItem.setRightToLeft(this.sourceBlock_.RTL);
336
+ menuItem.setCheckable(true);
337
+ menu.addChild(menuItem);
338
+ menuItem.setChecked(value === this.value_);
339
+ if (value === this.value_) {
340
+ this.selectedMenuItem_ = menuItem;
341
+ }
342
+ menuItem.onAction(this.handleMenuActionEvent_, this);
343
+ }
245
344
  }
246
- this.createTextElement_();
247
345
 
248
- this.imageElement_ = dom.createSvgElement(Svg.IMAGE, {}, this.fieldGroup_);
249
-
250
- if (this.getConstants().FIELD_DROPDOWN_SVG_ARROW) {
251
- this.createSVGArrow_();
252
- } else {
253
- this.createTextArrow_();
346
+ /**
347
+ * Disposes of events and DOM-references belonging to the dropdown editor.
348
+ * @private
349
+ */
350
+ dropdownDispose_() {
351
+ if (this.menu_) {
352
+ this.menu_.dispose();
353
+ }
354
+ this.menu_ = null;
355
+ this.selectedMenuItem_ = null;
356
+ this.applyColour();
254
357
  }
255
358
 
256
- if (this.borderRect_) {
257
- dom.addClass(this.borderRect_, 'blocklyDropdownRect');
359
+ /**
360
+ * Handle an action in the dropdown menu.
361
+ * @param {!MenuItem} menuItem The MenuItem selected within menu.
362
+ * @private
363
+ */
364
+ handleMenuActionEvent_(menuItem) {
365
+ dropDownDiv.hideIfOwner(this, true);
366
+ this.onItemSelected_(/** @type {!Menu} */ (this.menu_), menuItem);
258
367
  }
259
- };
260
-
261
- /**
262
- * Whether or not the dropdown should add a border rect.
263
- * @return {boolean} True if the dropdown field should add a border rect.
264
- * @protected
265
- */
266
- FieldDropdown.prototype.shouldAddBorderRect_ = function() {
267
- return !this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
268
- (this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
269
- !this.sourceBlock_.isShadow());
270
- };
271
368
 
272
- /**
273
- * Create a tspan based arrow.
274
- * @protected
275
- */
276
- FieldDropdown.prototype.createTextArrow_ = function() {
277
- this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
278
- this.arrow_.appendChild(document.createTextNode(
279
- this.sourceBlock_.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
280
- ' ' + FieldDropdown.ARROW_CHAR));
281
- if (this.sourceBlock_.RTL) {
282
- this.textElement_.insertBefore(this.arrow_, this.textContent_);
283
- } else {
284
- this.textElement_.appendChild(this.arrow_);
369
+ /**
370
+ * Handle the selection of an item in the dropdown menu.
371
+ * @param {!Menu} menu The Menu component clicked.
372
+ * @param {!MenuItem} menuItem The MenuItem selected within menu.
373
+ * @protected
374
+ */
375
+ onItemSelected_(menu, menuItem) {
376
+ this.setValue(menuItem.getValue());
285
377
  }
286
- };
287
378
 
288
- /**
289
- * Create an SVG based arrow.
290
- * @protected
291
- */
292
- FieldDropdown.prototype.createSVGArrow_ = function() {
293
- this.svgArrow_ = dom.createSvgElement(
294
- Svg.IMAGE, {
295
- 'height': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
296
- 'width': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
297
- },
298
- this.fieldGroup_);
299
- this.svgArrow_.setAttributeNS(
300
- dom.XLINK_NS, 'xlink:href',
301
- this.getConstants().FIELD_DROPDOWN_SVG_ARROW_DATAURI);
302
- };
379
+ /**
380
+ * Factor out common words in statically defined options.
381
+ * Create prefix and/or suffix labels.
382
+ * @private
383
+ */
384
+ trimOptions_() {
385
+ const options = this.menuGenerator_;
386
+ if (!Array.isArray(options)) {
387
+ return;
388
+ }
389
+ let hasImages = false;
390
+
391
+ // Localize label text and image alt text.
392
+ for (let i = 0; i < options.length; i++) {
393
+ const label = options[i][0];
394
+ if (typeof label === 'string') {
395
+ options[i][0] = parsing.replaceMessageReferences(label);
396
+ } else {
397
+ if (label.alt !== null) {
398
+ options[i][0].alt = parsing.replaceMessageReferences(label.alt);
399
+ }
400
+ hasImages = true;
401
+ }
402
+ }
403
+ if (hasImages || options.length < 2) {
404
+ return; // Do nothing if too few items or at least one label is an image.
405
+ }
406
+ const strings = [];
407
+ for (let i = 0; i < options.length; i++) {
408
+ strings.push(options[i][0]);
409
+ }
410
+ const shortest = utilsString.shortestStringLength(strings);
411
+ const prefixLength = utilsString.commonWordPrefix(strings, shortest);
412
+ const suffixLength = utilsString.commonWordSuffix(strings, shortest);
413
+ if (!prefixLength && !suffixLength) {
414
+ return;
415
+ }
416
+ if (shortest <= prefixLength + suffixLength) {
417
+ // One or more strings will entirely vanish if we proceed. Abort.
418
+ return;
419
+ }
420
+ if (prefixLength) {
421
+ this.prefixField = strings[0].substring(0, prefixLength - 1);
422
+ }
423
+ if (suffixLength) {
424
+ this.suffixField = strings[0].substr(1 - suffixLength);
425
+ }
303
426
 
304
- /**
305
- * Create a dropdown menu under the text.
306
- * @param {Event=} opt_e Optional mouse event that triggered the field to open,
307
- * or undefined if triggered programmatically.
308
- * @protected
309
- */
310
- FieldDropdown.prototype.showEditor_ = function(opt_e) {
311
- this.dropdownCreate_();
312
- if (opt_e && typeof opt_e.clientX === 'number') {
313
- this.menu_.openingCoords = new Coordinate(opt_e.clientX, opt_e.clientY);
314
- } else {
315
- this.menu_.openingCoords = null;
427
+ this.menuGenerator_ =
428
+ FieldDropdown.applyTrim_(options, prefixLength, suffixLength);
316
429
  }
317
430
 
318
- // Remove any pre-existing elements in the dropdown.
319
- DropDownDiv.clearContent();
320
- // Element gets created in render.
321
- this.menu_.render(DropDownDiv.getContentDiv());
322
- const menuElement = /** @type {!Element} */ (this.menu_.getElement());
323
- dom.addClass(menuElement, 'blocklyDropdownMenu');
324
-
325
- if (this.getConstants().FIELD_DROPDOWN_COLOURED_DIV) {
326
- const primaryColour = (this.sourceBlock_.isShadow()) ?
327
- this.sourceBlock_.getParent().getColour() :
328
- this.sourceBlock_.getColour();
329
- const borderColour = (this.sourceBlock_.isShadow()) ?
330
- this.sourceBlock_.getParent().style.colourTertiary :
331
- this.sourceBlock_.style.colourTertiary;
332
- DropDownDiv.setColour(primaryColour, borderColour);
431
+ /**
432
+ * @return {boolean} True if the option list is generated by a function.
433
+ * Otherwise false.
434
+ */
435
+ isOptionListDynamic() {
436
+ return typeof this.menuGenerator_ === 'function';
333
437
  }
334
438
 
335
- DropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
336
-
337
- // Focusing needs to be handled after the menu is rendered and positioned.
338
- // Otherwise it will cause a page scroll to get the misplaced menu in
339
- // view. See issue #1329.
340
- this.menu_.focus();
341
-
342
- if (this.selectedMenuItem_) {
343
- this.menu_.setHighlighted(this.selectedMenuItem_);
439
+ /**
440
+ * Return a list of the options for this dropdown.
441
+ * @param {boolean=} opt_useCache For dynamic options, whether or not to use
442
+ * the cached options or to re-generate them.
443
+ * @return {!Array<!Array>} A non-empty array of option tuples:
444
+ * (human-readable text or image, language-neutral name).
445
+ * @throws {TypeError} If generated options are incorrectly structured.
446
+ */
447
+ getOptions(opt_useCache) {
448
+ if (this.isOptionListDynamic()) {
449
+ if (!this.generatedOptions_ || !opt_useCache) {
450
+ this.generatedOptions_ = this.menuGenerator_.call(this);
451
+ validateOptions(this.generatedOptions_);
452
+ }
453
+ return this.generatedOptions_;
454
+ }
455
+ return /** @type {!Array<!Array<string>>} */ (this.menuGenerator_);
344
456
  }
345
457
 
346
- this.applyColour();
347
- };
348
-
349
- /**
350
- * Create the dropdown editor.
351
- * @private
352
- */
353
- FieldDropdown.prototype.dropdownCreate_ = function() {
354
- const menu = new Menu();
355
- menu.setRole(aria.Role.LISTBOX);
356
- this.menu_ = menu;
357
-
358
- const options = this.getOptions(false);
359
- this.selectedMenuItem_ = null;
360
- for (let i = 0; i < options.length; i++) {
361
- let content = options[i][0]; // Human-readable text or image.
362
- const value = options[i][1]; // Language-neutral value.
363
- if (typeof content === 'object') {
364
- // An image, not text.
365
- const image = new Image(content['width'], content['height']);
366
- image.src = content['src'];
367
- image.alt = content['alt'] || '';
368
- content = image;
369
- }
370
- const menuItem = new MenuItem(content, value);
371
- menuItem.setRole(aria.Role.OPTION);
372
- menuItem.setRightToLeft(this.sourceBlock_.RTL);
373
- menuItem.setCheckable(true);
374
- menu.addChild(menuItem);
375
- menuItem.setChecked(value === this.value_);
376
- if (value === this.value_) {
377
- this.selectedMenuItem_ = menuItem;
378
- }
379
- menuItem.onAction(this.handleMenuActionEvent_, this);
458
+ /**
459
+ * Ensure that the input value is a valid language-neutral option.
460
+ * @param {*=} opt_newValue The input value.
461
+ * @return {?string} A valid language-neutral option, or null if invalid.
462
+ * @protected
463
+ */
464
+ doClassValidation_(opt_newValue) {
465
+ let isValueValid = false;
466
+ const options = this.getOptions(true);
467
+ for (let i = 0, option; (option = options[i]); i++) {
468
+ // Options are tuples of human-readable text and language-neutral values.
469
+ if (option[1] === opt_newValue) {
470
+ isValueValid = true;
471
+ break;
472
+ }
473
+ }
474
+ if (!isValueValid) {
475
+ if (this.sourceBlock_) {
476
+ console.warn(
477
+ 'Cannot set the dropdown\'s value to an unavailable option.' +
478
+ ' Block type: ' + this.sourceBlock_.type +
479
+ ', Field name: ' + this.name + ', Value: ' + opt_newValue);
480
+ }
481
+ return null;
482
+ }
483
+ return /** @type {string} */ (opt_newValue);
380
484
  }
381
- };
382
485
 
383
- /**
384
- * Disposes of events and DOM-references belonging to the dropdown editor.
385
- * @private
386
- */
387
- FieldDropdown.prototype.dropdownDispose_ = function() {
388
- if (this.menu_) {
389
- this.menu_.dispose();
486
+ /**
487
+ * Update the value of this dropdown field.
488
+ * @param {*} newValue The value to be saved. The default validator guarantees
489
+ * that this is one of the valid dropdown options.
490
+ * @protected
491
+ */
492
+ doValueUpdate_(newValue) {
493
+ super.doValueUpdate_(newValue);
494
+ const options = this.getOptions(true);
495
+ for (let i = 0, option; (option = options[i]); i++) {
496
+ if (option[1] === this.value_) {
497
+ this.selectedOption_ = option;
498
+ }
499
+ }
390
500
  }
391
- this.menu_ = null;
392
- this.selectedMenuItem_ = null;
393
- this.applyColour();
394
- };
395
501
 
396
- /**
397
- * Handle an action in the dropdown menu.
398
- * @param {!MenuItem} menuItem The MenuItem selected within menu.
399
- * @private
400
- */
401
- FieldDropdown.prototype.handleMenuActionEvent_ = function(menuItem) {
402
- DropDownDiv.hideIfOwner(this, true);
403
- this.onItemSelected_(/** @type {!Menu} */ (this.menu_), menuItem);
404
- };
502
+ /**
503
+ * Updates the dropdown arrow to match the colour/style of the block.
504
+ * @package
505
+ */
506
+ applyColour() {
507
+ if (this.borderRect_) {
508
+ this.borderRect_.setAttribute(
509
+ 'stroke', this.sourceBlock_.style.colourTertiary);
510
+ if (this.menu_) {
511
+ this.borderRect_.setAttribute(
512
+ 'fill', this.sourceBlock_.style.colourTertiary);
513
+ } else {
514
+ this.borderRect_.setAttribute('fill', 'transparent');
515
+ }
516
+ }
517
+ // Update arrow's colour.
518
+ if (this.sourceBlock_ && this.arrow_) {
519
+ if (this.sourceBlock_.isShadow()) {
520
+ this.arrow_.style.fill = this.sourceBlock_.style.colourSecondary;
521
+ } else {
522
+ this.arrow_.style.fill = this.sourceBlock_.style.colourPrimary;
523
+ }
524
+ }
525
+ }
405
526
 
406
- /**
407
- * Handle the selection of an item in the dropdown menu.
408
- * @param {!Menu} menu The Menu component clicked.
409
- * @param {!MenuItem} menuItem The MenuItem selected within menu.
410
- * @protected
411
- */
412
- FieldDropdown.prototype.onItemSelected_ = function(menu, menuItem) {
413
- this.setValue(menuItem.getValue());
414
- };
527
+ /**
528
+ * Draws the border with the correct width.
529
+ * @protected
530
+ */
531
+ render_() {
532
+ // Hide both elements.
533
+ this.textContent_.nodeValue = '';
534
+ this.imageElement_.style.display = 'none';
535
+
536
+ // Show correct element.
537
+ const option = this.selectedOption_ && this.selectedOption_[0];
538
+ if (option && typeof option === 'object') {
539
+ this.renderSelectedImage_(
540
+ /** @type {!ImageProperties} */ (option));
541
+ } else {
542
+ this.renderSelectedText_();
543
+ }
415
544
 
416
- /**
417
- * Factor out common words in statically defined options.
418
- * Create prefix and/or suffix labels.
419
- * @private
420
- */
421
- FieldDropdown.prototype.trimOptions_ = function() {
422
- const options = this.menuGenerator_;
423
- if (!Array.isArray(options)) {
424
- return;
545
+ this.positionBorderRect_();
425
546
  }
426
- let hasImages = false;
427
547
 
428
- // Localize label text and image alt text.
429
- for (let i = 0; i < options.length; i++) {
430
- const label = options[i][0];
431
- if (typeof label === 'string') {
432
- options[i][0] = parsing.replaceMessageReferences(label);
548
+ /**
549
+ * Renders the selected option, which must be an image.
550
+ * @param {!ImageProperties} imageJson Selected
551
+ * option that must be an image.
552
+ * @private
553
+ */
554
+ renderSelectedImage_(imageJson) {
555
+ this.imageElement_.style.display = '';
556
+ this.imageElement_.setAttributeNS(
557
+ dom.XLINK_NS, 'xlink:href', imageJson.src);
558
+ this.imageElement_.setAttribute('height', imageJson.height);
559
+ this.imageElement_.setAttribute('width', imageJson.width);
560
+
561
+ const imageHeight = Number(imageJson.height);
562
+ const imageWidth = Number(imageJson.width);
563
+
564
+ // Height and width include the border rect.
565
+ const hasBorder = !!this.borderRect_;
566
+ const height = Math.max(
567
+ hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
568
+ imageHeight + IMAGE_Y_PADDING);
569
+ const xPadding =
570
+ hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
571
+ let arrowWidth = 0;
572
+ if (this.svgArrow_) {
573
+ arrowWidth = this.positionSVGArrow_(
574
+ imageWidth + xPadding,
575
+ height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
433
576
  } else {
434
- if (label.alt !== null) {
435
- options[i][0].alt = parsing.replaceMessageReferences(label.alt);
436
- }
437
- hasImages = true;
577
+ arrowWidth = dom.getFastTextWidth(
578
+ /** @type {!SVGTSpanElement} */ (this.arrow_),
579
+ this.getConstants().FIELD_TEXT_FONTSIZE,
580
+ this.getConstants().FIELD_TEXT_FONTWEIGHT,
581
+ this.getConstants().FIELD_TEXT_FONTFAMILY);
438
582
  }
439
- }
440
- if (hasImages || options.length < 2) {
441
- return; // Do nothing if too few items or at least one label is an image.
442
- }
443
- const strings = [];
444
- for (let i = 0; i < options.length; i++) {
445
- strings.push(options[i][0]);
446
- }
447
- const shortest = utilsString.shortestStringLength(strings);
448
- const prefixLength = utilsString.commonWordPrefix(strings, shortest);
449
- const suffixLength = utilsString.commonWordSuffix(strings, shortest);
450
- if (!prefixLength && !suffixLength) {
451
- return;
452
- }
453
- if (shortest <= prefixLength + suffixLength) {
454
- // One or more strings will entirely vanish if we proceed. Abort.
455
- return;
456
- }
457
- if (prefixLength) {
458
- this.prefixField = strings[0].substring(0, prefixLength - 1);
459
- }
460
- if (suffixLength) {
461
- this.suffixField = strings[0].substr(1 - suffixLength);
462
- }
583
+ this.size_.width = imageWidth + arrowWidth + xPadding * 2;
584
+ this.size_.height = height;
463
585
 
464
- this.menuGenerator_ =
465
- FieldDropdown.applyTrim_(options, prefixLength, suffixLength);
466
- };
586
+ let arrowX = 0;
587
+ if (this.sourceBlock_.RTL) {
588
+ const imageX = xPadding + arrowWidth;
589
+ this.imageElement_.setAttribute('x', imageX);
590
+ } else {
591
+ arrowX = imageWidth + arrowWidth;
592
+ this.textElement_.setAttribute('text-anchor', 'end');
593
+ this.imageElement_.setAttribute('x', xPadding);
594
+ }
595
+ this.imageElement_.setAttribute('y', height / 2 - imageHeight / 2);
467
596
 
468
- /**
469
- * Use the calculated prefix and suffix lengths to trim all of the options in
470
- * the given array.
471
- * @param {!Array<!Array>} options Array of option tuples:
472
- * (human-readable text or image, language-neutral name).
473
- * @param {number} prefixLength The length of the common prefix.
474
- * @param {number} suffixLength The length of the common suffix
475
- * @return {!Array<!Array>} A new array with all of the option text trimmed.
476
- */
477
- FieldDropdown.applyTrim_ = function(options, prefixLength, suffixLength) {
478
- const newOptions = [];
479
- // Remove the prefix and suffix from the options.
480
- for (let i = 0; i < options.length; i++) {
481
- let text = options[i][0];
482
- const value = options[i][1];
483
- text = text.substring(prefixLength, text.length - suffixLength);
484
- newOptions[i] = [text, value];
597
+ this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth);
485
598
  }
486
- return newOptions;
487
- };
488
599
 
489
- /**
490
- * @return {boolean} True if the option list is generated by a function.
491
- * Otherwise false.
492
- */
493
- FieldDropdown.prototype.isOptionListDynamic = function() {
494
- return typeof this.menuGenerator_ === 'function';
495
- };
496
-
497
- /**
498
- * Return a list of the options for this dropdown.
499
- * @param {boolean=} opt_useCache For dynamic options, whether or not to use the
500
- * cached options or to re-generate them.
501
- * @return {!Array<!Array>} A non-empty array of option tuples:
502
- * (human-readable text or image, language-neutral name).
503
- * @throws {TypeError} If generated options are incorrectly structured.
504
- */
505
- FieldDropdown.prototype.getOptions = function(opt_useCache) {
506
- if (this.isOptionListDynamic()) {
507
- if (!this.generatedOptions_ || !opt_useCache) {
508
- this.generatedOptions_ = this.menuGenerator_.call(this);
509
- validateOptions(this.generatedOptions_);
600
+ /**
601
+ * Renders the selected option, which must be text.
602
+ * @private
603
+ */
604
+ renderSelectedText_() {
605
+ // Retrieves the selected option to display through getText_.
606
+ this.textContent_.nodeValue = this.getDisplayText_();
607
+ dom.addClass(
608
+ /** @type {!Element} */ (this.textElement_), 'blocklyDropdownText');
609
+ this.textElement_.setAttribute('text-anchor', 'start');
610
+
611
+ // Height and width include the border rect.
612
+ const hasBorder = !!this.borderRect_;
613
+ const height = Math.max(
614
+ hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
615
+ this.getConstants().FIELD_TEXT_HEIGHT);
616
+ const textWidth = dom.getFastTextWidth(
617
+ this.textElement_, this.getConstants().FIELD_TEXT_FONTSIZE,
618
+ this.getConstants().FIELD_TEXT_FONTWEIGHT,
619
+ this.getConstants().FIELD_TEXT_FONTFAMILY);
620
+ const xPadding =
621
+ hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
622
+ let arrowWidth = 0;
623
+ if (this.svgArrow_) {
624
+ arrowWidth = this.positionSVGArrow_(
625
+ textWidth + xPadding,
626
+ height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
510
627
  }
511
- return this.generatedOptions_;
628
+ this.size_.width = textWidth + arrowWidth + xPadding * 2;
629
+ this.size_.height = height;
630
+
631
+ this.positionTextElement_(xPadding, textWidth);
512
632
  }
513
- return /** @type {!Array<!Array<string>>} */ (this.menuGenerator_);
514
- };
515
633
 
516
- /**
517
- * Ensure that the input value is a valid language-neutral option.
518
- * @param {*=} opt_newValue The input value.
519
- * @return {?string} A valid language-neutral option, or null if invalid.
520
- * @protected
521
- */
522
- FieldDropdown.prototype.doClassValidation_ = function(opt_newValue) {
523
- let isValueValid = false;
524
- const options = this.getOptions(true);
525
- for (let i = 0, option; (option = options[i]); i++) {
526
- // Options are tuples of human-readable text and language-neutral values.
527
- if (option[1] === opt_newValue) {
528
- isValueValid = true;
529
- break;
634
+ /**
635
+ * Position a drop-down arrow at the appropriate location at render-time.
636
+ * @param {number} x X position the arrow is being rendered at, in px.
637
+ * @param {number} y Y position the arrow is being rendered at, in px.
638
+ * @return {number} Amount of space the arrow is taking up, in px.
639
+ * @private
640
+ */
641
+ positionSVGArrow_(x, y) {
642
+ if (!this.svgArrow_) {
643
+ return 0;
530
644
  }
645
+ const hasBorder = !!this.borderRect_;
646
+ const xPadding =
647
+ hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
648
+ const textPadding = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_PADDING;
649
+ const svgArrowSize = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE;
650
+ const arrowX = this.sourceBlock_.RTL ? xPadding : x + textPadding;
651
+ this.svgArrow_.setAttribute(
652
+ 'transform', 'translate(' + arrowX + ',' + y + ')');
653
+ return svgArrowSize + textPadding;
531
654
  }
532
- if (!isValueValid) {
533
- if (this.sourceBlock_) {
534
- console.warn(
535
- 'Cannot set the dropdown\'s value to an unavailable option.' +
536
- ' Block type: ' + this.sourceBlock_.type +
537
- ', Field name: ' + this.name + ', Value: ' + opt_newValue);
538
- }
539
- return null;
540
- }
541
- return /** @type {string} */ (opt_newValue);
542
- };
543
655
 
544
- /**
545
- * Update the value of this dropdown field.
546
- * @param {*} newValue The value to be saved. The default validator guarantees
547
- * that this is one of the valid dropdown options.
548
- * @protected
549
- */
550
- FieldDropdown.prototype.doValueUpdate_ = function(newValue) {
551
- FieldDropdown.superClass_.doValueUpdate_.call(this, newValue);
552
- const options = this.getOptions(true);
553
- for (let i = 0, option; (option = options[i]); i++) {
554
- if (option[1] === this.value_) {
555
- this.selectedOption_ = option;
656
+ /**
657
+ * Use the `getText_` developer hook to override the field's text
658
+ * representation. Get the selected option text. If the selected option is an
659
+ * image we return the image alt text.
660
+ * @return {?string} Selected option text.
661
+ * @protected
662
+ * @override
663
+ */
664
+ getText_() {
665
+ if (!this.selectedOption_) {
666
+ return null;
667
+ }
668
+ const option = this.selectedOption_[0];
669
+ if (typeof option === 'object') {
670
+ return option['alt'];
556
671
  }
672
+ return option;
557
673
  }
558
- };
559
674
 
560
- /**
561
- * Updates the dropdown arrow to match the colour/style of the block.
562
- * @package
563
- */
564
- FieldDropdown.prototype.applyColour = function() {
565
- if (this.borderRect_) {
566
- this.borderRect_.setAttribute(
567
- 'stroke', this.sourceBlock_.style.colourTertiary);
568
- if (this.menu_) {
569
- this.borderRect_.setAttribute(
570
- 'fill', this.sourceBlock_.style.colourTertiary);
571
- } else {
572
- this.borderRect_.setAttribute('fill', 'transparent');
573
- }
675
+ /**
676
+ * Construct a FieldDropdown from a JSON arg object.
677
+ * @param {!Object} options A JSON object with options (options).
678
+ * @return {!FieldDropdown} The new field instance.
679
+ * @package
680
+ * @nocollapse
681
+ */
682
+ static fromJson(options) {
683
+ // `this` might be a subclass of FieldDropdown if that class doesn't
684
+ // override the static fromJson method.
685
+ return new this(options['options'], undefined, options);
574
686
  }
575
- // Update arrow's colour.
576
- if (this.sourceBlock_ && this.arrow_) {
577
- if (this.sourceBlock_.isShadow()) {
578
- this.arrow_.style.fill = this.sourceBlock_.style.colourSecondary;
579
- } else {
580
- this.arrow_.style.fill = this.sourceBlock_.style.colourPrimary;
687
+
688
+ /**
689
+ * Use the calculated prefix and suffix lengths to trim all of the options in
690
+ * the given array.
691
+ * @param {!Array<!Array>} options Array of option tuples:
692
+ * (human-readable text or image, language-neutral name).
693
+ * @param {number} prefixLength The length of the common prefix.
694
+ * @param {number} suffixLength The length of the common suffix
695
+ * @return {!Array<!Array>} A new array with all of the option text trimmed.
696
+ */
697
+ static applyTrim_(options, prefixLength, suffixLength) {
698
+ const newOptions = [];
699
+ // Remove the prefix and suffix from the options.
700
+ for (let i = 0; i < options.length; i++) {
701
+ let text = options[i][0];
702
+ const value = options[i][1];
703
+ text = text.substring(prefixLength, text.length - suffixLength);
704
+ newOptions[i] = [text, value];
581
705
  }
706
+ return newOptions;
582
707
  }
583
- };
708
+ }
584
709
 
585
710
  /**
586
- * Draws the border with the correct width.
587
- * @protected
711
+ * Dropdown image properties.
712
+ * @typedef {{
713
+ * src:string,
714
+ * alt:string,
715
+ * width:number,
716
+ * height:number
717
+ * }}
588
718
  */
589
- FieldDropdown.prototype.render_ = function() {
590
- // Hide both elements.
591
- this.textContent_.nodeValue = '';
592
- this.imageElement_.style.display = 'none';
593
-
594
- // Show correct element.
595
- const option = this.selectedOption_ && this.selectedOption_[0];
596
- if (option && typeof option === 'object') {
597
- this.renderSelectedImage_(
598
- /** @type {!FieldDropdown.ImageProperties} */ (option));
599
- } else {
600
- this.renderSelectedText_();
601
- }
602
-
603
- this.positionBorderRect_();
604
- };
719
+ let ImageProperties; // eslint-disable-line no-unused-vars
605
720
 
606
721
  /**
607
- * Renders the selected option, which must be an image.
608
- * @param {!FieldDropdown.ImageProperties} imageJson Selected
609
- * option that must be an image.
610
- * @private
722
+ * Horizontal distance that a checkmark overhangs the dropdown.
611
723
  */
612
- FieldDropdown.prototype.renderSelectedImage_ = function(imageJson) {
613
- this.imageElement_.style.display = '';
614
- this.imageElement_.setAttributeNS(dom.XLINK_NS, 'xlink:href', imageJson.src);
615
- this.imageElement_.setAttribute('height', imageJson.height);
616
- this.imageElement_.setAttribute('width', imageJson.width);
617
-
618
- const imageHeight = Number(imageJson.height);
619
- const imageWidth = Number(imageJson.width);
620
-
621
- // Height and width include the border rect.
622
- const hasBorder = !!this.borderRect_;
623
- const height = Math.max(
624
- hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
625
- imageHeight + IMAGE_Y_PADDING);
626
- const xPadding =
627
- hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
628
- let arrowWidth = 0;
629
- if (this.svgArrow_) {
630
- arrowWidth = this.positionSVGArrow_(
631
- imageWidth + xPadding,
632
- height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
633
- } else {
634
- arrowWidth = dom.getFastTextWidth(
635
- /** @type {!SVGTSpanElement} */ (this.arrow_),
636
- this.getConstants().FIELD_TEXT_FONTSIZE,
637
- this.getConstants().FIELD_TEXT_FONTWEIGHT,
638
- this.getConstants().FIELD_TEXT_FONTFAMILY);
639
- }
640
- this.size_.width = imageWidth + arrowWidth + xPadding * 2;
641
- this.size_.height = height;
642
-
643
- let arrowX = 0;
644
- if (this.sourceBlock_.RTL) {
645
- const imageX = xPadding + arrowWidth;
646
- this.imageElement_.setAttribute('x', imageX);
647
- } else {
648
- arrowX = imageWidth + arrowWidth;
649
- this.textElement_.setAttribute('text-anchor', 'end');
650
- this.imageElement_.setAttribute('x', xPadding);
651
- }
652
- this.imageElement_.setAttribute('y', height / 2 - imageHeight / 2);
653
-
654
- this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth);
655
- };
724
+ FieldDropdown.CHECKMARK_OVERHANG = 25;
656
725
 
657
726
  /**
658
- * Renders the selected option, which must be text.
659
- * @private
727
+ * Maximum height of the dropdown menu, as a percentage of the viewport height.
660
728
  */
661
- FieldDropdown.prototype.renderSelectedText_ = function() {
662
- // Retrieves the selected option to display through getText_.
663
- this.textContent_.nodeValue = this.getDisplayText_();
664
- dom.addClass(
665
- /** @type {!Element} */ (this.textElement_), 'blocklyDropdownText');
666
- this.textElement_.setAttribute('text-anchor', 'start');
667
-
668
- // Height and width include the border rect.
669
- const hasBorder = !!this.borderRect_;
670
- const height = Math.max(
671
- hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
672
- this.getConstants().FIELD_TEXT_HEIGHT);
673
- const textWidth = dom.getFastTextWidth(
674
- this.textElement_, this.getConstants().FIELD_TEXT_FONTSIZE,
675
- this.getConstants().FIELD_TEXT_FONTWEIGHT,
676
- this.getConstants().FIELD_TEXT_FONTFAMILY);
677
- const xPadding =
678
- hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
679
- let arrowWidth = 0;
680
- if (this.svgArrow_) {
681
- arrowWidth = this.positionSVGArrow_(
682
- textWidth + xPadding,
683
- height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
684
- }
685
- this.size_.width = textWidth + arrowWidth + xPadding * 2;
686
- this.size_.height = height;
729
+ FieldDropdown.MAX_MENU_HEIGHT_VH = 0.45;
687
730
 
688
- this.positionTextElement_(xPadding, textWidth);
689
- };
731
+ /**
732
+ * The y offset from the top of the field to the top of the image, if an image
733
+ * is selected.
734
+ * @type {number}
735
+ * @const
736
+ */
737
+ const IMAGE_Y_OFFSET = 5;
690
738
 
691
739
  /**
692
- * Position a drop-down arrow at the appropriate location at render-time.
693
- * @param {number} x X position the arrow is being rendered at, in px.
694
- * @param {number} y Y position the arrow is being rendered at, in px.
695
- * @return {number} Amount of space the arrow is taking up, in px.
696
- * @private
740
+ * The total vertical padding above and below an image.
741
+ * @type {number}
742
+ * @const
697
743
  */
698
- FieldDropdown.prototype.positionSVGArrow_ = function(x, y) {
699
- if (!this.svgArrow_) {
700
- return 0;
701
- }
702
- const hasBorder = !!this.borderRect_;
703
- const xPadding =
704
- hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
705
- const textPadding = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_PADDING;
706
- const svgArrowSize = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE;
707
- const arrowX = this.sourceBlock_.RTL ? xPadding : x + textPadding;
708
- this.svgArrow_.setAttribute(
709
- 'transform', 'translate(' + arrowX + ',' + y + ')');
710
- return svgArrowSize + textPadding;
711
- };
744
+ const IMAGE_Y_PADDING = IMAGE_Y_OFFSET * 2;
712
745
 
713
746
  /**
714
- * Use the `getText_` developer hook to override the field's text
715
- * representation. Get the selected option text. If the selected option is an
716
- * image we return the image alt text.
717
- * @return {?string} Selected option text.
718
- * @protected
719
- * @override
747
+ * Android can't (in 2014) display "▾", so use "▼" instead.
720
748
  */
721
- FieldDropdown.prototype.getText_ = function() {
722
- if (!this.selectedOption_) {
723
- return null;
724
- }
725
- const option = this.selectedOption_[0];
726
- if (typeof option === 'object') {
727
- return option['alt'];
728
- }
729
- return option;
730
- };
749
+ FieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '\u25BC' : '\u25BE';
731
750
 
732
751
  /**
733
752
  * Validates the data structure to be processed as an options list.